Skip to content

The platform is live.

Take a look
The Lab
Architecture essay · 22 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

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 the reason it is a rule and not a preference.

In a hurry? Skip to the prompt. It is free, and it works on any TypeScript repository.

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, and they are followed about as well as any request is followed by a busy team, which is to say 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.

The three codebases are a wagering platform with an append-only ledger and automated settlement, a two-sided coaching marketplace on Stripe Connect and a calendar provider, and the multi-tenant AI portal that runs this site, which runs coding agents in sandboxed virtual machines. Different products, same failure classes. 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 the standard those defects produced: seven things a production codebase has to have, the eleven areas of rules that hold them, the ladder of mechanisms that keeps them true without anyone remembering, and a prompt that puts a coding agent to work installing all of 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
11areas of rules, each with the failure it prevents
14grep probes, each a known defect class
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, balance arithmetic that never reads then writes, EXECUTE revoked from PUBLIC after the money functions turned out to be callable with the browser key, and provider adapters typed from captured responses because the spec lied about the field count.

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, refund and fee flags that default to the opposite of what a marketplace wants, batch email that does not lose ninety-nine recipients to one bad address, and sandbox mode everywhere but production.

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 on every paid surface, the sandbox as the security boundary, and the instruction layer and every one of the fifteen guardrails, most of them written after the defect they now catch had already happened here.

areas A · B · C · E · F · J · KRead 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

Read the top of each card and you have the whole standard. Under each one: the defect that made it a rule, what holds it in place once it exists, and the rules themselves for anyone building it.

01

A feature-sliced structure with one-way imports

areas A · G of the standard

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

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.

The failure it prevents

A module in the shared floor that imports a feature is that feature's policy wearing a shared path. Once one exists, the next agent cites it as precedent, and the direction is gone. This platform has four such edges: one sanctioned aggregator and three listed violations, and a test derives both sides so a fifth cannot join quietly and a fixed one cannot stay listed.

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 what kind of thing it is: all the screens in one folder, all the data access in another. It reads well on day one and falls apart the moment a change touches one feature, because 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, which matters because direction is the thing that erodes first and is noticed last.

Show the rulesfor engineers

features/<domain>/ owns components/, hooks/, services/, validation.ts and types.ts, with 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 and nothing else: no _components/ under it, and 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 whose whole job is to present 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, so the lift into a package is a move rather than 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 of the standard

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.

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

The failure it prevents

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

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. Every one of those shortcuts is a second door, and every second door has 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 all call identically. When the service fixes a bug, every consumer inherits the fix.

Show the rulesfor engineers

The route shape never varies: gate first, before any database work, with the handler early-returning 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, and 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, and the typed client 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 of the standard

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.

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

The failure it prevents

CREATE FUNCTION grants EXECUTE to the PUBLIC pseudo-role by default, and every role is a member of it, so revoking from named roles removes nothing. One of the source codebases found its settle, void and create money functions callable with the browser key that way.

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 is security that fails the day someone edits that check. The standard asks for three independent layers, each of which would hold on its own, so that a mistake in the application, a mistake in the database grants, or a mistake 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, and a migration tool that generates the schema diff will drop 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.

Show the rulesfor engineers

Layer one is the application gate: the session is verified, the tenant is resolved from the authenticated user, and 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, because an operator who resolves as owner on every tenant would otherwise 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 a balance history cannot be rewritten by buggy service code because 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 and 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, access helpers that are SECURITY DEFINER and STABLE with an empty search_path, and tests that run as the authenticated role inside a transaction, because 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 of the standard

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.

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

The failure it prevents

A hand-written status union compiled, linted and passed every test while every insert on the new surface violated the CHECK constraint in production, silently, because the write was fire-and-forget and nothing read its error.

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, so every other description of that data, 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, and the value sets a program is allowed to write are derived from those types rather than 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.

Show the rulesfor engineers

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 and 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 rather than 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 of the standard

The invariants that cost money, trust or tenancy are caught by a type, a derived test, a lint rule or a build assertion, and each one has been 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.

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

The failure it prevents

Two source-scan tests were provably vacuous because a symbol mentioned in a prose comment satisfied them. Five guardrails were recorded as verified by breaking; a second pass told to refute rather than confirm found that three of them could not fail.

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 that have each been shown to catch the exact defect they exist for, by introducing that defect deliberately and watching the check go red.

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

Show the rulesfor engineers

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, and 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 and 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, 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. And 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 of the standard

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

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

The failure it prevents

Nothing about portability matters while mutations live in server actions. And a native app that reimplements the pricing function will, eventually, quote a value the server rejects.

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: if every mutation already goes through an API, and the rules already live in framework-free modules, then 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 asking for the monorepo before it is earned. Keep the domain logic pure, keep the contracts in one place, and the day a second consumer arrives the work is a move rather than a rewrite.

Show the rulesfor engineers

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 of the standard

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.

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

The failure it prevents

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. Every instruction with an automatic check behind it had survived.

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, 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, and it needs the same discipline as the rest: nothing in it that a check does not hold, and nothing in it that 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.

Show the rulesfor engineers

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, and 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 11 areas of rules

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

This is what the prompt audits against. Two areas carry a condition: the money path applies only if the repository moves value, and the agent rules only if it runs models. Everything else applies to any repository of this shape.

AStructure and layeringFeature slices, not type folders, with 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 goes through a versioned API route that gates, validates, delegates and answers in 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 layersAn application gate, table privileges that start closed, and row-level policies underneath, so no single mistake opens the money or tenant 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, one atomic settlement procedure, content-addressed idempotency keys, and writes and reads 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 files, generated migrations reviewed against what the diff engine misses, additive changes, and types that come 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 from simplest up, one form pattern, query keys from a factory, and a design system that is the only 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 and cannot quote a value the server rejects.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, and wrap every provider call in one orchestration layer.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 lifecycleStructured logs at three levels, where the third means a control fired or money is provably wrong, and post-response work that survives the runtime 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 is reserved for a control firing
  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 on all three doors, and the sandbox as the security boundary.The failure Two gating tests were vacuous because a gate named in a prose comment satisfied them, and 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 resolution-time cooldown against fresh versions, and an audit gate held at the level the tree can bear.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, with the reason written down
  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.

05

