What has to be true
before we call it finished.

34 commitments. Each one says what happens without it, and each carries a real artifact from a system we built. Version 1.0, 2026-08-07.

Contents
  1. Security6
  2. Data6
  3. Reliability6
  4. Change and release6
  5. Access and identity6
  6. Ownership and handover4

34 commitments. Each is separately linkable, and your browser’s find-in-page searches inside the collapsed ones.

Security

The parts that have to hold when somebody is actively trying to get past them.

Access is enforced by the database, not by the application.v1.0

Who may see which row is decided below the API, in the database itself. Application code cannot grant itself more than it is allowed, and a query that forgets a filter returns nothing rather than returning somebody else's record.

Without this

One developer writes one query without a tenant filter on a Tuesday afternoon. It passes review because it looks like the four queries above it. Six weeks later a customer opens a page and sees another customer's employees.

From our own systems

289 row-level security policies across all 78 tables.

ODOnboard · supabase/migrations

#sec-database-enforced

Every access rule is tested by trying to break it.v1.0

The tests are written from the attacker's side: read another tenant's rows, reach another employee's files, complete a step that requires review without the review, call the procedures directly and go around the application.

Without this

The policies are correct on the day they are written and nobody notices when the thirty-first one is subtly wrong, because nothing was ever asked to prove they hold.

From our own systems

rls_isolation_sanity.sql
employee_files_cross_access_reaudit.sql
tasks_review_bypass_sanity.sql
onboarding_rpc_security_sanity.sql
e2e/tenant-isolation.spec.ts

ODOnboard · supabase/tests

#sec-policies-attacked

Keys that bypass security cannot reach a browser.v1.0

A service key ignores every access rule by design. Ours is read in exactly one place, on the server, and a test walks every component and page to prove nothing else touches it.

Without this

A key ends up in a client bundle during a refactor. Everyone who loads the page now holds read-write access to the whole database, and nothing looks wrong.

From our own systems

The service role key bypasses row-level security. Reading it outside a route handler risks it reaching a client bundle.

This site · lib/intake/intake.test.ts

#sec-no-secrets-client

Anything that accepts input from the public validates it and rate-limits it.v1.0

Type, size and content are checked before anything is stored, refusals explain how to fix them, and one sender cannot flood the endpoint.

Without this

The upload form accepts a macro-enabled workbook, or a two-hundred-megabyte file, or four hundred submissions in a minute, and you find out from the bill.

From our own systems

We do not accept .xls or .xlsm, because both can carry macros. Save it as .xlsx or .csv and it will go through.

This site · lib/intake/validate.ts

#sec-public-input

Demonstration and test modes cannot reach live data.v1.0

Where a system has a demo mode, destructive actions are pinned to simulate inside it — regardless of what the configuration says. Setting the mode to live while demo mode is on does nothing.

Without this

Somebody runs a demo against the wrong environment and disables a real person's account in front of an audience.

From our own systems

it('is pinned to simulate under DEMO_MODE regardless of the env var', () => {
  process.env.RESPONSE_ACTIONS_MODE = 'live';
  expect(getMode()).toBe('simulate');
});

Security Command Centre · tests/responseActions.test.ts

#sec-demo-cannot-reach-live

A check that could not run blocks the action rather than allowing it.v1.0

An empty result and a failed lookup are different values and are treated differently. If we could not confirm something is safe, we do not proceed on the assumption that it is.

Without this

The directory lookup times out, the code reads the empty response as "holds no admin roles", and an administrator account gets disabled during an incident.

From our own systems

Couldn't confirm whether this account holds an administrator role, so the usual check could not run. Verify in Entra before proceeding, or confirm you accept the risk.

Security Command Centre · server/services/responseActions.ts

#sec-fail-closed

Data

What we hold, how long we hold it, and how it leaves.

One organisation's data is unreachable from another's session.v1.0

Multi-tenant systems share one database and one deployment. The boundary between organisations is enforced per row, and it is verified by a signed-in session trying to cross it in a real browser.

Without this

Somebody writes a report query, tests it against their own organisation, and ships it. It is correct for every customer with one organisation and wrong for the one customer with three — and that customer finds out by reading a competitor's numbers.

From our own systems

Querying the intake table as the anonymous role returned [] while the table held two rows, and an insert returned 42501.

This site · verified 2026-08-07

#data-tenant-isolation

Deleting someone's data is a supported workflow, not a favour.v1.0

Erasure has a case record, a subject map, a log of what was removed and when, and a job that performs it. A person asking to be forgotten is a path through the system rather than a support ticket and somebody writing SQL.

Without this

