Conventions
Surplus is consistent on purpose. A small set of rules — applied everywhere — is what lets you drop into any part of the codebase and immediately know how it's shaped. Here are the ones that matter most.
Language and tooling
- Bun only. Bun is the runtime, package manager, and test runner. There is no npm, no Node.js step, and no separate TypeScript compile — Bun runs
.tsdirectly. - Biome handles linting and formatting. Run
bun run lintat the root to check and auto-fix in one step. - Type-check everything with
bun run typecheckbefore you push.
TypeScript
- No
any. The codebase is fully type-safe. Prefer inference over explicit annotations, and reach forunknownplus narrowing when a type is genuinely open. typealiases, notinterface— except when extending third-party types that require an interface.- Reuse schemas from
@surplus/types. When you need a variation of an existing shape, derive it with.pick(),.omit(),.partial(), or.extend()rather than redefining fields. One definition, many derivations.
Naming and data
- camelCase everywhere — tables, columns, files, CSS variables, and identifiers. No exceptions.
- UUID primary keys, generated with
gen_random_uuid(). Never auto-incrementing integers. - Unix epoch timestamps stored as integers, never
Dateobjects. - No ambiguous abbreviations. Spell out domain names — for example, never shorten
collectionEventItemordistributionEventItemto initialisms.
API and data access
- ORPC for all client-server communication. No hand-written REST endpoints. See API and contracts.
useSuspenseQuerywith Suspense boundaries for reads;useMutationfor writes.- Drizzle is the schema source of truth. Define schema changes in
packages/postgres/src/schema/and apply them with the Drizzle push workflow — don't hand-write ad-hoc SQL migrations for routine schema changes. - Always use transactions for multi-step database operations. Service methods open a transaction and pass it to service functions.
- Packages never read
process.env. All configuration is injected at the app layer. This keeps packages portable and testable.
UI
- Semantic tokens only —
bg-background,text-foreground, and the rest. No raw colors and no legacy token names. - Every interactive element gets a stable ID, following
{routeName}{sectionName}{elementName}. This is what makes end-to-end test selectors reliable. - Every button gets an icon paired with its label by default; an icon-only button must have an
aria-label. - Use the right picker —
SelectComboboxfor static option lists,SearchSelectfor database-backed lists. Never wire raw select primitives into feature code.
File organization
- Route-local code lives in
-helpers/folders next to the route that uses it. The leading dash keeps it out of the router. - Keep feature code colocated; promote shared code into
components/or a package only when it's genuinely reused.
Writing and copy
- When abbreviating pounds in UI copy or docs, always write
lbs.with the trailing period.
The deeper references
These conventions are enforced and expanded in the repository:
- Root
AGENTS.md— the canonical rule list. docs/conventions.md— TypeScript, Zod, UI, forms, naming, and file organization in detail.docs/design.md— the design system and semantic tokens.