A rule without its defect gets applied ceremonially

Why every rule names its failure

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

So the standard is written as rules with the failure each one prevents, and the prompt tells the agent to judge whether that failure applies here. When it does not, the agent says so in the proposal and skips the rule. That judgement is only possible when the defect is named. A rule that only says what to do cannot be skipped honestly, because there is nothing to weigh it against.

The other reason is that the defect is the memory. A rule outlives the incident that produced it, and a year later the team, or the agent, has no idea why it exists and quietly relaxes it. Written next to its failure, the rule carries its own justification, and relaxing it means arguing with a specific outage rather than with a preference.

06

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; a status union derived from generated types; a model key derived from the pricing table.
  2. 2A derived testnothing to update, so it cannot go staleIt walks the tree, parses the schema, reads the config, 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, in CI, before it fails in production.
  4. 4A build or startup assertiona misconfigured deploy fails before it servesThe env manifest fails the production build and names every missing variable at once.
  5. 5A CI step or hookwhen nothing above can hold itFrozen install, lint, typecheck, audit at the held level, a production build with dummy env, the local database, the tests.
  6. 6Proselast resort, and it says what it does not catchFor what genuinely cannot be mechanised. A 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. Each one is proven by breaking the thing it guards on purpose. And each one says what it does not catch, because a check without a stated limit invites more trust than it has earned.

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.
07

The prompt

run it against your own repository

Take this with you

Bring this repository up to the standard

Point a coding agent at any TypeScript repository and run this. It audits first and changes nothing until you say so, holds the code to the eleven areas above with the failure behind every rule, refuses to apply a rule the repository does not need, and proves every guardrail it installs by breaking it on purpose. Written for React, Node and Postgres, with Next.js, Hono and Expo as the named targets.

  1. 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.
  2. 01
    Audit, change nothingWhat exists and whether it is still true, with counts. A set of grep probes, each a known defect class. What has already gone wrong, from the evidence. What is checked today and what depends on memory. Then stop.
  3. 02
    The target shapeEleven areas of rules, each written with the failure it prevents, so the agent can judge whether the failure applies here and skip the rule honestly when it does not.
  4. 03
    Propose the cheapest mechanismOne 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. Wait for approval.
  5. 04
    Migrate the structureOne slice at a time, typecheck and lint after each move, and a move is a move: no behaviour change in the same unit. Where grants are open, one privilege-tightening migration, then the security suite.
  6. 05
    Mechanise, and break each oneWrite it, run it, copy the file, introduce the exact defect, watch it fail, restore byte-identical. Fifteen guardrails worth having, each derived, each with its stated limit. Delete any that cannot fail.
  7. 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.
  8. 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.
  9. 08
    The final reportWhich 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, the ceilings left in place, and every claim that could not be verified.

Run it on Opus 5 with maximum effort, in a fresh session. Phase 1 is a judgement task across a whole codebase and a weaker setting produces a plausible list rather than an evidenced one. Expect several sessions: it stops and waits twice before it builds anything.

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.

The standard below was distilled from production codebases built and operated by one engineer working 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 a multi-tenant AI portal that runs coding agents in sandboxed VMs. Every rule exists because its absence caused a real defect. Where a rule names the defect, that is the reason it is a rule and not a preference.

Framework stance: the invariants are framework-agnostic; the examples are TypeScript, React, Node and Postgres. Three concrete targets are named throughout: Next.js App Router for a full-stack app, Hono for a standalone or multi-runtime API, Expo for native. Adapt the naming to whatever the repository uses. Do not adapt the invariants.

What done looks like

When you finish, this repository has these seven things, and not one of them contains a claim that nothing checks:

  1. A feature-sliced structure with one-way imports. Domain code lives with its domain; the router holds only routing; a shared floor never imports upward.
  2. One data path. Every read and write from a client goes through a versioned API with a fixed envelope, gated, validated, delegated to a service, and consumed through one typed client helper. No server actions, no ORM calls from the browser.
  3. Three-layer security that holds when any one layer is wrong: an application gate that resolves identity and tenant before any data work, table-level privileges that start closed, and row-level policies as the net underneath.
  4. A data model whose types come from the database, whose schema is declarative, whose migrations are additive and reviewed, and whose value sets cannot drift between SQL and TypeScript.
  5. A verification floor: 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; plus an integration floor that runs the real service layer against a local database and cannot reach a third party.
  6. Portability by construction: pure domain logic and contracts in packages a second client (native, agent, another operator) can import without a build step.
  7. An instruction layer that stays true: standing instructions, path-scoped domain rules, an append-only decision ledger with a generated index, and a written loop that turns each defect into a mechanism.

If some of these exist, audit and repair them. If none do, build them. The governing idea is the same either way: an instruction nobody checks goes stale, and a stale instruction is worse than a missing one because an agent follows it without hesitating. Every claim you write must be verifiable against the code today, or replaced by a mechanism that verifies itself. Prefer the mechanism.

Absolute constraints

  • Never run git commit, git push, git reset --hard, or any history rewrite unless the owner tells you to in the message you are answering. Doing work is not consent to commit it. If the harness supports it, make this a permission prompt, not just prose.
  • Never run migrations, seeds, resets or deploys against anything but a local database. If a command's blast radius is unclear, do not run it. Ask.
  • Never read, print, grep or echo a secret value. Check existence or length only.
  • Never delete or overwrite an existing file without showing the diff and asking.
  • Do not install dependencies without asking. Do not remove a supply-chain control (install cooldown, audit gate, frozen lockfile) to make a command pass.
  • 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.
  • Long, compute-heavy runs (a full test suite, a browser walk, a production build, an eval) are the owner's clock. State what needs running and let them choose. Typecheck, lint and a single targeted test file are always fine.
  • Report what you could not verify. An unverified claim stated as fact is the exact failure this system exists to prevent.

Phase 1: audit. Change nothing.

Read only. Produce a written report, then stop and wait.

1a. What already exists. Agent instruction files at the repo root and in any tool config directory (AGENTS.md, CLAUDE.md, .claude/rules/, .cursorrules), plus README, CONTRIBUTING, ARCHITECTURE, docs and any decision log. For each: path, line count, rough token cost (words divided by 0.75), last modified, and how much restates what the code already says. 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. Run these probes and record counts and locations. Each one is a known defect class from the source codebases:

