Skip to content

The platform is live.

Take a look
The Lab
Architecture essay · 13 min read

Bring this repository up to the standard

Seven things a production codebase has to have before it is secure, sound and portable. Distilled from three of mine, with the failure behind every rule, and the prompt that installs them.

TypeScriptReactNodePostgres
What it is
A standard for a production codebase, distilled from three of mine, where every rule carries the defect that made it a rule.
What you do with it
Paste one line into a coding agent. It audits your repository, plans, executes, and leaves a report beside the diff.
What it costs
Nothing, and no sign-up. It never commits, so you review once, at the end.
Who it is for
Any TypeScript backend. The examples are Node, Postgres and Next.js, because that is where the defects happened, not where they apply.
What it is not
Not a style guide, and not a promise of good code. It holds the invariants that cost money, trust or tenant data.
Take the promptOr read on. The rest of this page is why each rule exists.

Every rule here was distilled from production codebases I built and operate with AI agents: a wagering platform with an append-only ledger and automated settlement, a coaching marketplace on Stripe Connect and a calendar provider, and the multi-tenant AI portal this site runs on. Every rule exists because its absence caused a real defect. Where a rule names the defect, that is why it is a rule and not a preference.

01

A codebase is judged by what it holds, not by what it says

The situation

Most engineering standards are style guides. They say what the code should look like. A busy team follows them the way it follows any request: until the day it costs something.

This one is different in one way that matters. Every rule in it is attached to a defect. Not a hypothetical one: a specific thing that broke, in a specific codebase I built, that cost money, trust or a weekend. The rule is the shape of the fix. Where I could not name the defect, the rule did not make it in.

Three products, one failure list. A ledger that could be rewritten, a tenant id trusted from a request body, a status union that compiled while every insert violated the database, a write whose error nobody read. What follows is what those defects produced, and the prompt that installs it in a repository it has never seen.

02

What it was distilled from

counted from the standard itself, not estimated

3production codebases it was distilled from
7things a codebase has when it is done
12areas of rules, each with the failure it prevents
9example probes, rewritten for your stack
15guardrails installed, each broken on purpose
6rungs on the mechanism ladder
01

A wagering platform

Free-to-play picks with the architecture to flip to real money: an append-only ledger, atomic settlement, defense in depth at the grants.

What it taught. The money path. Settlement as one stored procedure, and balance arithmetic that never reads then writes. And EXECUTE revoked from PUBLIC, after the money functions turned out to be callable with the browser key.

areas C · D · HRead it
02

A coaching marketplace

A two-sided marketplace on Stripe Connect and a calendar provider: discovery, booking, payment, video, and automatic coach payout.

What it taught. Integrations. Compensating transactions when the external create succeeds and the internal write fails, and refund and fee flags that default to the opposite of what a marketplace wants.

areas H · IRead it
03

The multi-tenant AI portal

The platform this site runs on: a private, grounded workspace per client, with coding agents that run in sandboxed virtual machines and spend real money.

What it taught. Everything else. Three-layer tenancy, the money gates, the sandbox as the security boundary, and every one of the fifteen guardrails. Most were written after the defect they now catch had already happened here.

areas A · B · C · E · F · J · K · LRead it

Three products, one failure list. Different domains, the same defect classes. That overlap is what made it a standard rather than three sets of notes.

03

The 7 things a codebase has when it meets the standard

what done looks like

Each one with the defect that made it a rule. That is the whole standard, and it is the whole of what you need to decide whether it is for you. Open a card for what holds it and the rules under it.

01

A feature-sliced structure with one-way imports

areas A · G · L

Every area of the product owns its own components, hooks, services and validation. Imports flow one way: the shared floor never reaches up into a feature.

The failure A shared-floor module that imports a feature is that feature's policy wearing a shared path, and the next agent cites it as precedent. This platform has four such edges. A test derives both sides, so a fifth cannot join quietly.

What holds it, and the rulesfor engineers

A workshop where each bench has its own tools. Nobody walks across the room to borrow a screwdriver from the bench that does something else.

What holds it
  • One directory per domain, owning its whole vertical
  • Router directories that hold routing primitives only
  • A layering test that derives the violator table both ways
  • Domain logic in framework-free modules until a second consumer exists

The usual way to organise code is by kind: all the screens in one folder, all the data access in another. It reads well on day one. It falls apart the moment a change touches one feature. That change now spans six folders, and a reviewer cannot see it whole.

Slicing by feature puts everything one area needs in one place. The page that renders it becomes five lines long. And the direction of imports becomes something a test can check. That matters: direction erodes first and is noticed last.

features/<domain>/ owns components/, hooks/, services/, validation.ts and types.ts, plus rules.ts and utils/ only when the domain needs them. Empty folders are forbidden. The router holds page.tsx, layout.tsx, route.ts and loading.tsx, nothing else. No _components/ under it: a page imports a feature component and renders it.

The import direction is types, then lib, then hooks, then components and features, then app. lib/ is the shared floor and never imports from features/, components/ or app/. The one sanctioned exception is an aggregator that presents every domain's tunables in an operator panel, and it is listed as such.

A monorepo arrives when a second consumer exists, not before. Until then, pure domain logic (rules, pricing, catalogues) lives in files that import nothing framework-specific. The lift into a package is then a move, not a rewrite.

types → lib → hooks → components + features → app

lib/            never imports from features/, components/, app/
features/<d>/   components/ hooks/ services/ validation.ts types.ts
app/            page.tsx layout.tsx route.ts loading.tsx, nothing else
The direction, and what each layer may hold. A test walks the tree and compares the real lib-to-features edges against the documented table, in both directions.
02

One data path

areas B · L

Every read and write from a client takes the same road: one typed helper, a versioned API route that gates, validates, delegates and answers in one envelope, and a service that does the work.

The failure Server actions lock every mutation to one framework and are invisible to a native app, an agent or a partner. When one route answers with an error field and its neighbour with a success flag, the client cannot read a failure the same way twice.

What holds it, and the rulesfor engineers

One front door with a receptionist. Nobody comes in through the loading bay, however convenient it looks from the car park.

What holds it
  • Gate, validate, service, envelope, always in that order
  • One fetchApi helper; a raw fetch in a hook is a defect
  • An explicit return type on every handler, with the raw-body set derived from the code
  • A register of public no-auth routes that a test derives from the route files

Frameworks offer shortcuts that let a screen talk to the database directly. Each shortcut is a second door, with its own lock, its own logging and its own way of failing. Two doors is a security review that never ends.

One road means one place to check who is asking, one place to validate what they sent, one shape for every answer, and one service that a web page, a phone app and an agent call identically. When the service fixes a bug, every consumer inherits the fix.

The route shape never varies. Gate first, before any database work, and early-return the gate's response. Validate the body with a module-level Zod schema from the feature's validation.ts. Call the service with the injected client and a params object. Return the envelope, never an error field. Rate-limited is 429 with Retry-After and the same body shape.

Every handler declares Promise<NextResponse>, or Promise<Response> only when it genuinely streams or returns a raw body. TypeScript infers a return type happily and reports nothing, so an unannotated handler drops the contract with no signal. The set of raw-body handlers is derived from the code, and a Promise<Response> on a handler that builds none fails.

Behaviour lives in the service and every consumer inherits it. A REST route, an agent tool and an MCP tool call the same updateTask. A miss returns false in one place and becomes 404, a tool error and not_found respectively. When the API is a separate deployable, Hono keeps the same four steps and the same envelope. The typed client then replaces hand-written response interfaces on both the web and the native client.

client component
  → features/<domain>/hooks (React Query)
    → fetchApi<T>('/api/v1/...')
      → route: gate → validate → service → envelope
        → features/<domain>/services (injected admin client,
          scoped by the RESOLVED owner or tenant id)
          → database
The one road. The tenant id on the service call is the one the gate resolved from the session, never one the request body offered.
03

Three-layer security

areas C

Identity and tenant are resolved before any data work, table privileges start closed, and row-level policies sit underneath. Any one layer can be wrong and the money and tenant paths stay shut.

The failure CREATE FUNCTION grants EXECUTE to PUBLIC by default and every role belongs to it, so revoking from named roles removes nothing. One of these codebases found its settle, void and create money functions callable with the browser key.

What holds it, and the rulesfor engineers

A vault with three locks on three different keys. Losing one key does not open the door.

What holds it
  • A gate that resolves the tenant from the session, never from the body or the query
  • REVOKE ALL, then the minimum grants back; anon gets nothing
  • EXECUTE revoked from PUBLIC, with the default privileges set so a future function cannot reopen it
  • Row-level security tested as the authenticated role inside a transaction

Security that depends on one check being right fails the day someone edits that check. The standard asks for three independent layers, each able to hold on its own. A mistake in the application, in the database grants, or in a row policy is caught by the other two.

The subtle part is the second layer. Database privileges start open in ways that are easy to miss. A migration tool that generates the schema diff drops the very statements that close them. So the grants are carried by hand, and the live database is probed after every change rather than trusted.

Layer one is the application gate. The session is verified and the tenant is resolved from the authenticated user. A resource the caller does not own is 404 on a tenant surface, so there is no existence leak. A platform-operator gate answers 404, and its predicate is an operator flag, never a role check. An operator resolves as owner on every tenant, so a role check would open the cross-tenant surface to every client owner.

