Conventions
The rules that exist because breaking them caused a real bug, plus the checklists for adding code.
Most of these are not style preferences. Each one exists because ignoring it produced a specific failure.
Names
Anything that must agree across code, the database, prompts, and Cloudflare lives in lib/registry.ts: module slugs, names and ids, worker names, queue names, domains, the R2 bucket, the AI Gateway alias. Code imports from it and never hardcodes. Renames happen there first, then in code, then in a migration.
Look modules up by MODULES[x].id. Never by slug or name, both of which are renameable in the admin UI.
Code
- camelCase in code, DTOs, and API responses. snake_case only for database column names.
- No
any. Typecheck is enforced at zero errors, so there is no pre-existing noise to hide in. - No
console.log. Use the structured logger inlib/logger.ts. - Types over prose for contracts. Define the schema (Zod, Drizzle) as the source and derive types from it. Reserve prose for why a system exists.
- Prefer
createServerFnover new API routes in new code.
UI
Never introduce a UI element type without grepping for an existing instance first. Default instincts (hand-drawn SVGs, ad-hoc tabs, a second icon set) are almost always wrong here.
| Concern | The one right answer |
|---|---|
| Icons | Hugeicons only, <HugeiconsIcon icon={XIcon} size={..} />. Grep for an existing name before inventing one. |
| Listings | Model on components/images/media-library-content.tsx. No dead disabled controls. |
| Sheets | Right-side Sheet at w-[880px] max-w-[calc(100vw-1rem)], title font-semibold text-2xl tracking-tight. |
| Detail sheets | DetailSheet plus DetailSheetSection, not an inline layout. |
| Prompt boxes | TintedPrompt plus components/chat/composer.tsx. There is no second composer. |
| Output cards | One OutputNavigator. One card design in options-selector.tsx. |
| Tokens | #efefef card borders, no shadows, h-8 and h-7 compact buttons, CSS variable colors only. |
Structured data (offers, research, personas, outputs) opens in the navigator sheet. Only genuine inline media (images, videos) renders directly in chat.
No em dashes and no horizontal rules in any user-facing copy. This applies to prompts too.
When asked to change something specific, change only that. A request to restyle buttons is not permission to touch the sidebar, badges, borders, or navigation.
Checklist: new API endpoint
Auth and ownership
requireUser first, then requireOwnedRow for any id you read or mutate.
Credits
Any generation cost goes through withCredits(..., waived: byok) so refunds are automatic.
Worker calls
submitToPipelineWorker, never a raw fetch without an ok check.
Names
From lib/registry.ts.
Casing
camelCase in code, snake_case only for columns.
Route tree
bun run build to regenerate routeTree.gen.ts, or createFileRoute("/api/x") will not typecheck.
Gates
bun run check, plus a unit test for any pure logic. Extract the logic (like lib/attachments.ts) so it tests without a database or a request.
Checklist: new agent
Covered in full on the Agents page. In short: registry, prompt files, migration, runtime loader with an inline fallback, agent definition and renderer, then check:truth and smoke.
Formatting trap
bun run format (ultracite fix --unsafe) rewrites the whole repo, not just your files. If you run it, revert the unrelated files before committing.
Known false positives
The dead-code scanners flag two things permanently. Both are correct as they are, and “cleaning” them breaks the build:
lib/registry.tsexports, which are the infrastructure source of truth and intentionally referenced by scripts and config.workers/stubs/empty.js, which is a wrangler module alias.