# Run from the repository root. git grep skips ignored paths (node_modules, build
# output) and takes quoted pathspecs, so these run the same under bash and zsh,
# and every regex is POSIX so BSD and GNU grep agree.

# Server actions (lock the backend to one framework; unusable by mobile/agents)
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: supabase-js RESOLVES { error }, it never throws
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'

# Data fetching in effects
git grep -nE "useEffect\([^)]*fetch|useEffect\([^)]*\.from\(" -- '*.tsx'

# Inline query keys (silently break invalidation)
git grep -nE "queryKey: \[['\"]" -- '*.ts' '*.tsx'

# Raw form controls where a design system exists
git grep -nE "<(input|button|select|textarea)([^[:alnum:]_]|$)" -- '*.tsx'

# Hardcoded colours where tokens exist
git grep -nE "bg-\[#|text-\[#|(bg|text|border)-(red|green|blue|emerald|amber)-[0-9]" -- '*.tsx'

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

# Route handlers without an explicit return type
git grep -nE "export async function (GET|POST|PUT|PATCH|DELETE)\([^)]*\)[[:space:]]*\{" -- '*route.ts'

# Numeric fallbacks in business logic (hide null bugs)
git grep -nE "(balance|stake|amount|price|total|count)[a-zA-Z_]* \?\? 0" -- '*.ts'

# Server-side getSession() (spoofable)
git grep -n "getSession()" -- '*.ts' | grep -v client

# Reads that can exceed the API row cap
git grep -nE "\.limit\((1[0-9]{3,}|[2-9][0-9]{3,})\)" -- '*.ts'

Then, against the database schema if there is one:

  • Which functions have EXECUTE granted to PUBLIC (a NULL proacl in pg_proc is the default, and the default is open). Any SECURITY DEFINER function in that set is a P0: it runs as its owner and bypasses both grants and RLS.
  • Which tables grant anything to anon; which grant writes to authenticated.
  • Which foreign keys to the auth users table lack an explicit ON DELETE.
  • Whether the ledger, audit or event tables can be updated or deleted by the service role.
  • Whether TypeScript status unions are hand-written or derived from generated types.

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; the code teaches it.

1c. What has already gone wrong. Evidence, not intuition:

  • Commit messages shaped like incidents: fix, revert, hotfix, regression, "again", "actually". A bug fixed twice is a guardrail waiting to be written.
  • Defensive comments: "do not remove", "must run before", "looks redundant but". Each is an invariant someone learned the hard way and could only write in prose.
  • Clusters of TODO, FIXME, HACK. Where they cluster matters more than how many.
  • Anything touching money, authentication, tenant or user isolation, data deletion, external calls, retries, background jobs, webhooks. Silent failure costs most here.
  • The same defensive check repeated in many places. Repetition under duress is an unmechanised invariant.

If history is squashed or short, say so and lean on the other sources. 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. Linter, and whether it gates or reports. Type checking. CI: what actually blocks a merge, and with what token permissions. Whether anything stops a test from 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 (mobile, an agent, an MCP server, a partner) today or in the stated roadmap? Which logic would it need (validation, pricing, rules) and where does that logic live now? Where are the hard ceilings: the API row cap, in-memory rate limiters per instance, a single-fixture or single-tenant foreign key that the roadmap needs to be many-to-many, a queue that does not exist.

Stop here. Report what exists, what is stale with counts, the invariants with their evidence, the portability gap, and the gap between what matters and what is checked. Wait for a decision.

Phase 2: the target shape

This is the reference you audit against and migrate toward. It is 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 proposal 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 (Zod, shared by client form and API route), types.ts (the contract both services and hooks import), optionally rules.ts (framework-free business rules) and utils/ (pure predicates). Create only what the domain needs; empty folders are forbidden. Thin slices (components only, or data only) are fine.
  • The router holds routing primitives only. page.tsx, layout.tsx, route.ts, loading.tsx. No _components/ directories under the router. A page is five to seven lines: it imports a feature component and renders it. Pages never contain form logic, data fetching or business logic.
  • Cross-cutting UI lives in components/ and nothing else: ui/ (design-system primitives, no business logic), providers/, layout/, marketing sections. Nothing new goes into a components/shared/; that directory is where slices go to die.
  • One-way imports. types → lib → hooks → components + features → app. lib/ is the shared floor and never imports from features/, components/ or app/. A module in lib/ that reaches up into a feature is that feature's policy wearing a lib/ path; move it. The one sanctioned exception is an aggregator whose whole job is to present every domain's tunables in an operator panel, and it is listed as such.
  • Derive the layering table, both ways. If you document which modules violate the direction, a test walks the filesystem and compares. A new violator fails; a fixed one still listed also fails, so nobody cites a dead violation as precedent.
  • Alias imports for cross-directory, relative only for siblings in the same slice.
  • A monorepo when a second consumer exists, not before. apps/{web,mobile,...} over packages/{types,contracts,domain,adapters,ui,tokens,eslint-config,typescript-config}. Packages that a native bundler must transpile from source stay dependency-free apart from the validation library. Until the second consumer arrives, keep pure domain logic (rules, pricing, catalogues) in files that import nothing framework-specific, so the lift is a move, not a rewrite.
  • Name conventions once. Schemas are <feature><Action>Schema. Hooks are use*. Query-key factories are <domain>Keys. Services take the injected database client as the first argument. Amounts are integers in the smallest unit; timestamps are UTC.