Layer two is table privileges: explicit REVOKE ALL on every table from every role, then the minimum back. authenticated gets SELECT only, row-scoped. service_role gets CRUD, except on append-only tables, where it gets SELECT and INSERT only. So buggy service code cannot rewrite a balance history: the privilege does not exist. The diff engine drops REVOKEs and does not diff function ACLs at all, so the grants file is carried verbatim. The deployed catalog is probed after every grants migration.

Layer three is row-level security as the net: own-row SELECT policies, zero client write policies anywhere, and access helpers that are SECURITY DEFINER and STABLE with an empty search_path. Its tests run as the authenticated role inside a transaction. Outside one you silently run as superuser, and the pass is false. Around all three: secrets fail closed through one requireEnv, an env manifest fails the production build and names every missing variable, webhooks verify an HMAC over the raw body, and every post-auth redirect goes through one safeNext.

04

A data model whose types come from the database

areas E

The schema is declared in numbered files, migrations are generated and reviewed, changes are additive, and every TypeScript value set is derived from the generated types so it cannot disagree with the SQL.

The failure A hand-written status union compiled, linted and passed every test. Every insert on the new surface violated the database in production, silently, because nothing read the write's error.

What holds it, and the rulesfor engineers

One set of blueprints. The builders do not each keep their own sketch of the house.

What holds it
  • Declarative schema files, numbered, applied in order
  • Migrations reviewed line by line against the silent-miss checklist
  • Additive by default; a destructive change needs a five-step plan
  • Status unions derived from generated types, with a test that parses the schema and compares

The database is the one place the truth about the data lives. Every other description of it, in the code and in the docs, is a copy. Copies drift. The standard makes the copies mechanical. Types are generated from the schema after every change. The value sets a program may write are derived from those types, never typed again by hand.

Migrations get the same treatment in reverse. The tool that generates them misses things silently. So the review is against a written list of what it misses, and the live catalog is probed afterwards. A clean diff proves only that the tool sees no difference.

Declarative schema files are the source of truth, numbered, applied in file order, with a table, its policy and its grants in the same file. Migrations are generated artefacts reviewed against the silent-miss checklist: all DML, ALTER POLICY, every REVOKE, function ACLs, views, schema-level privileges. Anything on that list is written by hand.

Additive by default. Create table, create index, create function, add a nullable or defaulted column, create policy, grant: permitted. Drop table, drop column, narrow a type, mass update, truncate, rename a live column: forbidden without a five-step plan (additive shape, backfill job, flip reads, flip writes, drop the old shape after verification). Never edit an applied migration, never change the remote through a dashboard, never reset a linked database.

Types are regenerated after every schema change. Status unions are Database["public"]["Enums"]["x"], never a hand-written literal union. A test parses the schema files and asserts each TypeScript value set equals its SQL constraint. The API caps every response at a fixed row count regardless of the requested limit. So any read that can exceed it pages with a stable unique order, and an .in() list is bounded by request-line bytes, not by key count. Postgres is the queue until job types multiply: FOR UPDATE SKIP LOCKED in a plpgsql claim function, a visibility-timeout lease, a bounded max_attempts that terminalises.

05

A verification floor

areas D · I · J · K

The invariants that cost money, trust or tenancy are caught by a type, a derived test, a lint rule or a build assertion, each proven by breaking it on purpose. Under that, integration tests run the real service layer against a local database and cannot reach a third party.

The failure Five guardrails were recorded as proven by breaking. A second pass, told to refute rather than confirm, found three of them could not fail at all.

What holds it, and the rulesfor engineers

A smoke alarm you have tested with actual smoke, not one you trust because its light is on.

What holds it
  • The mechanism ladder: a type, a derived test, a lint rule, a build assertion, a CI step, and prose last
  • Every guardrail broken on purpose, restored byte-identical, with the failure output recorded
  • A setup file that refuses a non-local database, and a guard that refuses any third-party host
  • A re-run matrix that says which change needs which check, and what the check cannot see

A check that has never failed is decoration, and worse than nothing, because it manufactures confidence. So the standard does not ask for tests. It asks for mechanisms, each shown to catch the exact defect it exists for: introduce that defect deliberately and watch the check go red.

The floor under those mechanisms is a suite that runs the real code against a real local database and provisions real tenants through the real onboarding path. It is physically unable to reach a payment provider or an AI gateway. A test that can spend money is not a test.

Every guardrail is built the same way: write it, run it, confirm it passes; copy the file it guards outside the repository; introduce the exact defect; run again and require a failure that names the file; restore from the copy, confirm byte-identical, re-run. Expect some first attempts to pass while broken. That is the most common defect in this kind of work. A guardrail that cannot be made to fail is deleted.

The guardrails worth having in most repositories of this shape are listed in the prompt, each derived, each with its stated limit: the unread-write lint rule, the money-table read rule, the route contract, the public-route register, the admin gate, the layering table, enum parity, the env manifest, the config registry, docs enumerations, the decision log, the alarm tier, paid-surface gating, audit labels, and egress. Every scanner that reads source strips comments first with a small lexer that respects strings and templates.

The integration floor hard-refuses a non-local URL before any client is built. It runs serially against one shared local database, and tests row-level security with an authenticated-role client for a real signed-in user. Money-path unit tests run against captured real provider responses. A read-only canary against production covers what code cannot see: provider dashboard settings, DNS, certificates, what is actually deployed. A control whose effect lives in a third party is tested by reading the third party's state back, never your own branch.

06

Portability by construction

areas G · H

Pure domain logic and contracts live in modules a second client can import without a build step. A native app or an agent then quotes the same price and follows the same rules as the server.

The failure A native app that reimplements the pricing function will, eventually, quote a price the server rejects.

What holds it, and the rulesfor engineers

A recipe written on a card, not memorised by one cook who might leave.

What holds it
  • The API-first data path, which is what makes a second client possible at all
  • Shared packages in the order a second client needs them: types, contracts, domain math, tokens, adapters
  • Vertical registries with an exhaustive switch and a never default
  • A native lint gate that runs at zero warnings

Portability is not a feature you add later. It is a consequence of the earlier parts. Every mutation already goes through an API, and the rules already live in framework-free modules. So a phone app or an agent is a new client of things that already exist.

The standard asks for that shape from the start, without the monorepo before it is earned. Keep the domain logic pure and the contracts in one place. The day a second consumer arrives, the work is a move, not a rewrite.

Shared packages arrive in the order a second client needs them: generated database types; contracts (rules, Zod schemas, catalogues, key factories); pure domain math (pricing, scoring, settlement predicates); design tokens emitted to whatever the native styling layer reads; provider adapters. Packages a native bundler must transpile from source stay dependency-free apart from the validation library.

React Query is a drop-in on React Native, so a hook that depends only on fetchApi and a key factory is shareable as-is. On Hono the typed client is the seam. Expo Router with native tabs caps the bar at four or five, so off-tab screens need an explicit entry point. Every navigation carries its subject as a param, and empty states state the real reason rather than saying no data.

Adding a vertical without editing callers is a registry. An exhaustive switch with a never default, so a new union member is a compile error. A lookup that throws on an unregistered key rather than falling back to the first entry. And parseX(value): X | null for any string from the database or a request that selects a code path. Integrations normalise at the adapter boundary, type against real captured responses rather than the spec, keep external ids as strings end to end, and distinguish absent from zero all the way through.

07

An instruction layer that stays true

areas A · K

One short file an agent reads before every task, a rules file per area that loads only when that area is touched, an append-only decision ledger with a generated index, and a written loop that turns each defect into a mechanism and deletes the prose it replaced.

The failure Nine hundred and thirty-seven claims in this platform's own instruction files, checked against the code. Sixteen had drifted, and every one of the sixteen was held by memory alone.

What holds it, and the rulesfor engineers

An induction pack that someone checks against the building every quarter, and shortens each time.

What holds it
  • Standing instructions that hold only what the code cannot say
  • Rules files with a paths declaration, so they load when their area is touched
  • A ledger whose superseded entries are stamped in place, with an index generated from the log itself
  • Harness permission prompts on commit and push, so the consent rule is a guarantee and not a request

A coding agent arrives every morning with no memory of yesterday. It reads whatever is written down and follows it with complete confidence, including when it is wrong. So the written layer stops being documentation and becomes infrastructure. It needs the same discipline as the rest: nothing in it that a check does not hold, and nothing the code already says.

The last part of the standard is the loop that keeps this true. When something breaks twice, build the check rather than add a paragraph. When a check replaces a rule, delete the rule. If the instruction files only ever grow, the loop has stopped running.

The root file is read by sixty-plus tools and holds only what cannot be inferred from the code: what is deliberately not obvious, what is dangerous, the commands to run, the consent rules, and a pointer to the domain rules. No directory tour, no framework explanation, no list of files. One rules file per area, declaring the paths it governs so it loads when that area is touched. Past roughly two hundred and fifty lines, the history splits out rather than appending.

The ledger is append-only and newest first, one entry per non-obvious decision with what was rejected and why. An entry later overturned is stamped where a reader meets it, citing the superseding entry's title rather than only its date. Once the log is too long to read whole, its index is generated from the log itself, with a check that fails when it is stale.

The loop closes the system: a decision gets its ledger entry in the same change as the code; a defect that recurs becomes a check, never a paragraph; no check may need a hand-maintained list; when a check replaces a rule the rule is deleted in the same change; a rules file must shrink as often as it grows. The docs tests check structure, not truth: a path that resolves, a register that is complete, a count that matches. A confidently wrong sentence passes every one of them, and that part stays with the human.

