Skip to content

Engineering standards

Engineering standards

Structure

apps/ deployable things (api, web, mobile, docs)
packages/ shared code (shared, db, catalogue, config)
tools/ standalone developer tooling, not deployed

An apps/ package may depend on packages/. A packages/ package must never depend on an apps/. Nothing depends on tools/.

Types and schemas

One definition, derived downstream. Per ADR-0008:

packages/db Drizzle schema ← the source of truth
↓ drizzle-zod
packages/shared Zod schemas + types ← what clients import
apps/web, apps/mobile, apps/api
  • Never hand-write a type that drizzle-zod can derive.
  • Never define the same shape twice for web and mobile. That is what packages/shared is for, and the contract check (gate step 8) exists to catch it.

Validation at the boundary

Parse untrusted input with Zod once, at the edge — request handlers, form submissions, external API responses. Inside the boundary, work with parsed types and trust them.

Do not validate defensively in the middle of the call stack. If an inner function needs to re-check its inputs, the boundary is in the wrong place.

Errors

  • Throw typed errors, not strings.
  • An error crossing the API boundary becomes a structured response with a stable code. Clients switch on the code, never on the message.
  • Never swallow an error to make a test pass. See ci-and-gates.md.
  • Errors reach Sentry. Anything caught and not rethrown must be logged deliberately, with a comment saying why it is safe to continue.

The CPU budget

Workers free tier allows 10ms CPU per invocation, and per ADR-0009 the web app is a Worker too. This is the real ceiling — not the 100K daily request count.

Average Workers use around 2.2ms, but auth validation plus a database query plus serialisation in one request gets close.

Rules:

  • No N+1 queries. Ever. One request, a bounded number of queries.
  • No heavy serialisation in the request path. Large transforms belong in a background worker or a cached column.
  • The feed and discovery queries are the ones at risk — they join across follows and recent activity. Measure them.
  • Bundle growth is the leading indicator of CPU growth, which is why gate step 5 checks it.

Rate limiting

Every public endpoint is rate limited. This ships in the first commit of apps/api (WI-012), not later.

Cloudflare has no spend protection by default. An unthrottled endpoint can exhaust the daily quota and force a mid-month upgrade. This is a cost control, not a security nicety.

Data access

  • All database access goes through packages/db. No package builds its own connection.
  • The connection string comes from the HYPERDRIVE binding, never a raw DATABASE_URLADR-0004.
  • Hand-written SQL is expected and fine for FTS and discovery. Use Drizzle’s sql template so it stays typed.

Domain invariants

These are correctness rules from ADR-0006, not style preferences. Each has a test.

  • The shelf rating is derived. It projects from the most recent session rating. Any write path setting it directly is a bug.
  • Older session ratings are never overwritten. Opinion changing over time is the product.
  • A session participant may have no user account. Every query joining participants tolerates a null user_id.
  • Everything on a session is nullable except game_id and played_atADR-0007. A change adding a required session field breaks that ADR and needs a superseding one.

Naming

  • Files and directories: kebab-case
  • TypeScript: camelCase values, PascalCase types
  • Database: snake_case tables and columns, tables plural (shelf_entries, sessions)
  • Branches: wi-###-slug for work items; chore-<slug> for the rare change that belongs to no work item — backlog bookkeeping, a dependency bump, fixing a stale link

Prefer folding bookkeeping into the next work item’s PR. A steady stream of chore- PRs is a smell: it usually means work is being done outside the backlog, which is exactly what the work-item model exists to prevent. Use chore- when the change genuinely cannot wait, not as a way around writing a spec.

Comments

Comment why, not what. A comment restating the code is noise that goes stale.

The exception worth writing every time: a non-obvious constraint. // Hyperdrive pools this; do not add a pool here earns its place forever.