The request arrives, somebody runs a delete by hand, and nobody can later evidence what was removed — which is the part a regulator asks about.

From our own systems

security.erasure_case
security.subject_map
public.erasure_log
public.retention_policies
functions/purge-employee-data

ODOnboard · supabase

#data-erasure-is-a-feature

Retention is enforced by the database, not by anyone remembering.v1.0

The date at which a record may be deleted is written by the database when the record is created, and the application cannot override it.

Without this

The retention policy is a paragraph in a document, and four years of data nobody needed is still there when it is asked about.

From our own systems

create trigger set_intake_dates
  before insert or update of submitted_at on public.spreadsheet_intake
  for each row execute function public.set_intake_dates();

This site · supabase/migrations/20260807000001_spreadsheet_intake.sql

#data-retention-enforced

We do not store what we can avoid storing.v1.0

Where a coarse signal will do, we hold the coarse signal. Rate limiting on this site works from a salted hash rather than an IP address, so there is no address to disclose, protect or delete.

Without this

A subject access request arrives. Somebody now has to work out what the field collected "in case it is useful" was ever used for, find every place it was copied to, and explain all of it in writing inside a month.

From our own systems

Coarse abuse signal only. Not an IP address: we do not need one, and holding one turns this table into a personal-data problem it does not have to be.

This site · supabase/migrations/20260807000001_spreadsheet_intake.sql

#data-collect-less

The audit trail is written by the work, not alongside it.v1.0

What happened and who did it is recorded as a consequence of the action itself. It is not a second step anyone has to remember, and it cannot be skipped when things are busy.

Without this

The log depends on somebody filling it in, which means it is complete for the first month and empty for the month you actually need.

From our own systems

public.audit_logs
public.jml_audit_events
public.task_activity
public.document_acknowledgements

ODOnboard · supabase/migrations

#data-audit-side-effect

Customer names do not appear in storage keys, logs or URLs.v1.0

Identifiers are opaque. The original filename is held in a column where it is protected, not in a path that ends up in an error report, a log line or a support conversation.

Without this

Somebody hits an upload error. The storage key rates-final-v3-KellyRetail.xlsx goes into the stack trace, the stack trace goes to an error dashboard, and a third-party vendor now holds one of your customers' names.

From our own systems

A storage key is the one part of this system most likely to end up in a log line, an error report or a support conversation. Putting a customer's name in it would leak by accident, forever, in places nobody thought to check.

This site · lib/intake/validate.ts

#data-no-identifiers-in-keys

Reliability

What we measure, and what we refuse to guess at.

Performance is gated on what a browser measured, not on a prediction.v1.0

Budgets are checked against values a real browser recorded. Where a tool offers a modelled figure and a measured one, we gate on the measured one and say why.

Without this

The gate reports a number that moves the wrong way when the page improves, so the team learns to ignore it — and then it catches nothing at all.

From our own systems

Simulated LCP: 2533ms. Observed LCP: 61ms. Same page, same run. We gate on the second.

This site · scripts/perf-budget.mjs

#rel-observed-not-simulated

A page that gets slower fails the build.v1.0

Script weight, total weight, fonts, third-party bytes, layout shift and paint are all budgeted, and the budget runs on every change rather than when somebody remembers to look.

Without this

Nothing is ever slow enough to fix on the day it lands, and eighteen months later the page takes four seconds and no single change is to blame.

From our own systems

app script 0.2 / 70 KiB · total weight 268.7 / 400 KiB · LCP (observed) 59 / 116 ms · cumulative layout shift 0

This site · npm run perf, 2026-08-07

#rel-budgets-in-ci

Nothing moves under the reader's cursor.v1.0

Cumulative layout shift is budgeted at exactly zero, not at the 0.1 the industry treats as a pass. Fonts are metric-matched and every image reserves its space.

Without this

Somebody taps a link, a web font loads, the paragraph moves, and they have tapped something else.

From our own systems

"cumulative-layout-shift": ["error", { "maxNumericValue": 0 }]

This site · lighthouserc.json

#rel-zero-cls

When an automated run fails, it names the stage that failed.v1.0

A failure is recorded with the step it happened in, the timings around it and the error, rather than as a single line saying something went wrong.

Without this

The nightly job failed at 3am. It is now 9am, and finding out which of eleven stages broke costs an hour before any fixing starts.

From our own systems

create table public.pipeline_traces (
  trace_id uuid not null,
  fn text not null,
  status text check (status in ('ok','failed')),
  failed_stage text,
  entries jsonb not null default '[]'
);

Scúp · supabase/migrations

#rel-failure-names-the-stage