04

The 12 areas of rules

each with the failure it prevents, so it can be skipped honestly

Hand an agent a list of rules and it applies every one, including those that do not fit the repository in front of it. A money-path rule on a blog is pure cost. A monorepo split before a second consumer exists is a rewrite nobody asked for.

So every rule carries the failure it prevents, and the agent judges whether that failure can happen here. When it cannot, it says so in the plan and skips the rule. That judgement needs the defect named. The defect is also the memory: a rule written next to its incident cannot be quietly relaxed a year later. Relaxing it means arguing with a specific outage, not with a preference.

Two areas carry a condition: the money path applies only if the repository moves value, and the agent rules only if it runs models. Open one for its failure and its rules.

AStructure and layeringFeature slices, imports that flow one way, and a router that holds only routing.

The failure A shared module that reaches up into a feature becomes the precedent the next change cites, and the direction is gone.

The rules
  1. A1Feature slices, not type folders
  2. A2The router holds routing primitives only
  3. A3Cross-cutting UI lives in components/ and nothing else
  4. A4One-way imports, with the layering table derived both ways
  5. A5A monorepo when a second consumer exists, not before
  6. A6Name conventions once

The full rules, with the defect behind each, are in the prompt below.

BThe one data pathEvery client read and write takes one road: gate, validate, service, one envelope.

The failure Server actions lock every mutation to one framework and are invisible to a native app, an agent, an MCP server or a partner.

The rules
  1. B1No server actions
  2. B2The route shape, always in the same order: gate, validate, service, envelope
  3. B3An explicit return type on every handler
  4. B4One typed client helper; raw fetch in a hook is a defect
  5. B5Behaviour lives in the service and every consumer inherits it
  6. B6Public no-auth routes are a register, and the register is derived
  7. B7Hono, with the same four steps, when the API is a separate deployable

The full rules, with the defect behind each, are in the prompt below.

CSecurity, three layersA gate, privileges that start closed, and row policies underneath, so no single mistake opens the money path.

The failure Money functions were callable with the browser key because EXECUTE is granted to PUBLIC by default and per-role revokes remove nothing.

The rules
  1. C1The gate resolves identity, then ownership; a tenant surface answers 404
  2. C2Never trust a tenant, owner or account id from the request
  3. C3Explicit REVOKE ALL, then the minimum grants; anon gets nothing
  4. C4Revoke EXECUTE from PUBLIC and set the default privileges
  5. C5Row-level security tested as the authenticated role inside a transaction
  6. C6Secrets fail closed through one requireEnv; an env manifest fails the build
  7. C7Webhooks verify an HMAC over the raw body; cron routes compare a bearer in constant time

The full rules, with the defect behind each, are in the prompt below.

DThe money pathskip when the repository moves no valueAn append-only ledger, atomic settlement, content-addressed idempotency, and writes that never fail silently.

The failure A positional idempotency key double-charged exactly the jobs that resumed, and an unread write error lost a paid invoice's credits with no retry and no alarm.

The rules
  1. D1The ledger is append-only at the grant level
  2. D2Settlement is one atomic stored procedure with balance = balance + delta
  3. D3Idempotency keys are per unit of work, content-addressed, never positional
  4. D4Writes resolve, they do not throw; read the error on every money path
  5. D5Money-path reads fail closed
  6. D6External resources own their teardown; every non-terminal status has a reaper
  7. D7Webhooks resolve the account from a stored mapping, never from payload metadata

The full rules, with the defect behind each, are in the prompt below.

EData modelling, schema and typesDeclarative schema, additive migrations reviewed against what the diff engine misses, and types from the database.

The failure A hand-written status union compiled and passed every test while every insert on the new surface violated the CHECK in production.

The rules
  1. E1Declarative schema files are the source of truth
  2. E2Migrations are reviewed against the silent-miss checklist
  3. E3Additive by default; destructive change needs a five-step plan
  4. E4Never edit an applied migration, never change the remote by hand
  5. E5Status unions come from generated types, checked against the SQL
  6. E6Every read that can exceed the row cap pages; an .in() list is bounded by bytes
  7. E7Postgres is the queue until job types multiply

The full rules, with the defect behind each, are in the prompt below.

FFrontend: server state, forms, design systemThree data tiers, one form pattern, query keys from a factory, and one source of primitives.

The failure An inline query key silently stopped an invalidation, and a raw input where a design system existed produced a second dialect of every form.

The rules
  1. F1Three data tiers, simplest first; never fetch in an effect
  2. F2Query keys come from the domain's key factory
  3. F3Forms are one pattern with no exceptions
  4. F4Every query has visible error feedback; every mutation has onError
  5. F5Render purity: no clock or randomness in render
  6. F6The design system is the only source of primitives; colours come from tokens
  7. F7Copy has no em dashes, no filler words, one voice

The full rules, with the defect behind each, are in the prompt below.

GPortability: native and shared packagesShared packages in the order a second client needs them, so the native app imports the real rules.

The failure A lint config that downgraded every rule to a warning made a bare lint exit zero no matter what was wrong, and a decision recorded as done was never executed.

The rules
  1. G1The API-first data path is what makes a native app possible
  2. G2Shared packages: types, contracts, domain math, tokens, adapters, in that order
  3. G3React Query and fetchApi are the shareable seam
  4. G4Native lint runs at zero warnings; verify a package resolves before building on it
  5. G5Vertical registries: an exhaustive switch, a throwing lookup, a nullable parser

The full rules, with the defect behind each, are in the prompt below.

HIntegrations and adaptersNormalise at the adapter boundary, type against captured responses, wrap every call once.

The failure One provider documented 97 fields and returned 170; another encoded a confirmed zero as an absent key, and a transformer that coalesced it to zero destroyed the distinction settlement needed.

The rules
  1. H1Normalise at the adapter boundary; raw types never leave the adapter folder
  2. H2Types come from real captured responses, never the spec
  3. H3Distinguish absent from zero end to end; external ids are strings
  4. H4One orchestration wrapper: duration, uniform errors, structured logs
  5. H5Compensating transactions and idempotent third-party operations
  6. H6Sandbox mode everywhere but production; ids and prices are per mode

The full rules, with the defect behind each, are in the prompt below.

IObservability and lifecycleThree log levels, where the third means a control fired, and post-response work that survives the freeze.

The failure A metering call issued after the response was dropped when the runtime froze, and a scheduled job that idled in silence was indistinguishable from one that had died.

The rules
  1. I1Structured JSON logs; never raw console output on the server
  2. I2Three levels, and alarm means a control fired
  3. I3Post-response side effects go through one persist()
  4. I4Every scheduled job emits one healthy-signature line
  5. I5Health endpoints degrade per query; analytics go through one typed track()

The full rules, with the defect behind each, are in the prompt below.

JAI agents, VMs and durable workskip when the repository runs no models, agents or sandboxesGrounding enforced in code, untrusted content fenced, every paid surface gated, the sandbox as the boundary.

The failure Two gating tests were vacuous because a gate named in a prose comment satisfied them. A turn that crashed mid-loop metered free because usage was summed from the top-level total.

The rules
  1. J1Grounding is code-enforced: retrieve first, refuse on empty, verify citations
  2. J2Retrieved, repository and sandbox content is untrusted; writes need approval
  3. J3Models are typed constants derived from the pricing table
  4. J4One metered ledger row per generation, with a deterministic idempotency key
  5. J5Every paid surface is gated on the route, the MCP tool and the background service alike
  6. J6Ingestion never stampedes, never truncates, never marks a partial index done
  7. J7The sandbox is the boundary: deny-by-default egress, credentials never enter the VM

The full rules, with the defect behind each, are in the prompt below.

KDependencies and supply chainOne package manager, a frozen lockfile, a cooldown against fresh versions, an audit gate you can hold.

The failure A worm that propagates through freshly published versions is stopped by a cooldown measured in days, and nothing else in the toolchain stops it.

The rules
  1. K1One package manager, pinned; frozen lockfile in CI and on the host
  2. K2A resolution-time cooldown in days, with exact pins excluded
  3. K3An audit gate at the level the tree can hold today, and say why
  4. K4CI token permissions are contents: read unless a step needs more
  5. K5Do not hand-roll a format a dependency already parses

The full rules, with the defect behind each, are in the prompt below.

LBackend structure: layers, middleware, and where the tests liveFour layers importing only downward, middleware composed once, one error path, tests in one convention.

The failure A handler that validated, decided, queried and formatted in one thousand-line file; the same guard copied into every handler but one; test files the runner's include glob never ran, read as coverage.

The rules
  1. L1Four layers, each importing only downward: transport, validation, service, data access
  2. L2Cross-cutting concerns are middleware, composed once; a route declares which it uses
  3. L3One error path: services return outcomes, the transport maps them in one place
  4. L4Single responsibility per module; split by responsibility, never by line count
  5. L5Dependency inversion at the trust boundary; nothing reaches for its own connection
  6. L6KISS: no abstraction before the second caller. DRY: one source per fact
  7. L7Tests in one convention, and the runner's include glob is the source of truth
  8. L8A test asserts an outcome, never a branch or a mock of the thing under test

The full rules, with the defect behind each, are in the prompt below.

05

The mechanism ladder

