Assistant guide
This file is for AI assistants (Claude, Cursor, Copilot, etc.) working on the Brimble docs. It captures the conventions, the hard rules, and the pitfalls that have already been worked through. Read it before making changes. If you are a human reading this, the operational stuff for editors lives inREADME.md. This file is the why and the how to not break things.
1. What this repo is
The source for Brimble’s public documentation site, built with Mintlify.- Pages are
.mdxfiles organized by feature area (projects/,domains/,environments/, etc.). - Navigation, theming, fonts, and brand colors live in
docs.jsonat the root. - Run locally with
mintlify dev.
main after Mintlify validates docs.json and the MDX files.
2. The single hardest rule
The codebase is the only source of truth. Never document something based on what feels right, what other PaaS docs say, or what plausibly should be true. Verify against the relevant repo first. Brimble services and where to look:| Concern | Repo |
|---|---|
| Project lifecycle, deployments, domains, env vars, autoscaling, webhook emission, sandbox API surface | ~/Documents/Archive/PERSONAL/work/core |
| Build pipeline, builder, build cache | ~/Documents/Archive/PERSONAL/work/runner |
| Database and sandbox provisioning, backups | ~/Documents/Archive/PERSONAL/work/heracle |
| Edge proxy, request routing, password protection, MCP auth | ~/Documents/Archive/PERSONAL/work/proxy |
| DNS service, Caddy config, CoreDNS Corefile | ~/Documents/Archive/PERSONAL/work/dns |
| Subscriptions, Stripe, build minutes, refunds | ~/Documents/Archive/PERSONAL/work/brimble-payment |
| Login, OAuth, 2FA, passkeys | ~/Documents/Archive/PERSONAL/work/auth-service |
| Webhook delivery (factory + dispatch), email templates | ~/Documents/Archive/PERSONAL/work/brimble-email |
| Dashboard UI, all settings panels, plan defaults, sandbox modals | ~/Documents/brimble/apps/dashboard/src |
| Marketing site styling, tokens | ~/Documents/brimble/apps/web/src |
3. Document only what users can do
The mongoose documents, RabbitMQ events, internal queues, and HashiCorp stack details are not the public contract. Public docs cover:- What the user sees and does in the dashboard.
- Behaviors that the user observes from outside (URLs, headers, response codes, webhook payload shapes after the factory transforms them).
- The CLI and webhook configuration, anything users wire into their own systems.
core emits the full IProject mongoose document, but brimble-email/src/factory/webhook.factory.ts reshapes it to a clean schema before delivery. The clean schema is what we document. The raw IProject with vaultPath, vaultToken, tracking_token, nomadJobId, etc. is not.
Internal bookkeeping isn’t user-valuable. Lines like “Persists a sandbox record with status: starting”, “Hands provisioning off to Heracle”, “Emits a sandbox:ready event”, “Marks the sandbox ready in MongoDB”, “Polls Nomad for the allocation” belong in an engineering RFC, not a doc. Users can’t act on internal sequencing; they care about what they observe. Rewrite as user-observable behavior:
| Internal bookkeeping (don’t write) | User-observable behavior (write this) |
|---|---|
“Persists a record with status: starting" | "The response comes back with status: starting." |
"Marks the sandbox ready and emits a sandbox:ready event" | "The sandbox flips to ready and the API starts accepting runtime operations." |
| "Polls Nomad for the allocation" | "If the container doesn’t come up within a few minutes, the sandbox is marked failed." |
| "Updates the record so the dashboard reflects the destroyed state" | "The sandbox’s status becomes destroyed.” |
4. Files and structure
docs.json → navigation.tabs[] carries the Documentation tab (everything above) plus a Sandbox API tab driven by the OpenAPI spec at api-reference/sandboxes.openapi.yaml. Add new pages to the right area, then register them under the right group in the Documentation tab’s groups[]. New API specs go in api-reference/ and get their own tab entry.
5. Page conventions
Frontmatter. Every page has YAML frontmatter:title as the H1, so don’t write a duplicate # Title in the body.
Internal links. Use Mintlify’s path-style links without the .mdx extension:
/path) and relative (../area/page) work. Drop .mdx either way.
Headings. Use ## for top-level sections and ### for subsections. The frontmatter title is the page H1; never start with # in the body.
Tone. Direct, second person, active voice. Short sentences. No “simply,” “just,” “easily,” “powerful,” “seamless.” Don’t apologize for the product.
Em-dashes are banned. They make prose feel AI-generated. Use commas, periods, colons, or rewrite the sentence. Sweep them out before committing. Em-dashes inside fenced code blocks are fine.
6. Mintlify components
Use these instead of HTML or markdown image syntax. Callouts. Available without imports:<Info>, <Note>, <Tip>, <Warning>, <Check>. They render as colored callout boxes:
<Frame> with a single child <img>, plus a one-sentence caption:
.jpg, not .png or .jpeg. Mintlify’s CDN reliably serves .jpg and .svg; .png and .jpeg files in the repo are intermittently returning 404 on the deployed site, which renders as broken-image icons. When new screenshots land, convert with sips -s format jpeg src.png --out src.jpg and rename the reference. Don’t hotlink external CDNs unless you’re explicitly intentional (Simple Icons in projects/frameworks.mdx is the one exception).
Diagrams: Mermaid, not ASCII. ```text fences for ASCII flowcharts render with the full code-block chrome (drop shadow, copy button, right-edge gradient) and clip long lines on narrow viewports. Use ```mermaid blocks instead. They render natively, theme cleanly in dark mode, and don’t clip. Pattern that themes well with our dark mode tokens:
Image placeholders. When a page references UI but no screenshot exists yet, use:
/images/, swap the <Info> for a <Frame> block. The placeholder convention makes outstanding screenshots greppable: grep -rn "Image needed:" ..
Cards. For landing-style pages (the home index.mdx), use <CardGroup> with <Card> children:
7. Code blocks
Fences must be paired correctly:- The opener can carry a language tag:
```bash,```typescript,```text. - The closer must be bare
```with no language. A closer with a tag (```bashat the bottom of a block) is not recognized as a closer; the parser keeps reading until it finds a bare fence, which collapses subsequent prose into one giant code block. This produces 404s in production and rendering chaos in preview. Always check that closers are bare.
text for ASCII diagrams, DNS zone records, and other non-language content so they still get the code-block styling (border, copy button, monospace) without misleading colors.
Common tags by content:
| Content | Tag |
|---|---|
Shell commands (curl, dig, npm, etc.) | bash |
| HTTP request/response | http |
| JSON | json |
| TypeScript or JS | typescript / javascript |
| YAML | yaml |
.npmrc, .env, INI-style | ini (or bash for env-var assignments) |
| ASCII art, request flow, DNS records | text |
8. Two MDX gotchas you will hit
HTML comments break the page. MDX treats< as the start of JSX, so <!-- ... --> causes a parse error and the page returns 404. Use JSX comments instead:
{{shared.NAME}} outside a code span will be parsed by MDX as a JSX expression and fail. Always wrap brace-syntax env references in inline code:
{{@project-slug.NAME}}. Inside fenced code blocks (``` ... ```) and inline code (`...`) braces are safe — MDX doesn’t parse JSX inside code.
9. Hard editorial rules
-
No API endpoint references outside the webhook docs. Don’t write
curl https://api.brimble.io/v1/...in any user guide. The webhook configuration page (and only that page) gets exceptions. For everything else, point at the dashboard. -
No
<curl> | shinstall commands unless the user explicitly asks. -
No invented numbers. If you don’t have a value (rate limit, retry count, plan price), find it in the codebase or omit. The
core/src/services/v1/compute.service.tshas the real metering math.dashboard/src/utils/default-pricing.tshas the real plan defaults.brimble-payment/COMPUTE_PRICING_GUIDE.mdis internal but accurate. - No bullets that just describe what’s not there. “What’s not shown” sections, “What you can’t do” lists, defensive “the dashboard surfaces but you can’t change in place” notes — fold the real point into the relevant section, drop the negation.
-
No “Verification” section just to add one. A
curl -I https://your-app.brimble.appand “you should see HTTP/2 200” doesn’t help anyone. Keep Verification only when the verification step is non-obvious (the righttools/listJSON-RPC payload for an MCP server, thewrkinvocation that actually exercises autoscaling, thedig @1.1.1.1for DNS troubleshooting). -
No multi-language code triplets for trivial things. Don’t write the same
process.env.Xexample in Node, Python, Ruby, and Go. Devs know how to read env vars in their language. -
No worked-example math whiteboard for pricing. The
(0.5 - 0.5) × $4 / 720hstyle is alienating. State the rule, point at the dashboard view that shows the running cost, move on. -
No fabricated “Discriminated union for handlers” / pattern-matching examples. Webhook payload schemas are the doc; how the user dispatches on
eventis their business. - No competitor name-drops. Don’t reference Vercel, Railway, Render, Heroku, Fly, Cloudflare Workers, AWS, or any other PaaS by name in user-facing docs, even when comparing architectures. Use neutral framing like “A common alternative is X” and let the reader connect the dots. The build-system and sandbox-system deep-dives use this pattern explicitly.
- No Kubernetes references. Brimble runs on bare metal with the HashiCorp stack; positioning against K8s reads as defensive and dates badly. If you’d be tempted to write “without Kubernetes” or “instead of Kubernetes”, drop the comparison entirely.
- No troubleshooting items that are pure semantics or beyond user control. Every troubleshooting bullet should be a concrete diagnostic plus a fix the user can run. “‘Add a payment method’ → add a payment method” is reading the error message back at the user. “Region out of capacity” is something the user can’t act on. Cut both.
10. OPSEC
Public docs are reconnaissance fodder. The goal isn’t to hide that Brimble uses Nomad / Consul / Vault / BuildKit / Cloudflare — those are fine to mention by name when the context calls for it. The goal is not to hand attackers a configuration map. Safe to mention.- Cloudflare sits in front of every request (the user can see this themselves from response headers).
- Brimble uses HashiCorp’s stack, gVisor, BuildKit, Railpack at a high level.
- User-facing hostnames like
gateway.brimble.app,ns1.brimble.io,ns2.brimble.io, the internal DNS suffix*.service.brimble.internal(which is what users wire into their own configs). - Public response headers like
X-Brimble-Id,X-Brimble-Project-Version. - What the build does, what the runner does, what the cache does.
- The names of third-party infrastructure providers Brimble runs on (referred to as “bare metal” or “Brimble’s regions”).
- Specific versions of any internal component.
- Internal config: ACL setup, sandbox/seccomp rules, network egress rules from builders, internal hostnames, default ports, ports on internal services, agent configs.
- Recovery, bootstrap, or admin procedures.
- Token, cookie, or session formats for internal auth between platform components.
- Internal API endpoints that aren’t exposed to customers.
- How tenant isolation is enforced beyond “tenants are isolated.”
- Internal storage backends by name (MongoDB, Redis, RabbitMQ, Ably, Loki). Say “the sandbox record”, not “the Mongo document”. Say “the dashboard updates”, not “the Ably channel pushes an event”.
- Internal event names (
sandbox:provision:failed,sandbox:ready,sandbox:expired, anything that looks like<service>:<noun>:<verb>). Frame as status transitions the user can observe: “the sandbox flips toready”. - Internal queue / bus references: “message bus”, “RabbitMQ”, “Bull queue”, “Redis pub/sub”. Users don’t see these. Talk about the user-visible side effect.
- Internal namespace / task / pool names: Nomad job names (
sandbox-<id>,sbx,run), node pool names, Consul namespace names. Say “scheduled on a sandbox host” without leaking the labels. - CNI / CSI plugin names:
s3,geesefs, the specific bridge/no-egress CNI plugin name. Say “CSI-backed persistent volume” or “outbound network blocked” without naming the implementation. - Specific defense mechanisms that map to attacker tools. Don’t write “cloud-metadata endpoints at
169.254.169.254are spoofed to localhost”, that’s a recipe for someone to test whether your mitigation is in place. Say “the container can’t reach the host’s identity or credentials surface”. Same idea: don’t enumeratecap_droplists, seccomp filters, or pid limits with their exact values. - Internal timing constants (provision-poll timeout, stuck-starting threshold, reaper intervals). Say “a few minutes” or “hourly”.
11. Brand
Pulled fromapps/dashboard/src/styles.css and apps/web/src/styles.css (identical tokens):
| Token | Light | Dark |
|---|---|---|
| Brand blue | #006fff | #006fff (unchanged) |
| Accent orange | #f5a623 | #f5a623 |
| Foreground | #222528 | #e8eaed |
| Page background | #fafafa | #1a1c1e |
.woff2 files live in /fonts/. Trial cuts are wired today; production should use the licensed cuts when available. Mintlify only accepts woff / woff2, not TTF / OTF.
docs.json references the body font; the mono font isn’t wired up yet.
12. Things that have already burned me
A running list. Add to it._idon the wire. The webhook payload usesid, not_id. The dashboard backend strips Mongo’s_idbefore sending. Don’t exposeObjectIdas a TypeScript type in docs; it’sstring.- Dashboard label vs runtime behavior. The database overview card says “Backup frequency: Daily” but
heracle/internal/service/cron.go:42-43cron is0 0 * * * *— every hour. The cron is the truth. - Hacker price. 7. Pro is 19. Sourced from
dashboard/src/utils/default-pricing.ts(the live config). Older internal docs have 19; ignore them. - Plan free CPU/memory baseline. Free is 0.25 vCPU / 0.25 GB. The compute slider’s smallest stop is 0.5 — meaning Free-plan projects pick up metered overage as soon as they create a project. Document this honestly.
- Persistent disk default. 10 GB minimum, not 1. Sizes go 10–150 GB in 10 GB steps (
disk-size-options.ts). - Password protection uses a session cookie + form login (
x-brimble-session), not HTTP Basic Auth.curl -u user:passis wrong. There’s no self-serve toggle in the dashboard yet. - MCP auth header is
x-brimble-key, notAuthorization: Bearer. subscription_idin webhook envelope is in the internal RabbitMQ payload, butbrimble-email/src/factory/webhook.factory.tsstrips it before sending to the user. The user-visible envelope is{event, data}.- Webhook signing. Real events (factory → Hookdeck → user) are unsigned. The
X-Brimble-WebhookHMAC andX-Brimble-Testheader only apply to the one-off test endpoint, not production deliveries. Don’t claim users should verify HMAC. - Env var values are not in webhook payloads. Only the variable name (
envData.name). Thedata.valuefield doesn’t exist in the factory output. - Region defaults are read at runtime from the
PlanConfigurationMongo collection, not hardcoded. The internalCOMPUTE_PRICING_GUIDE.mddocuments the values; the dashboard’sdefault-pricing.tsmirrors them as fallbacks. - Hugging Face git integration was removed. Don’t reintroduce it without confirmation.
- Builder, not Runner. The build worker is called the Builder everywhere user-facing. The codebase still uses “runner” in file names and class names, but the docs are renamed. Don’t backslide.
- Build pipeline step 7 is “Launch”, not “Orchestrate”. That’s the step where the artifact goes live on the Nomad cluster. Same applies in
projects/builds.mdxandprojects/build-system.mdx. - Free plan can only deploy static sites + 1 free-tier database.
core/src/services/v1/verify-payment.service.ts:58-59rejects every non-static project on Free at deploy time. So there’s no path where a Free user picks a slider position that meters compute, don’t write callouts about “quiet overage” on Free; it’s impossible. Static sites have no compute allocation. - Free databases are deleted after 15 days, not paused.
core/src/services/v1/database.service.tsreapExpiredFreeTierDatabasescallsprojectService.deleteProject(project). The dashboard’s “paused” copy is softer than the reality. - Rate limiting is Cloudflare, not the proxy middleware. The proxy has a
rate-limit.tsmiddleware that defaults to “500 req / 10 min / IP / domain”, but it’s not wired into the live request path. Cloudflare handles it with adaptive rules. Don’t quote the 500 number. - MCP
x-brimble-keyis account-level, not per-project. The key lives in the user profile drawer under API key, paid plans only. Action is Reroll key (not “Rotate”). One key authenticates every MCP server in the account. - Apex A record IP is
157.90.225.125at@. Don’t say “see the dashboard for the IP”; write the value, sourced fromdns/lb/keepalived-backup.conf. - Workspace pricing: 8/build, min 2 builds. Sourced from
dashboard/src/config/index.ts:36-37. 3 seats + 2 builds = 7.50/build or 0/1 minimum builds. - Payment retry copy avoids Stripe by name. The user-facing retry flow describes what users see (warnings, builds disabled at attempt 3, projects suspended when retries exhaust, canceled at 30 days unpaid). It doesn’t mention Stripe, Smart Retries, or webhook event names. The internal payment service handles the “how”; the user only needs the “what”.
- Header strip + Header inject sections were removed from
networking/request-lifecycle.mdx. The numbered spine goes 1-10 now; don’t reintroduce header-injection or header-strip explanations without confirmation. - Next.js / Nuxt SSR-vs-static gotcha. A Next.js or Nuxt repo deployed as a static site (the only option on Free) needs explicit static-export config (
output: "export"for Next,nitro.preset: "static"for Nuxt), otherwise the build emits SSR artifacts the static deploy can’t serve, and the deployed site loads blank. Warning lives inprojects/service-types.mdxStatic site section. - Database backups are rolling latest, not retained.
heracle/internal/service/storage.go:752-755deletes prior snapshots before each upload, so only one snapshot per project per backup type survives. Hourly schedule fromBackupCronSchedule envDefault:"0 0 * * * *". Don’t claim a multi-day retention window.
13. Workflow for adding or editing a page
- Read the relevant codebase paths to ground the claims.
- Write or edit the
.mdxfile. - If new, add it to
docs.jsonunder the right group. - Run
mintlify devand visually verify onhttp://localhost:3000. Check:- The page renders (no 404).
- Code blocks have syntax highlighting where expected.
- Internal links resolve.
- No JSX parse errors in the terminal.
- Sweep the file for em-dashes, fabrications, em-dash-y “Verification” sections, and HTML comments.
- Commit. Reasonable message format:
14. When the user pushes back
Most edits in this repo come from a user pointing out something is wrong, missing, or over-explained. Default response:- Look at the code first. Before defending or extending what’s there, search the codebase for the actual behavior. The user’s instinct that something is wrong is usually right.
- Cut, don’t decorate. When in doubt, less is better. The user has repeatedly pulled out filler (“Verification” sections that just curl the URL, multi-language env-var examples, math-whiteboard pricing, “Why use X” sales sections, “Discriminated union” patterns). Those are high-likelihood removal candidates.
- Acknowledge the specific mistake. Don’t generalize. If the FAQ had six fabricated entries, name the six and what was wrong about each.
- Don’t propose to keep “less sure about” items in the user’s court while doing the rest unilaterally. When the user says “go ahead,” do all the cuts you flagged.
15. If you have to invent something to keep going
Don’t. Stop and ask the user. The blast radius of a confident-sounding fabrication in docs is months of bad behavior in user code, support tickets, and trust erosion. Asking is cheaper than every alternative.16. Mintlify config quirks
Hard-won and easy to re-trip on:background.color, singular, notcolors. The schema field isbackground.color.{light, dark}. Writingbackground.colorsfails validation with a misleading “Property colors is not allowed” error. Same shape as thecolorsblock at the top ofdocs.json(which IS plural), so the inconsistency catches everyone.- No custom-CSS hook in the
minttheme schema. Thestylingblock acceptseyebrowsandcodeblocksenums only, not acsspath. Astyle.cssat repo root will exist but Mintlify won’t auto-load it on deploy (https://<site>/style.cssreturns 404). For now we don’t have a working path for custom CSS; the orphanedstyle.cssis intentional, kept around as a parking spot in case Mintlify ships a hook or we move to a different platform. - There are 9 themes.
mint(default),maple,palm,willow,linden,almond,aspen,luma,sequoia. Switching is a one-line"theme": "..."change indocs.json; the schema is identical across all 9 (zero theme-unique fields), so existing config keeps working. - OpenAPI tab integration. Pass the spec path as a bare string at the tab level:
{ "tab": "Sandbox API", "openapi": "/api-reference/sandboxes.openapi.yaml" }. Mintlify auto-generates one page per endpoint. The object form{ source, directory }is also valid but isn’t required and the directory field doesn’t change behavior here. - SEO block shape:
seo.indexing: "all"to index every page (default is"navigable", which only indexes nav-listed pages).seo.metatagsis an arbitrary key→value object that becomes<meta>tags on every page (OG, Twitter Card, robots, verification tokens). metadata.timestamp: trueshows last-modified date at the bottom of every page. Useful for trust signals.contextual.optionscontrols the Copy page dropdown. Valid values includecopy,view,chatgpt,claude,perplexity,grok,aistudio,devin,windsurf,cursor,vscode,mcp,add-mcp,devin-mcp. We ship["copy", "view", "chatgpt", "claude", "perplexity"].- Fonts: two slots,
headingandbody. Onlyheadingis currently wired to Marfa (weight 700). The body font falls back to Mintlify’s default, which means bold runs in body copy also fall back to system bold. Wiringbodyto Marfa fixes the bold-in-paragraph appearance but also Marfa-fies all paragraph text. Choose accordingly. - OG images: 1200×630, public URL, paired with
twitter:card: summary_large_image.og:imageinseo.metatagsis currently a placeholder pointing atbrimble.io/og.png; ensure the URL actually serves a 1200×630 image before deploy. Without one, link previews on Slack/X/Discord render text-only. mint devconfig-level changes don’t always hot-reload. Tab additions, theme switches,seo.*andmetadata.*changes often need a restart. Page content (MDX) hot-reloads fine.
17. The sandbox-system / build-system voice
Bothprojects/build-system.mdx and sandboxes/sandbox-system.mdx follow the same pattern. Use it when writing future engineering deep-dives:
- Title: “Inside the Brimble [thing]” or “A deep dive into [thing]”.
- First paragraph declares the audience: “This page is for the curious: developers who want to understand the system they’re trusting their workloads to.”
- Pointer to the user-facing companion doc immediately after the intro: “If you’re looking for the user-facing documentation, see …”
- H2 sections, flat (no H3 nesting unless absolutely necessary):
- Inside [thing], name the components, one bullet each, what they own.
- How the pieces fit together, Mermaid
flowchart TBdiagram + 1-2 callouts highlighting what’s structurally interesting. - Life of a [thing], chronological walkthrough from request to terminal state. Number the steps as
### 1. .... - The decisions behind the architecture, 2-3 deliberate trade-offs in “a common alternative is X; we chose Y because Z” form. No competitor names.
- What it means for you, measured outcomes / what the architecture buys the user.
- Voice: present-tense, declarative, no marketing words, no apology. Lift the same tonal register as Vercel’s Hive post (you can read it for cadence, but don’t quote, link, or name them).
18. Adding a new API tab from an OpenAPI spec
- Drop the spec file under
api-reference/[feature].openapi.yaml. Copy from the source-of-truth repo, don’t write from scratch. - Strip operations Brimble doesn’t expose publicly. Sweep for any
description:orsummary:fields that reference internal services (e.g., “Loki-backed log query”) and rewrite them user-side. - Confirm the spec has a
servers:block with the user-facing URL (https://sandbox.brimble.io,https://api.brimble.io, etc.). Without a server URL, Mintlify renders pages with the heading but no method/path/playground. - Add to
docs.jsonnavigation.tabs[]: - Restart
mint dev. - Verify each endpoint page renders the method+path block, request schema, response schemas, and the Try it playground. If the page is empty below the heading, the YAML probably has a broken
$ref, a missingservers:block, or an OpenAPI version Mintlify doesn’t support (use 3.0.x).