Anything that acts on your behalf leaves an account of what it did.v1.0

Not a log line — a structured record of what was looked at, what was acted on, what was deliberately left alone, and what changed as a result.

Without this

The automation did something you disagree with and there is no way to establish what it saw, so the only available response is to switch it off.

From our own systems

run_receipts.summary — watched, surfaced, rested, mailSeen,
automatedIgnored, contactsCreated, quotesDetected, chasesDue,
repliesNoticed, filesClosed, phantomsCleared, invoicesOverdue,
remindersDue, autoSent, stateChanges, mode, errors

Scúp · supabase/migrations

#rel-runs-leave-receipts

The page renders its full argument without JavaScript.v1.0

Content, navigation and forms work with scripting unavailable, blocked by a corporate policy, or still loading on a bad connection.

Without this

A procurement reviewer on a locked-down machine opens the page and sees an empty shell, and that is the impression that goes into the report.

From our own systems

0.2 KiB of application JavaScript, including the two pages with forms and the estimator.

This site · npm run perf, all six measured routes

#rel-works-without-js

Change and release

How something gets from an idea into production without surprising anyone.

Nothing merges that has not typechecked, linted, tested and built.v1.0

One command runs all of it, the same one locally and in CI, so there is no version of "it works on my machine" available.

Without this

The check that is slow to run is the check that stops being run, and the first person to notice is a user.

From our own systems

"verify": "npm run typecheck && npm run lint && npm run format:check && npm run test && npm run build"

This site · package.json

#chg-one-gate

The build refuses to ship a claim we cannot stand behind.v1.0

Where a value is provisional, the code knows it is provisional and fails a production build rather than rendering it. Honesty is enforced by the toolchain rather than by memory.

Without this

A draft price, an unvalidated figure or a placeholder goes out because the person who knew it was draft was not in the review.

From our own systems

Refusing to publish draft pricing. The bands are development defaults that have not been validated against the market.

This site · lib/pricing/guard.ts

#chg-build-refuses

Every visual component appears in a reviewable specification.v1.0

Each one is rendered in isolation, in all its states — including the failure states nobody can reach by clicking around — and a test fails if a component exists that nothing displays.

Without this

The error state was never looked at, so the first person to see it is the person it is happening to.

From our own systems

A component nothing displays is a component nobody reviews.

This site · app/page-discipline.test.ts

#chg-visual-spec

Schema changes are versioned files, applied in order, with a way back.v1.0

The database's shape is in the repository and arrives at the same state on every machine. Reversals are written where the change is not trivially reversible.

Without this

Two environments diverge, and the difference is only discovered by a query that works in staging and fails in production.

From our own systems

migrations/
migrations_rollback/
migrations_archive/

ODOnboard · supabase

#chg-migrations-versioned

Every non-obvious decision is written down with what it cost.v1.0

Not just what was chosen — what was rejected, what the trade-off was, and what evidence would change it. Including the ones that turned out badly.

Without this

In eighteen months somebody undoes a deliberate decision because it looked like an oversight, and rediscovers the reason the expensive way.

From our own systems

49 decisions recorded, including the ones we reversed and why.

This site · docs/DECISIONS.md

#chg-decisions-recorded

Comments explain why, and are kept where the next person will look.v1.0

The reasoning lives beside the code it justifies, not in a wiki that goes stale. What a line does is readable from the line; why it is that way is not.

Without this

The next developer — possibly one of yours — deletes something load-bearing because nothing said what it was load-bearing for.

From our own systems

A grid item defaults to min-width: auto, which means it refuses to shrink below its content's minimum. So a table cell containing a long identifier made its grid item 505px wide inside a 320px viewport.

This site · components/layout/Section.module.css

#chg-comments-carry-why

Access and identity

Who can do what, and how that is decided.

A person sees their own record and nothing else.v1.0

Where a system has people outside the organisation in it, what each can see is scoped to them at the database level, not by a filter the interface applies.

Without this

An employee opens their onboarding page and sees a colleague's salary, because one query was written without the clause the other forty had.

From our own systems

79 tables with row-level security enabled; 31 SQL suites verifying it; one browser-level tenant-isolation spec above them.

ODOnboard · supabase

#acc-own-record-only

Permission levels are data, not a deployment.v1.0

Adding a role, or changing what one can do, is a change an administrator makes. It does not require us, and it does not require a release.

Without this

A supervisor is promoted and needs to approve timesheets. Changing that is a support request to us, so it waits three days — and in the meantime somebody lends her their login, which is now how she does it permanently.

From our own systems

public.org_roles
public.org_role_assignments
public.access_grants

ODOnboard · supabase/migrations