the cheapest mechanism that works is the one that survives

  1. 1A type or signaturethe wrong thing fails to compileAn exhaustive switch with a never default, or a status union derived from the database types.
  2. 2A derived testnothing to update, so it cannot go staleIt walks the tree and compares both sides. A hand-maintained list is already broken.
  3. 3A lint rulecustom if the linter allows it, scoped to production sourceAn awaited database write whose error is discarded fails the build, not production.
  4. 4A build or startup assertiona misconfigured deploy fails before it servesThe build refuses and names every missing variable at once.
  5. 5A CI step or hookwhen nothing above can hold itFrozen install, lint, typecheck, audit, a production build, the local database, the tests.
  6. 6Proselast resort, and it says what it does not catchA sentence that restates what a test now proves is dead weight that will one day contradict it.
The ladder. Cheapest first. A rule that has actually broken gets pushed down as many rungs as it can go, and the prose it replaced is deleted in the same change. A hand-maintained list is not a rung: if someone must add a line when they add a file, it is already broken.

The 15 guardrails the prompt leaves behind

Each one derives what it expects from the codebase, so there is nothing to keep up to date, and each is proven by breaking the thing it guards on purpose. Each also states what it does not catch: a check without a stated limit invites more trust than it has earned.

All 15, with what each one does not catchfor engineers
GuardrailWhat it catchesWhat it does not
  • Unread write resultsAn awaited database write or RPC whose error is discarded or destructured away.A builder captured to a variable and awaited later.
  • Money-table readsA SELECT on the wallet, membership, ledger or grant tables that does not read its error.It narrows the judgement by table; a money read elsewhere still passes.
  • Route contractA handler without an explicit return type, or a raw-body type on a handler that builds none.It checks the annotation, not what the handler does with it.
  • Public-route registerA route file that calls no tenant gate and is not named in the rules file with a reason.It checks presence, never whether the reason is good.
  • Admin gateAn operator route or page that does not use the one gate, including a correct hand-rolled copy.Duplication was the defect, so a correct duplicate still fails.
  • LayeringA new lib-to-features edge, and a fixed one still listed as a violation.Only the documented direction; other directions are not checked.
  • Enum parityA TypeScript value set that no longer equals its SQL CHECK or enum.Only sets that mirror a constraint the parser can find.
  • Env manifestA duplicate, a missing enforced variable at build time, an enforced tier on a feature flag.It cannot tell an enforced variable from one that should be.
  • Config registryA source pointer that no longer resolves to a file exporting the constant.Whether a constant is registered at all is not derivable.
  • Docs enumerationsA path a live document names that does not exist, or a doc that hand-enumerates the rules or the schema.A path that resolves can still be described wrongly.
  • Decision logA superseded stamp that cites no existing entry, and a stale generated index.It cannot tell whether an entry should have been stamped.
  • Alarm tierAn event raised at both error and alarm, or alarms growing past a tenth of errors.It cannot tell whether a classification is right.
  • Paid-surface gatingA file that calls a spending symbol without a gate symbol, comments stripped.A gate that is called and then ignored still passes.
  • Audit labelsA label without a writer, a writer without a label, an action nothing emits.It cannot tell whether an emitted action is the right one.
  • EgressA test that reaches a third-party host.Only from inside the suite; a script outside it is not covered.
06

The prompt

run it against your own repository

Take this with you

Bring this repository up to the standard

Written for Claude Code, and it fits in one paste. Point it at any TypeScript repository, send it, and let it run. It learns the repository first and translates every rule onto the stack that is actually there, then audits with one subagent per area and a verifier told to refute, plans in the order the standard dictates, and executes end to end without asking: independent work in isolated worktrees, every guardrail proven by a different agent breaking it on purpose. It never commits, so you review once, at the end, on the diff.

  1. 0A
    How to run thisAs an orchestrator, not a single reader. Reading fans out to subagents. Every finding and every guardrail is checked by a different agent told to refute, and independent work runs in isolated worktrees. Nothing here is a ceiling: what the repository needs and the standard does not name, it does, and records.
  2. 00
    What done looks likeSeven things in the repository, none of them containing a claim that nothing checks. If some exist, audit and repair them. If none do, build them.
  3. 0B
    Know the repository firstThe phase everything depends on. Readers fan out per segment: the tree, the dependencies read as decisions, real files per layer, every instruction file and agent setup. Then each area is translated into this stack's own mechanism and probes. Grep matches spelling, not meaning, and zero hits is not clean.
  4. 01
    Audit, change nothingOne reader per area, in parallel, each carrying the map and reading files. The example probes are rewritten in this stack's spelling, and a probe that finds nothing is recorded as unknown, not clean. Then a verifier told to refute; only confirmed findings go on. Written into the repository, then straight on.
  5. 02
    The target shapeThe areas of rules, each written with the failure it prevents. The agent judges whether that failure applies here, and skips the rule honestly when it does not.
  6. 03
    Plan, then execute in that orderOne ordered plan: the invariant, the evidence, the cheapest rung on the ladder, and what it will not catch. One guardrail per thing that has actually broken. No hand-maintained lists. Then it executes the plan in that order, deciding alone where the standard leaves a choice.
  7. 04
    Migrate the structureSerial items first, one at a time in the main tree. Independent items to builders in isolated worktrees, each with a brief and its own verification, merged one at a time. A move is a move: no behaviour change in the same unit.
  8. 05
    Mechanise, and break each oneThe builder writes it. A different agent copies the guarded file, introduces the exact defect, watches it fail, restores byte-identical, and hands back the proof. Fifteen guardrails worth having, each derived, each with its stated limit. Delete any that cannot fail.
  9. 06
    The verification floorReal services against a local database that refuses any other, a SQL security suite as a deploy gate, a browser walk that checks on-screen claims against the data, a read-only canary for what code cannot see.
  10. 07
    The instruction layer, the ledger and the loopThe root file, as short as it can be while true; path-scoped rules; the append-only ledger with a generated index; permission prompts in the harness; and the loop, written down.
  11. 08
    The final reportWritten into the repository: which parts existed and which were created, every move, every guardrail with what it does not catch and whether it was proven by breaking, what was deleted and what was added, the ceilings left in place, every step left blocked, and every claim that could not be verified.

The prompt fits in one paste, just. If your client truncates it, or you would rather not paste ten thousand words, use this line instead: your agent fetches the whole thing itself.

Read https://alicantorun.com/lab/repository-standard.md in full and follow it exactly. Do not summarise it, do not plan around it: execute it, starting with the reconnaissance pass.

Run it in Claude Code on Opus 5 with maximum effort, in a fresh session with a long budget; it spawns its own subagents and worktrees. Phase 1 is a judgement task across a whole codebase and a weaker setting produces a plausible list rather than an evidenced one. It does not stop to ask, so give it the whole run, then read the diff and the report it leaves in the repository before anything is committed.

The whole prompt

Bring this repository up to the standard

You are working in a repository you may not have written. Your job is to leave it structurally sounder, more secure, more portable and more verifiable than you found it, and to leave behind the mechanisms that keep it that way after you are gone.

Every rule below was distilled from production codebases built and operated with AI agents, and exists because its absence caused a real defect: where a rule names the defect, that is why it is a rule and not a preference.

Read it as invariants, not as a stack. The examples are TypeScript, React, Node and Postgres, mostly through Next.js App Router and Supabase, because that is where these defects happened, not where they apply. Translate as you read: a named client means whatever client you have, row-level security means whatever row-scoped authorisation your database offers, a named framework means the worked example. Express, Fastify, NestJS, a worker and a queue consumer are the same shape underneath. Adapt the naming; do not adapt the invariants. Where your stack has no equivalent of a mechanism, the invariant moves up a layer, usually into the application gate: say which layer you moved it to, in the plan.

How to run this

You run inside Claude Code or a harness like it, on your own, end to end: audit, plan, execute, verify, report. You never ask the owner what to act on, in what order or how; the standard decides, and where it leaves a choice you make it and record why. Nothing is committed, so the diff is the owner's review and undo.

Work as an orchestrator, not as a single reader:

  • Do not read the whole repository yourself. Context is the scarce resource; spend yours on summaries, the plan and the report. Reading is delegated.
  • Fan out. Subagents in parallel for work that does not depend on each other's results: one reader per area in the audit, one builder per independent plan item in the execution. Give each a precise brief and a fixed return shape, and keep what it returns rather than what it read.
  • Verify adversarially. Every finding and every guardrail is checked by a different agent than the one that produced it, instructed to refute. An author cannot verify their own guardrail, for the same reason nobody proofreads their own writing well.
  • Isolate parallel work. Builders touching disjoint files run in their own worktrees and hand back a diff; merge one at a time, and typecheck, lint and test after every merge. Structural moves that touch shared files run serially in the main tree, and the audit file, the plan and the report stay yours.
  • Nothing here limits you to these steps. If the repository needs something this document does not name, do it, hold the invariants, and record it in the ledger. If a step does not apply, skip it and say why. The standard is the floor, not the ceiling.

What done looks like