B. The one data path

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/tenant id)
          → database
  • No server actions. They lock every mutation to one framework and are invisible to a native app, an agent, an MCP server or a partner. Route handlers are inspectable, testable and callable by anything that speaks HTTP. This applies to auth callbacks and integrations too; the only thing the browser SDK does directly is authentication and push-based realtime.
  • The route shape, always in this order:
    1. Gate first, before any database work. The gate returns a response on failure and the handler early-returns it.
    2. Validate the body with a module-level Zod schema from the feature's validation.ts. Report the first issue as path: message, HTTP 400. A malformed [id] param answers 404, never a raw cast error 500.
    3. Call the service with the injected client and a params object. The service returns an outcome; it never returns a response object.
    4. Return the envelope: { success: true, message, data } or { success: false, message }, HTTP status on the response. Never { error }. Rate-limited is 429 with Retry-After and the same body shape.
  • Explicit return type on every handler. Promise<NextResponse> for the envelope, Promise<Response> only for a handler that 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. Derive the set of raw-body handlers from the code (a new Response(, a ReadableStream) and fail a Promise<Response> on a handler that builds none.
  • One typed client helper. Every hook calls fetchApi<T>(url, init?, timeoutMs?). It sets the JSON header unless the body is FormData, applies a default abort timeout, reads the envelope once, throws message on failure, returns unwrapped data on success. Raw fetch in a hook is a defect. The browser timeout stays below the server's hard kill so the client sees a clean error before the function dies.
  • Behaviour lives in the service; every consumer inherits it. A REST route, an agent tool and an MCP tool all call the same updateTask; a miss returns false in one place and becomes 404, a tool error and not_found respectively. Never fix behaviour in a consumer.
  • Input bounds live in validation.ts field validators and are imported by the API schema and every tool surface. A .max() hardcoded in a tool will drift.
  • Public, no-auth routes are a register, and the register is derived. A test walks every route file, finds the ones that call no tenant 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.

Hono, when the API is a separate deployable. Same four steps, same envelope, same services. Differences that matter:

  • Routes must be chained and composed with app.route('/x', sub) for the type to be inferred; export type AppType = typeof routes from the composition root.
  • Validate with zValidator('json' | 'form' | 'query' | 'param', schema); without a validator the client's request type is unknown.
  • Return c.json(body, status) with an explicit status on every arm, or the client cannot narrow on res.status.
  • The typed client hc<AppType>(baseUrl) replaces hand-written response interfaces on the web and native clients. In a monorepo, compile the API's types (project references) or the IDE will compute them on every keystroke.
  • One codebase deploys to Node, Bun, Cloudflare Workers, Deno and Vercel. Choose Hono over framework route handlers when the API has non-React consumers, needs to run somewhere the frontend does not, or when coupling the backend's release cadence to the frontend's has become a liability. Keep the envelope identical so clients cannot tell which one they are talking to.

C. Security, three layers

Any one layer failing must not open the money or tenancy path.

Layer 1: the application gate.

  • verifyAuth() verifies the session (local JWT verification is fine for the proxy; a server-authoritative call when you need the user object; never a spoofable session read on the server) and returns { userId, adminClient } or a 401 response.
  • Ownership after identity. A resource the caller does not own is 403 on an owned surface, 404 on a tenant surface (the same answer as "does not exist", so there is no existence leak).
  • verifyAdmin() checks a role stored in a row the user cannot write, never a self-writable flag. A platform-operator gate answers 404, never 403, and its predicate is an operator flag, 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 body or query. It is resolved from the authenticated user. The one sanctioned body-carried id is on an operator route whose gate is identity-based, and it is documented as the exception.
  • Paid or expensive surfaces take a second gate immediately after the tenant gate: rate limit, then budget. It returns 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), a replay window where the provider supports one, and answers 401 on a bad signature.
  • The CSRF check lives in the proxy: a mutating /api/* request whose Origin is present and not one of this deployment's hosts is 403. Fail closed on an unparseable origin. Token and HMAC callers send no origin and pass.
  • Every post-auth redirect target goes through one safeNext() that allows only a clean single-slash relative path, blocking //evil and /\evil.

Where the application reaches the database through a single role (an ORM over a pooled connection, a service account) rather than per-request roles, layers two and three collapse into the gate plus the append-only grants: say so in the proposal, and do not invent roles the database does not have.

Layer 2: table privileges, starting closed.

  • Explicit REVOKE ALL on every table from every role, then the minimum grants back. anon gets nothing. authenticated gets SELECT only, row-scoped by RLS. service_role gets CRUD 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 to rewrite it does not exist.
  • Revoke EXECUTE from PUBLIC, not just from named roles. CREATE FUNCTION grants EXECUTE to the PUBLIC pseudo-role by default and every role is a member of it, so per-role revokes remove nothing. Add ALTER DEFAULT PRIVILEGES ... REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC or every future function reopens the hole. A source codebase found its settle, void and create money functions callable with the browser key this way.
  • A new table starts closed. A client-readable table gets RLS, a SELECT policy and a SELECT grant in its own schema file. A secret, money or infrastructure table gets RLS on, no policy, no grant: service-role only, served to the client through the API.
  • The diff engine drops REVOKEs and does not diff function ACLs at all. Carry the grants file verbatim and probe the deployed catalog after every grants migration.

Layer 3: row-level security as the net.

  • Own-row SELECT policies on user tables; SELECT policies on reference tables; zero client write policies anywhere, so a direct write is denied even if a grant slips.
  • Access helpers are SECURITY DEFINER, STABLE, set search_path = '', read auth.uid() internally, and are the single source every policy calls. Grant them to authenticated only where a policy needs them.
  • Test RLS as authenticated inside a transaction (set local role plus the JWT claims config). Outside a transaction you silently run as superuser and RLS is bypassed, which is a false pass.
  • A settlement-lock or job table with RLS on and no policies is intentional and must be documented as such.

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 (required | production | feature | platform) and a note. The production build fails when a required or production var is missing and names every missing one at once. The same list feeds an operator config page. A test pins the two. Enforce when absence produces a healthy-looking lie; leave as feature only when degradation is visible at the point of use.
  • Tunable constants that govern spend or safety are registered in one aggregator surfaced read-only to the operator, with a source: file pointer a test resolves.
  • Unsubscribe and similar public links use a per-user random UUID token, never an enumerable row id; GET renders a confirmation, POST mutates (mail scanners fetch every link); the RFC 8058 one-click headers point at the same URL; the route always redirects to the same page regardless of outcome so an attacker gets no signal.
  • Free text that reaches an HTML sink (email bodies) is escaped. JSX auto-escapes; the email templates do not.
  • Rate limiting is per serverless instance and says so. Public no-auth routes that send email or spend money IP-limit themselves and carry a honeypot.
  • An audit log for operator actions is append-only at the grant level, records both success and failure paths (including the validation-fail 400), and runs a recursive key scrubber over its payloads that drops balances, stakes, payouts, emails and tokens. In development the scrubber throws so the leak surfaces at the call site.
  • Every foreign key to the auth users table declares ON DELETE: cascade for owned data, set null for attribution, and a deliberate block on primary ownership. Right to erasure must be executable, not blocked by a constraint nobody chose.
  • Check-then-act RPCs (caps, quotas, once-per-user) take a transaction-scoped advisory lock keyed by the user id as their first statement. The session-scoped variant breaks under connection pooling.

D. The money path

Skip this section if the repository moves no value. Apply all of it if it moves any.

  • The ledger is append-only at the grant level. No UPDATE, no DELETE, for anyone. Corrections are new signed rows, never edits.
  • Settlement is one atomic stored procedure. Resolve, update status, write the ledger row and adjust the balance in one transaction. Balance arithmetic is balance = balance + delta, never read-then-write, with a CHECK (balance >= 0) underneath. Concurrent settlement is guarded by FOR UPDATE SKIP LOCKED, a settled_at IS NULL predicate 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 (a batch cursor) double-charges on exactly the jobs that resume. ON CONFLICT (key) WHERE key IS NOT NULL DO NOTHING must match the partial unique index predicate exactly or Postgres raises 42P10.
  • Writes resolve, they do not throw. The database SDK resolves { error } on a constraint or timeout. A bare await db.from(t).update(v) or a destructure without error is a fully silent failure. Every write on a money, membership or tenancy path reads { error }, logs it with context, and throws on a webhook or retry path so the provider redelivers. Enforce with a lint rule (see Phase 5); satisfy it by reading the error, never by disabling the rule.
  • Money-path reads fail closed. 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. Read { error } and throw. Scope a second lint rule by table.
  • Conditional claims go through one primitive. claimOne(query) returns matched | not_matched | error; map only not_matched to 404 or busy. A bare !data guard hides a real error as a benign no-match with zero operator signal.
  • External resources are owned, including their teardown. A cascade reaches only rows. Whatever creates a subscription, a project, a repo, a VM, owns destroying it on delete, through a single teardown primitive every delete path reuses, or documents in the decision ledger why it is retained. Persist the reclaim handle atomically with or before the create; a handle written in a later statement leaks when the request dies between them.
  • Every non-terminal status has a reaper that is not the in-request catch. A hard kill (OOM, max duration, SIGKILL) never runs the catch. An idle-windowed sweep folded into the jobs worker terminalises processing, publishing, running. Its window is a parameter, because an updated_at trigger defeats an aged test row.
  • Webhook handlers resolve the account from a stored mapping written from an authenticated context, never from payload metadata; a disagreement is an alarm. Process synchronously (the provider retries a non-2xx) and rely on the idempotent grant. Gate on the provider's live-mode flag where events from both modes share an endpoint.
  • A provider's refund and fee flags default to the opposite of what a marketplace wants; name them explicitly and comment the consequence of each default.
  • Stakes, caps, fee percentages and multipliers are configurable data or named constants, never inline literals; the client displays what the server will honour, computed by the same function.

E. Data modelling, schema and types

  • Declarative schema files are the source of truth, numbered, applied in file order. A new table, its RLS policy and its grants go in the same file. A policy or index that references a column added in a later file breaks the shadow database and disables diffing repo-wide.
  • Migrations are generated artefacts, reviewed line by line against the silent-miss checklist: all DML, ALTER POLICY, every REVOKE, function ACLs, views, schema-level privileges. Anything in that list is written by hand. A clean diff proves only that the engine sees no difference; probe the catalog for grants and ACLs.
  • Additive by default. Permitted without discussion: create table, create index, create function, add nullable or defaulted column, create policy, grant. Forbidden without a five-step plan (additive shape, backfill job, flip reads, flip writes, drop old shape after verification): drop table, drop column, narrow a type, mass update, truncate, rename a live column. Three sanctioned backfills, all idempotent and bounded: the add-nullable → update → set-not-null triple in one file; an insert-on-conflict to seed a new table for existing users; a narrowly-scoped integrity fix under ten thousand rows.
  • Never edit an applied migration. Never change the remote through a dashboard. Never reset a linked database. Migrations are applied before the code that reads them, and the window between is written down, including what pre-existing surfaces break in it (an export that enumerates all tenant data reads the new table too).
  • Types come from the database. Regenerate after every schema change. Status unions are Database["public"]["Enums"]["x"], never a hand-written literal union: a hand-written surface set 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 files and asserts each TS value set equals its SQL constraint.
  • Query-specific interfaces match the select shape exactly. Joins are typed at the query with the SDK's override method, not double-cast. Spread from rows rather than listing fields. No any, no ignore directives; as only for as const, realtime payloads, external API responses after a runtime shape check, and enum narrowing with the check on the line above. No numeric fallbacks in business logic; ?? null for inserts, ?? undefined for SDK params, display fallbacks for nullable UI text only. ?? for nullable, || only when falsy is intended.
  • The API caps every response at a fixed row count regardless of .limit(). Any read that can exceed it pages with a stable unique order. An .in() list is bounded by request-line bytes, not by key count; batch by measured size for the key shape actually passed.
  • Postgres is the queue until job types multiply. One generic jobs table, SELECT ... FOR UPDATE SKIP LOCKED in a plpgsql claim function (a language sql set-returning function gets inlined and the LIMIT stops binding), a visibility-timeout lease, attempts counting deaths not drives, a bounded max_attempts that terminalises, a partial unique index for dedup of live jobs, a per-minute cron as the heartbeat. Swap the dispatcher for a durable external queue when scale demands; the worker logic does not change.
  • Derived counts that several writers must keep true are maintained by a trigger, not by remembering to set them in each writer.
  • format() in PL/pgSQL supports %s, %I, %L only. A C-style specifier fails at runtime on the first winning settlement.

F. Frontend: server state, forms, design system

  • Three data tiers, simplest first. Server component fetch → props (no client cache) for pages that render without refetch. Server fetch → props → useQuery({ initialData }) when the client must refetch, filter or paginate. Client useQuery/useMutation for everything that cannot be server-fetched. Never useState + useEffect to fetch. Dehydrate and hydration boundaries only when a page genuinely needs SSR plus stale tracking; the framework already does SSR through props.
  • Query keys are declared in the domain's key factory and imported, never written inline. Nested prefixes so ['stripe','status'] invalidates under ['stripe']; each namespace has .all. Parameterised keys with arrays are memoised by callers. Invalidation is scoped and written at the mutation's onSuccess, never over-invalidated.
  • staleTime 60 seconds, refetchOnWindowFocus off, one provider at the root, a singleton client in the browser.
  • Forms are one pattern with no exceptions: useForm + resolver + the design system's FormField/FormMessage. A form owns its mutation lifecycle; do not also wrap it in useMutation. Server errors map to form.setError. File inputs keep their own state inside the same form.
  • Per-item pending state is mutation.variables === id && mutation.isPending, not a separate useState. Derived lists are useMemo, not a synced state. Destructive actions confirm first and use one retryable-mutation hook that toasts with a Retry action; do not also toast in a caller's onError.
  • Realtime subscriptions stay manual (useEffect + channel) with minimal dependency arrays; optionally write into the query cache.
  • Every useQuery has visible error feedback; every user-visible mutation has onError. No console.* in client code; the server already logged it.
  • Render purity: no Date.now() or Math.random() in render or in a memo. Use the query's dataUpdatedAt, a lazy useState initialiser, or pass nowMs into pure predicates.
  • The design system is the only source of primitives. No raw <input>, <button>, <select>. Colours come from theme tokens, never hex or palette classes; exceptions are white or black with opacity on dark surfaces and third-party brand marks. Dynamic class names are purged by the JIT compiler; keep materialised lookup maps so every variant exists as a literal. Interactive filter chips are buttons with aria-pressed or role="radio", never a clickable badge. Repeated typographic treatments are primitives, not inline classes. When you need smaller than a primitive's default, build a feature-local styled span and leave the primitive alone.
  • Internal navigation uses the framework's link component. Sign-in and sign-out finish with a full reload, never a client-side push (stale server-component cache). Public env vars are read as literal process.env.NEXT_PUBLIC_X; optional chaining or dynamic lookup prevents inlining and yields undefined in the bundle.
  • The request proxy excludes /api/ (routes do their own auth) and every static asset type including root-level .js (a service worker behind a redirect fails to register).
  • User-facing copy: no em dashes, no AI-slop words, correct pluralisation, one voice. Emails are light-designed regardless of a dark app, use solid backgrounds (gradients survive dark-mode inversion while text does not), and bake logo contrast into the image.

G. Portability: native and shared packages

  • The API-first data path is what makes a native app possible. Nothing else in this document matters for portability if mutations live in server actions.
  • Shared packages, 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 (palette emitted to whatever the native styling layer reads); provider adapters. The native app imports the real pricing function and the real rules, so it cannot quote a value the server rejects.
  • React Query is a drop-in on React Native; a hook that depends only on fetchApi and a key factory is shareable as-is. The typed API client (hc<AppType> on Hono, or the envelope helper) is the seam.
  • Expo Router with native tabs; the tab bar caps at four or five, so off-tab screens need an explicit entry point. Every navigation carries its subject as a param; a bare push opens whatever the screen defaults to. Empty states state the real reason ("squads publish an hour before kickoff"), never "no data".
  • Extract native primitives after duplication appears, not before: press feedback, screen header, segmented control. Record which screens they replaced.
  • The native lint gate must run with --max-warnings=0; a config that downgrades every rule to a warning makes a bare lint exit zero no matter what is wrong. Verify a package resolves before building on it; a decision recorded as done that was never executed is a recurring class.
  • Public env vars on native use the platform's prefix. Nothing native reads a secret.
  • Vertical registries for "add a sport, a provider, a tenant type without editing callers": an exhaustive switch with a never default so a new union member is a compile error; a registry lookup that throws on an unregistered key rather than falling back to the first entry; parseX(value): X | null for any string from the database or a request that selects a code path, with every caller handling null.

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. Consumers dispatch by a stored provider column, never by a default.
  • Types come from real captured responses, never the spec. Keep scrubbed captures as test 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. null means the provider reported nothing and the consumer decides (push, refuse, default); 0 is a value. A transformer that coalesces to zero destroys the distinction the settlement layer needs.
  • External ids are strings end to end; convert integers inside the one adapter that emits them. A Number() on a UUID silently turns lookups into "no data".
  • One orchestration wrapper around every provider-touching call: duration, uniform error shape, structured logs, one instanceof on a shared base error class. The adapter retries HTTP internally with a per-attempt AbortSignal.timeout; the wrapper handles route-level concerns. Retry mechanism and transient-classifier are separate: one bounded exponential loop, a per-caller classify.
  • Compensating transactions: external create succeeds, internal write fails, clean up the external resource. Idempotent third-party operations: check-before-create, upsert, reactivate-existing. Batch APIs with permissive validation so one bad recipient does not lose ninety-nine; detect 429 and stop batching; pace between batches.
  • Never let a client pace-less server job stampede an external gateway: one bounded concurrency primitive, one retry-with-jitter primitive, both shared.
  • Where a provider has two modes (sandbox and live), the local and preview environments point at sandbox permanently; only production points at live; ids and prices are per-mode and never hardcoded.

I. Observability and lifecycle

  • Structured JSON logs through logInfo / logError / logAlarm with { context, userId | tenantId, error }. Never raw console output on the server. Log success and failure; never a silent catch. errText(e) for thrown values that are not Error (a plain error 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 (metering, persistence) go through one persist(fn) that keeps the function alive until the callback settles; a bare void fn() is dropped when the runtime freezes after the response.
  • Every scheduled job emits one healthy-signature line with its counts so "is it alive" is a grep. An idle run is all zeros, not silence.
  • Health and analytics endpoints degrade per query: one failing query renders a dash, not a blank page. The SDK does not throw on a permission failure; check the resolved error explicitly and log it.
  • Analytics in the client go through one typed track() over a typed event registry, gated on consent, never a raw capture call.

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; empty retrieval refuses without calling the model (zero cost); citations are verified against what retrieval returned, never parsed from prose; a fabricated reference is logged loudly. Two predicates in one module: the ledger's coarse refused and the card's strictly stronger refusalIsShowable; a surface that asserts "I stopped rather than guess" must exclude coding turns, tool answers and failed searches.
  • Retrieved content, repository content and sandbox content are UNTRUSTED. The prompt says quote-never-obey from one shared constant; the hard backstop for writes is an approval gate. Approval classes (DESTRUCTIVE_TOOLS, ADDITIVE_TOOLS) are single-sourced and read by both the server and the client renderer; 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 code constants, ModelKey derived from the pricing table, so an unpriced model is a compile error rather than a turn billed at zero. Budgets, step caps and stream timeouts are central constants. Keep the model SDK and its React binding in lockstep; a lone bump forks the SDK into two copies.
  • Metering is one ledger row per generation with a deterministic idempotency key, written through persist(), and it still reads { 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 MCP tool, and the background service. A source-scan test derives the spending symbols and the gate symbols per file, strips comments first (two such tests were vacuous because a gate mentioned in a comment satisfied them), and fails a spender without a gate unless it is listed as deliberately ungated with a decision entry.
  • Ingestion holds four invariants: never stampede the gateway (serialise client uploads, bound concurrency at one chokepoint with retry and jitter); 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); no arbitrary caps (background and resumable rather than truncated). A big job runs on the Postgres 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 (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. The clone token is scoped to one repository or the boot fails with nothing reserved. Wipe the tree before overlaying a repository; exclude build artefacts from history at boot; scan for secrets before publishing. Call stop() in finally; meter VM compute as its own ledger row; persist the VM handle before or with the create; sweep orphans on a timer.
  • Durable execution when a multi-step agent must survive a crash or deploy: Vercel Workflows ("use workflow" / "use step", a durable agent wrapper that makes each tool call a retryable step) or an equivalent (Temporal, Inngest, DBOS, Restate). Below that scale the Postgres queue plus a worker is enough, and the worker body does not change when the dispatcher does.
  • MCP surfaces: a public one self-limits by IP, caps every string input at ten to a hundred times any legitimate size, and spends no model calls per request; a tenant one authenticates with a hashed bearer token, resolves the workspace from the token, 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 surfaces cannot drift.
  • A brand or knowledge corpus that feeds both pages and an agent is single-sourced; the hand-written seams are listed and updated in the same change as the page.

K. Dependencies and supply chain

  • One package manager, pinned in packageManager. Frozen lockfile in CI and on the host.
  • A resolution-time cooldown (minimumReleaseAge, days not hours; pnpm counts it in minutes, so three days is 4320) against freshly published versions, with exact pins excluded because they have no older fallback. A PR-layer cooldown in the dependency bot config with security updates exempt. Neither replaces the other; document which door each covers and what changes on the next major of the package manager.
  • audit gates CI at the level the tree can actually hold today; 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; ask what the tree already knows before writing a parser. The security content (which bound to enforce) is the earned part; the byte-poking is not.

Phase 3: propose

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 (an exhaustive switch with never, a key type derived from a record, a status union derived from generated types).
  2. A test that derives what it expects from the codebase: it walks the tree, parses the schema, reads the config. There is nothing to update, so it cannot go stale.
  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, for what cannot be mechanised, and it says what it does not catch.

Rules for the plan:

  • One guardrail per thing that has actually broken or is 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 it needs someone to add a line when they add a file, it is already broken. Derive both sides and compare.
  • Structural moves and behaviour changes are separate items. A move that also changes behaviour cannot be reviewed.
  • Name the ceilings you are leaving in place (per-instance limiter, a single-parent foreign key) and the trigger for revisiting each.

Present it. Wait for approval before building.

Phase 4: migrate the structure

Confirm the working tree is clean, or that every file you will touch is unmodified. If not, stop and say so.

  • One slice at a time. Move a domain's components, hooks, services and validation into its slice; leave the page as a thin import; run typecheck and lint after each move. A move is a move: no behaviour change in the same commit-sized unit.
  • Introduce the gate, validate and envelope helpers first, then convert routes to the four-step shape one at a time, each with its explicit return type. Convert server actions to routes plus hooks; delete the action.
  • Introduce fetchApi and the key factories; convert hooks; delete inline keys.
  • Pull pure domain logic (rules, pricing, catalogues) into framework-free modules. If a second consumer exists or is on the roadmap, create the packages and point both apps at them; otherwise leave them in place, framework-free, and note the lift in the ledger.
  • Where the schema is not declarative, do not rewrite history. Capture the current state into declarative files, verify that a diff against a local database built from those files is empty (a diff against the linked database is the owner's step), and land every change from then on as an additive incremental. Never regenerate the baseline.
  • Where grants are open, write one privilege-tightening migration: revoke from PUBLIC, close anon, restrict authenticated to SELECT, restrict append-only tables, set the default privileges. Then run the security suite. This one is a behaviour change, not a move: any client that still writes through the browser SDK stops working the moment it lands. Take the browser-side write sites from the second probe, move each behind the API first, and land the revoke only when that list is empty. Never apply it to a database whose clients still write directly.
  • Every non-obvious choice you make here gets its ledger entry in the same change.

Phase 5: mechanise the invariants, and break each one

One at a time:

  1. Write it. Run it. Confirm it passes on the correct codebase.
  2. Break the invariant deliberately. Copy the file to a temporary location outside the repository first. Introduce the exact defect the guardrail exists to catch. Run again. It must fail, and the message must name the file and say what is wrong.
  3. Restore from the copy, not from memory. Confirm byte-identical. Re-run, green.
  4. Record what you broke, the failure output, and the restore.

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 of your 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 that fit this shape, each derived, each with its stated limit:

  • Unread write results: a lint rule that flags an awaited database write or RPC whose { error } is discarded or destructured without error. Precise by design: the awaited chain must contain .from(...) plus a write method, or .rpc(...). Known gap: a builder captured to a variable and awaited later.
  • Money-table reads: a second rule, scoped by table name, that requires { error } on SELECTs of the wallet, membership, ledger and grant tables. It narrows the judgement; it does not remove it.
  • Route contract: every route handler declares Promise<NextResponse> or, only when the file builds a raw body, Promise<Response>.
  • Public-route register: every route file that calls no tenant gate is named in the rules file with a justification.
  • Admin gate: every operator route and operator page uses the one gate; a hand-rolled inline check fails even when correct, because duplication was the defect.
  • Layering: the lib → features edge table is derived both ways.
  • Enum parity: each TypeScript value set that mirrors a SQL CHECK or enum equals it; status unions are derived from generated types.
  • Env manifest: no duplicates; production build refuses and names every missing variable; feature and platform tiers are never enforced; the CI production-build 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; no doc hand-enumerates the rules corpus or the schema files; docs that declare Status: PLAN or an archived banner in their first fifteen lines are exempt.
  • Decision log: every > SUPERSEDED stamp cites an existing entry title; the generated index is not stale.
  • Alarm tier: no event is raised at both error and alarm; alarms stay under a tenth of errors.
  • Paid-surface gating: every file that calls a spending symbol also calls a gate symbol, comments stripped, or is listed as deliberately ungated with a decision.
  • Audit labels: the canonical action set, its label map and its writers are one type; a call-site scan rejects an action nothing emits.
  • Egress: the test setup patches fetch and the HTTP modules to refuse any non-local host, and a test in the suite asserts the guard is installed.

Every scanner that reads source strips comments first with a small lexer that respects strings and templates. Two scanners in the source 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. Row-level tests use an authenticated-role client for a real signed-in user, which is what RLS actually sees. Test files run serially against one shared local database.
  • Money-path unit tests run against captured real provider responses, not against the spec.
  • A SQL security suite asserts anon lockout on every table, append-only behaviour for the ledger and audit tables even as the service role, EXECUTE denied on every SECURITY DEFINER function from anon, and seed counts by identity not by table count. Counts use an exact-match helper; 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. A repeatable walk of every key surface at mobile and desktop viewports records console errors and same-origin failures. If the product asserts things on screen (a balance, a receipt, a status, a refusal), a flow drives that surface and a judge compares the claim against the data behind it. It runs against an isolated copy of the tree on its own port with neutralised credentials behind a deny-by-default egress firewall that logs every blocked call; a plain dev server holds live credentials underneath its local overrides and must never be driven. It is not CI-gated, so it is a discipline written down, with the judge rule that a red step is suspected of being the harness before it is called a product bug.
  • A read-only canary against production for the things code cannot see: provider dashboard settings, DNS, certificates, what is actually deployed. Each item carries its probe command.
  • CI on every push to the deploying branch: install frozen, lint, typecheck, dependency audit at the held level, a production build with dummy env (which runs the env assertion), start the local database, run the tests. permissions: contents: read.
  • The re-run matrix is a table in the rules file: which change requires which check. It says which suite, and it says what the suite cannot see.
  • Two rules about what a test may assert. A control whose effect lives in a third party is tested by reading the third party's observable state back, never your own branch; a stored content type was correct and inert for months until someone read it back out of the bucket. And a test that is green locally can be green while production violates the invariant; say so in the test when the invariant depends on a migration being applied.
  • 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; build fixtures the way production builds them.
  • Deliberately absent is a section, not a silence. Tell "deferred on purpose" from "forgotten" by listing 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 is read by most agent tools and is the cross-tool standard; in a monorepo the nearest one wins. If the repository uses a tool-specific file (CLAUDE.md), either make it import the root file or keep it to what is genuinely tool-specific. Either way, one short file an agent reads before every task, holding 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 where the domain rules live. It does not hold a directory tour, a framework explanation, or any list of files, modules or tables. Point at the directory instead.

The domain rules. One file per area, named for the area, under a rules directory. At the top, declare the paths it governs (paths: frontmatter, or whatever the local tooling reads) so it loads when that area is touched and stays out of the way otherwise; cross-cutting rules carry no glob and load always. Inside: how the area works and why, the invariants, the mistakes already made there, and what to re-run before calling a change done. Do not restate what another file owns. Past roughly two hundred and fifty lines, split the history out rather than append. The rules file holds the rule; the narrative goes to the ledger or an archived history file it points at.

If an instruction file already existed, this phase is mostly deletion: remove everything the code, the filesystem or a new guardrail now states, and move area-specific content into its own file. Report the token cost before and after. If it grew, justify it.

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. It records reasoning, not instructions. 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; a date alone can point at twenty entries. Once the log is too long to read whole, generate an index of titles and line numbers from the log itself and add a check that fails when the index is stale. Never hand-maintain the index.

The backlog. One index file that owns no content and points at every source that owns unfinished state, each with its shape (append-only, delete-as-you-close, plan, runbook). A doc that describes unbuilt code declares Status: PLAN in its first fifteen lines so the docs test stands down for it. Closing an item means deleting it from its owning source.

Harness enforcement. Where the agent tooling supports permission rules, add ask-prompts on commit and push and deny rules on reading production env files and on destructive linked-database commands. The prose is the rule; the prompt is 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. 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.
  • Surgical edits only. Never rewrite an instruction file from scratch.
  • The docs tests check structure, not truth: a path that resolves, a register that is complete, a count that matches. A confidently wrong sentence still passes every one of them. That part stays yours, and it is the part that matters.
  • If these files only ever grow, the loop has stopped running.

Final report

State plainly: which of the seven parts existed before and which you created; every structural move, as a list of from → to; each guardrail, what it catches and what it does not; which you proved by breaking and which you could not; what you deleted; the migrations you wrote and the exact operator steps to apply them in order; the ceilings you left in place and their triggers; what you found in the audit but deliberately did not act on; and every claim you could not verify. Recommend nothing you have not tested.

08

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, but in the defects behind them, which you can 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 audits first and changes nothing. It proposes one mechanism per thing that has actually broken, names what each one will not catch, and waits. Then it builds each guardrail and breaks it on purpose, because a guardrail that has never failed is decoration, and it deletes the ones it cannot make fail.

The honest version of the claim is narrower than the flattering one. This was distilled from three codebases by one engineer working with agents, so it is opinionated and it is scoped: TypeScript, React, Node and Postgres, with Next.js, Hono and Expo as the named targets. Adapt the naming to your repository. Do not adapt the invariants, because every one of them is the shape of something that broke.

And a green run is not proof. The mechanisms check structure: a path that resolves, a register that is complete, a count that matches. A confidently wrong sentence passes every one of them, and an agent that has just installed twenty checks will tell you the repository is sound. Read the diff. The prompt ends with a report of every claim it could not verify, and that section is the one to read first.

09

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.