Surplus logo
Surplus Docsby Sharing Excess

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 .ts directly.
  • Biome handles linting and formatting. Run bun run lint at the root to check and auto-fix in one step.
  • Type-check everything with bun run typecheck before you push.

TypeScript

  • No any. The codebase is fully type-safe. Prefer inference over explicit annotations, and reach for unknown plus narrowing when a type is genuinely open.
  • type aliases, not interface — 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 Date objects.
  • No ambiguous abbreviations. Spell out domain names — for example, never shorten collectionEventItem or distributionEventItem to initialisms.

API and data access

  • ORPC for all client-server communication. No hand-written REST endpoints. See API and contracts.
  • useSuspenseQuery with Suspense boundaries for reads; useMutation for 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 onlybg-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 pickerSelectCombobox for static option lists, SearchSelect for 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.