Seven things, and not one of them contains a claim that nothing checks:

  1. A feature-sliced structure with one-way imports: domain code with its domain, a router that holds only routing, a shared floor that never imports upward.
  2. One data path: every client read and write through a versioned API with a fixed envelope, gated, validated, delegated to a service, consumed through one typed helper.
  3. Three-layer security that holds when any one layer is wrong: a gate resolving identity and tenant before any data work, privileges that start closed, row-level policies underneath.
  4. A data model whose types come from the database, with a declarative schema, additive reviewed migrations, and value sets that cannot drift from the database.
  5. A verification floor: every invariant that costs money, trust or tenancy caught by a type, a derived test, a lint rule or a build assertion, each proven by breaking it, over an integration floor that runs the real services against a local database and cannot reach a third party.
  6. Portability by construction: pure domain logic and contracts in modules a second client can import without a build step.
  7. An instruction layer that stays true: standing instructions, path-scoped rules, an append-only ledger with a generated index, and a loop that turns each defect into a mechanism.

If some exist, audit and repair them; if none do, build them. An instruction nobody checks goes stale, and a stale one is worse than none because an agent follows it without hesitating. Every claim you write is verifiable against the code today, or replaced by a mechanism that verifies it.

Absolute constraints

  • Never run git commit, git push, git reset --hard, git stash, or any history rewrite. Every change stays in the working tree. If the harness supports it, make this a permission prompt, not just prose.
  • Never run migrations, seeds, resets or diffs against anything but a local database, and never deploy. If a command's blast radius is unclear, record it as blocked and move on.
  • Never read, print, grep or echo a secret value. Check existence or length only.
  • Delete, move and overwrite freely inside the repository, since git holds the originals. Never touch a file outside it. List every deletion in the report.
  • Add a dependency only when a guardrail needs it, as a dev dependency, through the lockfile, and never by removing a supply-chain control. List every addition.
  • Never weaken a security or billing invariant to satisfy a design principle. A cleaner abstraction that loosens a gate or an idempotency key is not cleaner.
  • Run whatever verification the work needs, as often as it needs it: typecheck, lint and targeted tests after every step, the full suite and a production build before you finish.
  • Never stop to ask. If something is genuinely blocked, do everything that does not depend on it, then name it in the report with what you would have done.
  • Report what you could not verify. An unverified claim stated as fact is the exact failure this system exists to prevent.

Know the repository first

Everything after this depends on it, and it is the phase most easily done badly. Grep matches spelling, not meaning: a probe written for one client finds nothing in a repository that uses another, and zero hits then reads as clean. So nothing is judged until the repository is understood in its own terms. Fan the reconnaissance out, one reader per segment, and write the result as a map at the top of docs/STANDARD-AUDIT.md:

  • The tree. Workspaces and packages, entry points (servers, routes, handlers, workers, jobs, CLIs), generated code, where tests live and what the runner's config actually includes. Segment the repository by what the code does, not by folder names.
  • The dependencies, read as decisions. From the manifest and lockfile: framework, HTTP layer, database client or ORM, validation, auth, server-state and form libraries, design system, test runner, linter, deploy target. For each, the fact the standard turns on: does the database client throw or resolve its errors; is there a mutation path that bypasses the API; does the database enforce row-level authorisation, or connect as one pooled role?
  • The shape, read from real files. Per segment, open a representative route, service, schema, test and component, and write down how each is actually built: how errors travel, where the tenant comes from, where validation runs. Two shapes for one job is a finding.
  • The instruction layer. Every file telling an agent or a person how to work here: AGENTS.md, CLAUDE.md, .claude/ rules, agents, skills, hooks and settings, cursor and copilot files, ADRs and decision logs. Claims to check in 1a, not truth; reuse their vocabulary.

Then translate. Map every area (A to L) to this repository's own mechanism and name, or "not applicable" with the reason, and write the probes you will run for it, derived from the dependencies and the files you read. Rules hold at the invariant level, not the syntax level: with a client that throws, the defect is a swallowed catch rather than an unread error object; with one pooled connection, row authorisation has to live in the gate, so name where it lives. Where the stack offers a better mechanism than the one named here (a typed router instead of hand-typed envelopes, ORM middleware for tenant scoping, the schema library already in use), propose it: the invariant is fixed, the mechanism is yours. Every later phase reads this map.

Phase 1: audit. Change nothing.

Read only, fanned out: one reader per area, plus one each for 1a, 1c, 1d and 1e, each handed the map and reading files, not only probe output. Every reader returns findings in one shape: the rule, a verdict (holds, breaks, not applicable), the evidence as file and line, a severity, and what it did not read. Then a verifier, told to refute: it re-reads the evidence behind every finding marked breaks or critical and returns confirmed, refuted or downgraded, with its reason. Only confirmed findings enter the plan; refuted ones stay in the report as refuted, so the next reader does not rediscover them. Write it to docs/STANDARD-AUDIT.md, then continue without waiting.

1a. What already exists. Every instruction file the map found, plus README, ARCHITECTURE and docs. For each: path, line count, rough token cost (words divided by 0.75), last modified, and how much restates what the code already says. Then sample every concrete claim: does that file exist, does that function have that name, does that command run, is that list complete? Report how many you checked and how many were wrong. Do not soften it.

1b. Which dialects the code speaks. Do not ask; read. These probes are examples written for Next.js, supabase-js and Postgres. Run those the map says apply, rewrite the rest in this stack's spelling, add your own for whatever the map surfaced, and record which you replaced. A probe that returns nothing proves only that the spelling does not occur: record zero as unknown, not clean, until a probe for this stack or a read of the files settles it.

# From the root: git grep skips ignored paths; quoted pathspecs and POSIX regexes run anywhere.

# Framework-bound mutations (unusable by native, agents or partners)
git grep -n "use server" -- '*.ts' '*.tsx'

# Browser-side database access (bypasses the API boundary)
git grep -nE "createBrowserClient|from\('|from\(\"" -- '*.tsx' | grep -v "auth\."

# Unread write results: many clients RESOLVE an error rather than throwing
git grep -nE "await [a-zA-Z_.]+\.from\([^)]*\)\.(insert|update|upsert|delete)" -- '*.ts' | grep -v "const {"

# Silent config fallbacks (set once, forgotten, repo says otherwise)
git grep -nE "process\.env\.[A-Z_]+ \?\? ['\"]" -- '*.ts'

# Type escape hatches
git grep -nE ": any([^[:alnum:]_]|$)|as any([^[:alnum:]_]|$)|@ts-ignore|@ts-expect-error" -- '*.ts' '*.tsx'

# Error envelope drift
git grep -n "{ error:" -- '*route.ts' '*controller*' '*handler*'

# Data work inside a transport module, and HTTP inside a service
git grep -nE "\.from\(|\.query\(|prisma\.|drizzle|knex\(|INSERT INTO|SELECT .* FROM" -- '*route*' '*routes/*' '*controller*' '*handler*'
git grep -nE "new Response\(|NextResponse|res\.(status|json|send)\(|c\.json\(" -- '*service*' '*services/*'

# The same guard copied per handler instead of composed once
git grep -nE "getSession\(|verifyAuth\(|requireAuth\(|jwt\.verify\(" -- '*route*' '*routes/*' '*handler*' | cut -d: -f1 | sort | uniq -c | sort -rn

# Where tests live, against what the runner's include glob picks up
git ls-files '*.test.*' '*.spec.*' | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn

# Files that do everything (a prompt to look, not a verdict)
git ls-files '*.ts' '*.tsx' | grep -v '\.test\.' | xargs wc -l | sort -rn | head -20

Then, against the database if there is one, in its own dialect: which functions a public or unauthenticated role can execute (in Postgres a NULL proacl means PUBLIC can, and any SECURITY DEFINER function in that set is a P0, because it bypasses grants and row policies); which tables grant anything to the anonymous role or writes to the authenticated one; which foreign keys to the users table lack an ON DELETE; and whether ledger or audit tables can be rewritten by the application's own role.

Note where a pattern is consistent and where it has forked into two dialects. A forked pattern is worth writing down; a consistent one usually is not, because the code teaches it.

1c. What has already gone wrong. Evidence, not intuition: commit messages shaped like incidents (fix, revert, hotfix, regression, "again", "actually"), because a bug fixed twice is a guardrail waiting to be written; defensive comments ("do not remove", "must run before", "looks redundant but"), each an invariant someone learned the hard way; clusters of TODO, FIXME and HACK, where they cluster mattering more than how many; anything touching money, authentication, tenant isolation, deletion, external calls, retries, jobs or webhooks, because silent failure costs most there; and the same defensive check repeated in many places, which is repetition under duress. If history is squashed, say so. Do not invent incidents; a short evidenced list is the correct output.

1d. What verification exists. Test framework and how it runs, whether tests hit a real local database or mocks, whether the linter gates or reports, type checking, what actually blocks a merge in CI and with what token permissions, and whether anything stops a test reaching a third-party service with live credentials. Then the question that matters: of the invariants in 1b and 1c, which are caught automatically today, and which depend on someone remembering?

1e. Portability and scale ceilings. Is there a second consumer of the API, today or on the roadmap, and where does the logic it would need live now? Where are the hard ceilings: a row cap, per-instance rate limiters, a single-parent foreign key, a queue that does not exist?

Phase 2: the target shape

The reference you audit against and migrate toward, written as rules with the failure each prevents, so you can judge whether the failure applies here. When it does not, say so in the plan and skip the rule; do not apply it ceremonially.