#acc-roles-are-data

An organisation can bring its own identity provider.v1.0

Single sign-on is configurable per organisation, so leavers lose access when they are removed centrally rather than when somebody remembers this system exists.

Without this

An employee leaves on a Friday. IT disables their directory account that afternoon, and nobody thinks about this system because it is not on the offboarding checklist. Their login still works in March.

From our own systems

public.sso_configurations
tests/sso_login_sanity.sql

ODOnboard · supabase/migrations

#acc-sso

An action that cannot be undone says so before it runs.v1.0

Every consequential action declares its effect, whether it is reversible, what its inverse is if it has one, and any delay between doing it and it taking effect.

Without this

An analyst signs someone out everywhere, believes the account is contained, and does not know that tokens already issued stay valid for a few more minutes.

From our own systems

Microsoft can take a few minutes to fully invalidate tokens already issued. Treat the account as still reachable until then.

Security Command Centre · server/services/responseActions.ts

#acc-irreversible-declared

You cannot perform a destructive action on your own account.v1.0

The system refuses, and says why, rather than letting somebody lock themselves out of the tool they are using to handle the incident.

Without this

The only person with access disables their own account at the worst possible moment.

From our own systems

This is your own account. Ask a colleague to action it so you don't lock yourself out.

Security Command Centre · server/services/responseActions.ts

#acc-no-self-action

Every integration declares the exact permission it needs.v1.0

Not a broad scope covering everything we might one day want. Each action names the specific permission it requires, so what we have been granted can be audited against what we actually use.

Without this

The integration is granted full directory access because that was easiest, and it stays that way for three years.

From our own systems

REVOKE_SESSIONS      → User.RevokeSessions.All
FORCE_PASSWORD_RESET → User-PasswordProfile.ReadWrite.All
DISABLE_ACCOUNT      → User.EnableDisableAccount.All

Security Command Centre · server/services/responseActions.ts

#acc-least-privilege-declared

Ownership and handover

What is yours, and what you would need if we stopped existing tomorrow.

You own the repository from the first commit.v1.0

Not on final payment, not on project completion. It is in your account or your organisation from the start, and we work in it.

Without this

The code is leverage. Every conversation about scope happens with the other party holding the thing you paid for.

From our own systems

You own everything from commit one. Here's how you'd leave us.

This site · published on / and /how-we-work

#own-repository

Infrastructure runs in your accounts, not ours.v1.0

Database, hosting, storage and the domain are billed to you and administered by you. We hold access to operate them; we do not hold them.

Without this

Leaving means a migration project rather than removing a user, and the estimate for that migration comes from the people you are leaving.

From our own systems

It is in your accounts, on your infrastructure, with the repository in your name — so stopping here leaves you with software, not with a bill for a discovery exercise.

This site · published on /how-we-work

#own-infrastructure

The schema explains itself to whoever reads it next.v1.0

Tables and the non-obvious columns carry comments in the database, so the documentation arrives with the data rather than in a file that drifts from it.

Without this

A new developer finds a nullable column called status_2 and has to guess, or ask somebody who has left.

From our own systems

comment on table public.spreadsheet_intake is
  'No role reachable from a browser may read or write this table;
   the route handler uses the service role. Rows are deleted
   after retain_until.';

This site · supabase/migrations/20260807000001_spreadsheet_intake.sql

#own-schema-documents-itself

Nothing is built on a platform only we can operate.v1.0

Standard languages, standard databases, standard hosting. No in-house framework, no licence held by us, nothing that requires our tooling to deploy.

Without this

Another firm quotes to take it over, then doubles the quote when they see what it is built on.

From our own systems

Three runtime dependencies: next, react, react-dom.

This site · package.json

#own-no-proprietary-lock

What this version does not cover.

  • Restore-time and recovery-point objectives. We take backups; we have not yet published a tested time to restore, and publishing one we had not rehearsed would be the exact failure this document argues against.
  • Response times and out-of-hours cover. All support is business hours today, which is stated on the homepage rather than hidden here.
  • Independent penetration testing. Our access rules are verified by our own tests, which is not the same as being verified by somebody whose job is to defeat them.
  • Sub-processors and data residency. Named per engagement in the contract; not yet a standing published list.
  • A formal access-review cadence. Permissions are auditable and reviewed in practice; there is no published schedule for it yet.
Version history

What changed, and why.

v1.0
Added
34 commitments across 6 sections, each with a real artifact.WhyEvery one is true today and evidenced from a system we built and run. The roadmap asked for forty; six of those would have been commercial promises rather than engineering practices, and they are named in the section above instead of being written as though they were already true.