A. Structure and layering

  • Feature slices, not type folders. features/<domain>/ owns its vertical: components, hooks, services, validation.ts (shared by the client form and the API), types.ts (the contract both sides import), optionally framework-free rules and pure utils. Create only what the domain needs; empty folders are forbidden, thin slices are fine.
  • The router holds routing primitives only. A page imports a feature component and renders it, and holds no form logic, data fetching or business rule. Cross-cutting UI lives in one place: design-system primitives, providers, layout. Nothing new goes into a shared/ bucket; that is where slices go to die.
  • One-way imports: types → lib → hooks → components + features → app. A module in the shared floor that reaches up into a feature is that feature's policy wearing a shared path; move it. The one sanctioned exception is an aggregator presenting every domain's tunables in an operator panel.
  • Derive the layering table, both ways. A new violator fails, and a fixed one still listed also fails, so nobody cites a dead violation as precedent.
  • A monorepo when a second consumer exists, not before; until then keep domain logic in framework-free files, so the lift is a move rather than a rewrite.
  • Name conventions once: <feature><Action>Schema, use* hooks, <domain>Keys, the injected client first, integer amounts in the smallest unit, UTC timestamps.

B. The one data path

client component
  → features/<domain>/hooks (server-state library)
    → fetchApi<T>('/api/v1/...')
      → route: gate → validate → service → envelope
        → features/<domain>/services (injected client, scoped by the RESOLVED tenant id)
          → database
  • No framework-bound mutations. Server actions and their equivalents lock every mutation to one framework and are invisible to a native app, an agent or a partner; route handlers are callable by anything that speaks HTTP. The only thing the browser SDK does directly is authentication and push-based realtime.
  • The route shape, always in this order. Gate first, before any data work, early-returning the gate's response. Validate with a module-level schema from the feature's validation.ts, reporting the first issue as path: message with a 400, and answering 404 on a malformed id param rather than a raw cast error 500. Call the service with the injected client and a params object, and let it return an outcome, never a response. Return one envelope: { success, message, data }, status on the response, never a bare { error }; rate-limited is 429 with Retry-After and the same shape.
  • An explicit return type on every handler. TypeScript infers one happily and reports nothing, so an unannotated handler drops the contract with no signal. Derive the raw-body set from the code and fail that annotation on a handler that builds none.
  • One typed client helper: fetchApi<T>(url, init?, timeoutMs?) sets the JSON header unless the body is FormData, applies an abort timeout below the server's hard kill, reads the envelope once, throws message on failure and returns data unwrapped. Raw fetch in a hook is a defect.
  • Behaviour lives in the service; every consumer inherits it. A REST route, an agent tool and an MCP tool call the same updateTask, and a miss returns false in one place, becoming 404, a tool error and not_found respectively. Never fix behaviour in a consumer, and keep input bounds in validation.ts where every surface imports them, because a .max() hardcoded in a tool will drift.
  • Public, no-auth routes are a register, and the register is derived. A test finds every route that calls no gate and fails unless each is named in the rules file with one sentence of why it is safe. It checks presence, never the reasoning; say so in the test.
  • A separate API deployable (Hono, Express, Fastify) keeps the same four steps and the same envelope, and should give the client an end-to-end typed contract.

C. Security, three layers

Any one layer failing must not open the money or tenancy path. Where your stack has only one of these layers, the missing invariants move into the gate; name that in the plan.

Layer 1: the application gate.

  • verifyAuth() verifies the session and returns { userId, client } or a 401. Local JWT verification is fine at the edge, a server-authoritative call when you need the user object, and never a spoofable session read on the server.
  • Ownership after identity: a resource the caller does not own is 403 on an owned surface and 404 on a tenant surface, so there is no existence leak. An admin check reads a role from a row the user cannot write, never a self-writable flag, and a platform-operator gate answers 404 with an operator flag as its predicate, never a role check: an operator who resolves as owner on every tenant would otherwise open the cross-tenant surface to every client owner.
  • Never trust a tenant, owner or account id from the request. Resolve it from the authenticated user. The one sanctioned body-carried id is on an operator route whose gate is identity-based, documented as the exception.
  • Paid or expensive surfaces take a second gate immediately after the tenant gate: rate limit, then budget, returning 429 or 402 with a human message, never a silent degrade.
  • A cron or worker route authenticates with a constant-time bearer comparison against a secret that fails closed when unset. A webhook authenticates with an HMAC over the RAW body, never re-serialised before verifying, with a replay window where the provider supports one.
  • One CSRF check: a mutating request whose Origin is present and not one of this deployment's hosts is 403, failing closed on an unparseable origin; token and HMAC callers send none and pass. Every post-auth redirect goes through one safeNext() allowing only a clean single-slash relative path, blocking //evil and /\evil.

Layer 2: privileges, starting closed.

  • Explicit REVOKE ALL on every table from every role, then the minimum back: nothing for the anonymous role, SELECT only and row-scoped for the authenticated one, CRUD for the service role except on append-only tables (ledger, audit, events) where it gets SELECT and INSERT only. A balance history cannot be rewritten by buggy service code because the privilege does not exist.

  • Revoke EXECUTE from PUBLIC, not just from named roles. CREATE FUNCTION grants it to PUBLIC by default and every role is a member, so per-role revokes remove nothing; add ALTER DEFAULT PRIVILEGES ... REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC or every future function reopens the hole. One of these codebases found its settle, void and create money functions callable with the browser key this way.

  • A new table starts closed: client-readable means row-level security, a SELECT policy and a SELECT grant in its own schema file; secret, money or infrastructure means row-level security on, no policy, no grant, served to the client only through the API. Layer 3: row-level policies as the net.

  • Own-row SELECT policies on user tables, SELECT policies on reference tables, and zero client write policies anywhere, so a direct write is denied even if a grant slips. Access helpers are SECURITY DEFINER, STABLE, with an empty search_path, and are the single source every policy calls. Test policies as the authenticated role inside a transaction; outside one you silently run as superuser and the pass is false.

Supporting controls, each with its incident.

  • Secrets fail closed through one requireEnv(name), never ?? "" (fails open on an HMAC) and never ! (a cryptic 500 later). Non-secret config is a typed constant in the module that owns it, not process.env.X ?? "default".
  • An env manifest declares every variable the app reads with a tier and a note; the production build fails when a required one is missing and names every missing one at once; the same list feeds an operator config page, and a test pins the two. Tunable constants governing spend or safety join one aggregator surfaced read-only to the operator, with a source: pointer a test resolves.
  • Every foreign key to the users table declares ON DELETE: cascade for owned data, set null for attribution, a deliberate block on primary ownership, because erasure must be executable.

D. The money path

Skip this area if the repository moves no value; apply all of it if it moves any.

  • The ledger is append-only at the privilege level: no UPDATE, no DELETE, for anyone. Corrections are new signed rows, never edits.
  • Settlement is one atomic procedure: resolve, update status, write the ledger row, adjust the balance, in one transaction, with balance = balance + delta over a CHECK (balance >= 0) and a unique index on (user, type, ref) that makes a second payout row impossible.
  • Idempotency keys are per unit of work, content-addressed, never positional: per invocation, per content hash, per provider reference. A positional key such as a batch cursor double-charges exactly the jobs that resume, and ON CONFLICT (key) WHERE key IS NOT NULL DO NOTHING must match the partial index predicate exactly or Postgres raises 42P10.
  • Writes resolve, they do not throw. Many clients resolve an error object on a constraint or timeout, so a bare write or a destructure without error is a fully silent failure. Every write on a money, membership or tenancy path reads the error, logs it with context, and throws on a webhook or retry path so the provider redelivers. Enforce with a lint rule; satisfy it by reading the error, never by disabling it.
  • Money-path reads fail closed, because a transient blip read as "no rows" is a mis-charge: zero members grandfathers the cheapest tier forever, an unresolved customer drops a paid invoice. Conditional claims go through one primitive: claimOne(query) returns matched | not_matched | error, and only not_matched maps to 404 or busy, because a bare !data guard hides a real error as a benign no-match.
  • External resources are owned, including their teardown. A cascade reaches only rows, so whatever creates a subscription, a project, a repo or a VM owns destroying it, through one teardown primitive every delete path reuses, or documents in the ledger why it is retained. Persist the reclaim handle with or before the create: one written later leaks when the request dies in between.
  • Every non-terminal status has a reaper that is not the in-request catch, because a hard kill never runs the catch. An idle-windowed sweep terminalises processing, publishing, running, and its window is a parameter, because an updated_at trigger defeats an aged test row.
  • Webhooks resolve the account from a stored mapping written in an authenticated context, never from payload metadata, and a disagreement is an alarm.

E. Data modelling, schema and types

  • Declarative schema files are the source of truth, numbered, applied in order, with a table, its policy and its grants in the same file. A policy or index referencing a column added in a later file breaks the shadow database and disables diffing repo-wide.
  • Migrations are generated artefacts, reviewed against the silent-miss checklist: all DML, policy alterations, every REVOKE, function ACLs, views, schema-level privileges. Anything on that list is written by hand, and a clean diff proves only that the engine sees no difference.
  • Additive by default. Permitted: create table, index, function, policy, grant, and adding a nullable or defaulted column. Forbidden without a five-step plan (additive shape, backfill, flip reads, flip writes, drop the old shape): drop table, drop column, narrow a type, mass update, truncate, rename a live column. Never edit an applied migration, never change the remote through a dashboard, never reset a linked database, and apply migrations before the code that reads them, writing down which pre-existing surfaces break in the window between.
  • Types come from the database, regenerated after every schema change. Status unions are the generated enum, never a hand-written literal union: one compiled, linted and passed every test while every insert on the new surface violated the CHECK in production, silently, because the write was fire-and-forget. A test parses the schema and asserts each code-side value set equals its database constraint.
  • The API caps every response at a fixed row count regardless of the requested limit, so any read that can exceed it pages with a stable unique order. A list passed to an IN clause is bounded by request-line bytes, not key count.
  • Postgres is the queue until job types multiply: FOR UPDATE SKIP LOCKED, a lease, bounded attempts, dedup by partial index; the worker body survives a dispatcher swap.

F. Frontend: server state, forms, design system

  • Three data tiers, simplest first: server fetch into props; server fetch seeding a client query when the client must refetch, filter or paginate; a client query for the rest. Never useState plus useEffect to fetch.
  • Query keys come from the domain's key factory and are imported, never inline, with nested prefixes so a parent key invalidates its children, and invalidation written at the mutation's success handler rather than broadened to everything.
  • Forms are one pattern with no exceptions: the form library, a schema resolver, and the design system's field and message components, with server errors mapped back onto the field. Per-item pending state derives from the mutation's own variables, every query has visible error feedback, and there is no console.* in client code.
  • The design system is the only source of primitives: no raw <input> or <button>, colours from tokens, and lookup maps where every class name exists as a literal.
  • Render purity: no clock or randomness in render. Copy carries no em dashes and no filler words.

G. Portability: native and shared packages

  • The API-first data path is what makes a native app possible; nothing else here matters for portability if mutations live in framework-bound actions.
  • Shared modules, in the order a second client needs them: generated database types; contracts (rules, schemas, catalogues, key factories); pure domain math; design tokens; provider adapters. Modules a native bundler must transpile stay dependency-free apart from the validation library. The native app imports the real pricing function, so it cannot quote a value the server rejects.
  • A hook depending only on fetchApi and a key factory is shareable as-is. The native lint gate runs at zero warnings, and a package is verified to resolve before anything builds on it.
  • Vertical registries: an exhaustive switch with a never default, a lookup that throws on an unknown key, and parseX(v): X | null for any string that selects a code path.

H. Integrations and adapters

  • Normalise at the adapter boundary: each provider implements one interface and returns only normalised shapes, raw response types never leave the adapter folder, and consumers dispatch by a stored provider column rather than a default.
  • Types come from real captured responses, never the spec, kept as scrubbed fixtures. One provider documented 97 fields and returned 170; another encodes a confirmed zero as an absent key. Distinguish absent from zero end to end, because a transformer that coalesces to zero destroys the distinction settlement needs, and keep external ids as strings, because a Number() on a UUID silently turns lookups into "no data".
  • One orchestration wrapper around every provider call: duration, uniform errors, structured logs. Compensating transactions when the external create succeeds and the internal write fails, idempotent third-party operations, and sandbox mode everywhere but production.

I. Observability and lifecycle

  • Structured JSON logs through logInfo / logError / logAlarm with context, actor and error. Never raw console output on the server, never a silent catch, and a helper for thrown values that are not Error, because a plain object stringifies to [object Object].
  • Three levels, and the third is the point. error is diagnostics and there will be hundreds of distinct events; alarm means a control fired or money or trust is provably wrong, never a retryable failure. A test derives the alarm set from call sites and fails if an event is raised at both levels or alarms grow past a tenth of errors.
  • Post-response side effects go through one persist(fn), or the runtime drops them when it freezes. Every scheduled job logs one healthy-signature line with its counts, health endpoints degrade per query, and client analytics go through one typed track().

J. AI agents, VMs and durable work

Apply when the repository runs models, agents or sandboxes. Skip otherwise.

  • Grounding is code-enforced where it can be: retrieve first, refuse without calling the model on empty retrieval, verify citations against what retrieval returned rather than parsing them from prose, and log a fabricated reference loudly.
  • Retrieved, repository and sandbox content is UNTRUSTED: the prompt says quote-never-obey from one shared constant, and the hard backstop for writes is an approval gate whose classes are single-sourced and read by both the server and the client renderer, because a write tool missing from them hangs the turn. Write tools call the same feature services as the routes, never their own SQL.
  • Models are typed constants derived from the pricing table, so an unpriced model is a compile error rather than a turn billed at zero, and budgets, step caps and stream timeouts are central constants.
  • Metering is one ledger row per generation with a deterministic idempotency key, written through persist(), and it still reads the error. Sum usage from the finished steps, not the top-level total, or a hard mid-loop error meters free.
  • Every paid surface is gated on all three doors alike: the HTTP route, the tool surface and the background service. A source-scan test derives spending symbols and gate symbols per file, strips comments first (two such tests were vacuous because a gate named in a comment satisfied them), and fails a spender without a gate unless it is listed as deliberately ungated.
  • Ingestion holds four invariants: never stampede the gateway; non-destructive swap (embed then replace, keep the original on failure); honest failure (failed with a reason and a retry, never a partial index marked done); and no arbitrary caps, being background and resumable rather than truncated. A big job runs on the queue with a convergent drive keyed by content hash, so any interruption resumes to the same fixed point.
  • The sandbox is the security boundary. Pin the agent runtime version, because billing parses its event stream. Egress is deny-by-default with an exact host allowlist, credentials are injected at the firewall edge and never enter the VM, and the clone token is scoped to one repository. Wipe the tree before overlaying a repository, scan for secrets before publishing, call stop() in finally, meter VM compute as its own ledger row, persist the handle with the create, and sweep orphans on a timer.
  • Agent-facing surfaces: a public one self-limits by IP, caps every string input well above any legitimate size, and spends no model calls per request; a tenant one authenticates with a hashed bearer token, resolves the workspace from it, rejects an expired token as unknown, and omits write tools entirely for a read-only token. Tool behaviour lives in the services so the two cannot drift.

K. Dependencies and supply chain

  • One package manager, pinned, with a frozen lockfile in CI and on the host.
  • A resolution-time cooldown against freshly published versions, measured in days (pnpm counts minimumReleaseAge in minutes, so three days is 4320), with exact pins excluded because they have no older fallback, plus a PR-layer cooldown with security updates exempt. Neither replaces the other; document which door each covers.
  • audit gates CI at the level the tree can actually hold today, because a red gate everyone ignores is worse than none. Say why the level is what it is, and when to tighten.
  • CI workflow token permissions are contents: read unless a step needs more.
  • Do not hand-roll a format a dependency already parses: the security content, which bound to enforce, is the earned part; the byte-poking is not.

L. Backend structure: layers, middleware, and where the tests live

  • Four layers, each importing only downward: transport, validation, service, data access. The transport layer (a route handler, a controller, a bot handler, a queue consumer) parses the request, runs the gates and maps an outcome to a response, holding no business rule and no query. The service holds the rules and knows nothing about HTTP. Data access holds the queries and no rules. A file doing two of these is the file everything else ends up in: a thousand-line handler that validates, decides, queries and formats is the shape this prevents.
  • Cross-cutting concerns are middleware, composed once: authentication, tenant resolution, rate limiting, request ids, structured logging, the error envelope, CSRF, idempotency keys, with a route declaring which it uses. The same check copied into twelve handlers is twelve places for it to drift, and the thirteenth handler, the one without it, is the incident.
  • One error path: services return outcomes or throw one hierarchy, and the transport maps them to status codes in one place, because a handler that catches and formats its own errors invents its own envelope.
  • Single responsibility per module, split by responsibility (routing, rules, queries, formatting) and never by line count; a file past a few hundred lines is a prompt to look, not a verdict. Open for extension through seams: a registry or an exhaustive switch with a never default, so adding a provider means adding a file rather than editing every caller.
  • Dependency inversion at the trust boundary: services take the database client, the clock and the external adapters as arguments. Nothing reaches for its own connection, which is what makes the gate the real boundary and the service testable without a network.
  • KISS: no abstraction before the second caller (no repository layer over a client that already is one, no base class for one handler, no event bus for one subscriber), and delete the seam that never got one. DRY: one source per fact, with shared bounds in a validation module and shared literals in a contract module, because a copied literal in a second file drifts.
  • A test asserts an outcome, not a branch: the row that landed, the status the caller was told, never a mock standing in for the thing under test, and keep the happy-path twin or a hard-coded safe value passes.
  • Tests live in one convention, and the runner's include glob is the source of truth: either colocated or a mirrored tree, never both, with integration tests that need the database sitting together behind one setup file and one fixtures directory. A test file outside the glob never runs and reads as coverage, so derive the list from the tree and compare it with what the runner picks up.

Phase 3: plan

Produce one ordered plan. For each item: the invariant, the evidence from Phase 1, the cheapest mechanism, and what it will not catch. Order by consequence to a paying user, then by cost.

The mechanism ladder, cheapest first, because the cheapest one that works is the one that survives:

  1. A type or signature that makes the wrong thing fail to compile.
  2. A test that derives what it expects from the codebase, so there is nothing to update.
  3. A lint rule, custom if the linter supports authoring one, scoped to production source.
  4. An assertion at build or startup, so a misconfigured deploy fails before it serves.
  5. A CI step or hook.
  6. Prose. Last resort, and it says what it does not catch.

Rules for the plan:

  • One guardrail per thing that has actually broken or sits on a money, tenancy or deletion path. If you cannot name the incident, the commit or the defensive comment, do not propose it.
  • No hand-maintained list, ever: if someone must add a line when they add a file, it is already broken. Derive both sides and compare.
  • Decide alone. Where the standard leaves a choice, take the option that changes the least behaviour and holds the invariant with the cheapest rung, and record the rejected alternative in the ledger. Never leave a question for the owner in place of a decision.

Write the plan into docs/STANDARD-AUDIT.md under the audit, then execute it in that order. You do not wait for approval; the ordering rules are the approval. When an item turns out to be wrong once you are inside the code, revise the plan in the file and say why.

Phase 4: migrate the structure

Confirm the working tree is clean. If it is not, do not stash and do not touch the modified files: work around them and list them in the report as untouched.

Split the plan into serial items (structural moves, anything touching shared files) and independent items (disjoint files, one mechanism each). Serial items go first, one at a time in the main tree, with typecheck and lint after each. Independent items go to builders in isolated worktrees, each briefed with the invariant, the evidence, the mechanism, what done looks like and the verification to run. Merge one at a time, verifying after each.

  • One slice at a time: move a domain's components, hooks, services and validation into its slice and leave the page as a thin import. A move is a move, with no behaviour change in the same unit.
  • Where the schema is not declarative, do not rewrite history: capture the current state into declarative files, verify a diff against a LOCAL database built from those files is empty (the linked diff is the owner's step), and land every change from then on as an additive incremental.
  • Where privileges are open, write one tightening migration: revoke from PUBLIC, close the anonymous role, restrict the authenticated one to SELECT, restrict append-only tables, set the default privileges. This is a behaviour change, not a move: any client still writing directly from the browser stops working the moment it lands, so take those write sites from the second probe, move each behind the API first, and land the revoke only when that list is empty.

Phase 5: mechanise the invariants, and break each one

One at a time, with two agents, because the one that wrote a guardrail cannot see what it misses. The builder writes it, runs it, and confirms it passes. Then a different agent breaks the invariant deliberately: it copies the guarded file outside the repository, introduces the exact defect the guardrail exists to catch, and runs again, requiring a failure whose message names the file and says what is wrong. It restores from the copy rather than from memory, confirms byte-identical, re-runs green, and hands back what it broke and the failure output as the guardrail's proof.

Not optional, and not skippable for being obvious. A guardrail that has never failed is decoration, and worse than nothing because it manufactures confidence. Expect some first attempts to pass while broken; that is the most common defect in this kind of work. If you cannot make one fail on purpose, delete it.

Guardrails worth having in most repositories of this shape, each derived from the tree, each with its stated limit:

  • Unread write results: a lint rule flagging an awaited write whose error is discarded. Known gap: a builder captured to a variable and awaited later.
  • Money-table reads: the same rule scoped by table name for wallet, membership, ledger and grant reads. It narrows the judgement, not removes it.
  • Route contract: every handler declares the envelope return type, or the raw-body type only when the file builds a raw body.
  • Public-route register: every route that calls no gate is named in the rules file with a justification. It checks presence, never whether the reason is good.
  • Admin gate: every operator route and page uses the one gate, and a hand-rolled inline check fails even when correct, because duplication was the defect.
  • Layering: the shared-floor-to-features edge table is derived both ways.
  • Enum parity: each code-side value set that mirrors a database constraint equals it.
  • Env manifest: no duplicates, the build refuses and names every missing variable, and the CI env block contains every enforced key.
  • Config registry: every source: pointer resolves to a file that exports the constant.
  • Docs enumerations: every path a live doc names exists, and no doc hand-enumerates the rules corpus or the schema files.
  • Decision log: every superseded stamp cites an existing entry title, and the index is not stale.
  • Alarm tier: no event is raised at both error and alarm, and alarms stay under a tenth of errors.
  • Paid-surface gating: every file calling a spending symbol also calls a gate symbol, comments stripped, or is listed as deliberately ungated.
  • Audit labels: the action set, its label map and its writers are one type, and a call-site scan rejects an action nothing emits.
  • Egress: the test setup refuses any non-local host, and a test asserts the guard is installed.

Every scanner that reads source strips comments first, with a lexer that respects strings and templates. Two scanners in these codebases were provably vacuous because a symbol in a prose comment satisfied them.

Phase 6: the verification floor

  • Integration tests run the real service layer against a local database. The setup file hard-refuses a non-local URL before any client is built, fixtures provision real tenants through the real onboarding service, and row-level tests use an authenticated-role client for a real signed-in user, which is what the policies actually see. Money-path unit tests run against captured real provider responses, not against the spec.
  • A database security suite asserts anonymous lockout on every table, append-only behaviour for ledger and audit tables even as the service role, and EXECUTE denied on every security-definer function. Counts use an exact-match helper, because a substring match passes "48" against "8". It is a deploy gate, run after every migration.
  • A browser layer that checks claims, not just requests: where the product asserts something on screen, a flow drives it and compares the claim with the data behind it, against an isolated copy with neutralised credentials, never a dev server holding live ones.
  • CI on every push to the deploying branch: install frozen, lint, typecheck, audit, a production build with dummy env (which runs the env assertion), the local database, the tests, with contents: read permissions. The re-run matrix is a table in the rules file: which change requires which check, and what that check cannot see.
  • Test the outcome, not the branch; keep the happy-path twin so a hard-coded safe value cannot pass; name the safe set, not the unsafe one. Deliberately absent is a section, not a silence: list every known gap with its reason.

Phase 7: the instruction layer, the ledger and the loop

Now write the standing instructions and the domain rules, with the guardrails in place so the prose no longer has to carry what a check now covers.

The root file. AGENTS.md, or a tool file that imports it: one short file read before every task, holding only what the code cannot say (what is not obvious, what is dangerous, the commands, the consent rules) and a pointer to the domain rules. No directory tour, no framework explanation, no file lists.

The domain rules. One file per area, declaring the paths it governs so it loads only when that area is touched, with a test that every glob matches a real file. It holds how the area works, its invariants, its past mistakes and what to re-run. Where instruction files already existed, this phase is mostly deletion: report the token cost before and after.

The ledger. An append-only decision log, newest first, one entry per non-obvious decision: YYYY-MM-DD — Title, what was decided, what was rejected, why. An entry later overturned is stamped in place, where a reader meets the dead claim: > SUPERSEDED <date> by "<title>" — <one clause why>. Cite the title, not just the date, because a date alone can point at twenty entries. Once the log is too long to read whole, generate an index from the log itself, with a check that fails when it is stale. Never hand-maintain the index.

Harness enforcement. Where the tooling supports it: ask-prompts on commit and push, deny rules on production env files and destructive linked-database commands, a verifier agent defined with its instruction to refute, and hooks for checks that must run on every edit. The prose is the rule; the prompt, the hook and the permission rule are the guarantee.

The loop, written into the standing instructions as the closing section:

  • A decision gets its ledger entry in the same change as the code, never after.
  • When something breaks twice, build the check. Do not add a paragraph.
  • Never write a check that needs a hand-maintained list; derive both sides.
  • When a check replaces a rule, delete the rule in the same change, and when a check makes an existing claim false, fix every place the claim appears.
  • A rules file must shrink as often as it grows. Delete on sight: a claim the code no longer supports, a waiver whose bug is fixed, a seam that got a body.
  • The docs tests check structure, not truth: a path that resolves, a register that is complete, a count that matches. A confidently wrong sentence passes every one of them, and that part stays yours.
  • If these files only ever grow, the loop has stopped running.

Final report

Write it at the end of docs/STANDARD-AUDIT.md and summarise it in your last message: which of the seven parts existed and which you created; every move, as from and to; each guardrail, what it catches and does not, and whether breaking it proved it; what you deleted and added; the migrations and the exact steps to apply them; the ceilings left and their triggers; what you deliberately did not act on; what stayed blocked; which subagents ran and what the verifier refuted; and every claim you could not verify. Recommend nothing untested, and never commit for the owner.

07

What I actually think, stated plainly

the position

A standard is a list of failures you have already paid for. That is the whole of it. The value is not in the rules, which anyone can write. It is in the defects behind them, which you only collect by running production systems and being honest about what broke.

So the standard travels with its defects, and the prompt refuses to apply a rule ceremonially. It learns the repository before it judges it, and maps every rule to what is actually there. It audits with one reader per area and a second agent whose only job is to refute them. It writes down what survived, plans one mechanism per thing that has actually broken, and names what each one will not catch. Then it executes, in the order the standard dictates, without asking. It builds each guardrail and breaks it on purpose, because a guardrail that has never failed is decoration. The ones it cannot make fail, it deletes. It never commits. The review is the diff, once, at the end.

The honest version is narrower than the flattering one. This came from three codebases and one engineer, so it is opinionated. The examples are Next.js, Postgres and Supabase because that is where these defects happened, not where they apply. The prompt says so in its own first lines and tells the agent to translate. A named client means whatever client you have. Where your stack has no equivalent, the invariant moves up a layer into the gate rather than vanishing. Adapt the naming; do not adapt the invariants, because each one is the shape of something that broke. And a green run is not proof. The checks verify structure, a confidently wrong sentence passes all of them, and an agent that has just installed twenty of them will tell you the repository is sound. Read the diff, and read the report's list of what it could not verify first.

08

The companion piece

the instruction layer, on its own

The seventh part of the standard, the instruction layer and the loop that keeps it true, has its own essay with the measurements behind it and a narrower prompt that installs only that part. The five files that run this codebase.