Milestone 1 — Tenant Hardening & Axiomatic + Categorical Kernel Architecture
Why This Matters
Multi-tenant accounting software has one non-negotiable guarantee: a tenant’s financial data must be structurally impossible to read, write, or reassign to another tenant — not merely unlikely, not protected only by application logic, but enforced at the database level regardless of how the application behaves.
Before this release, SpeyBooks enforced tenant isolation at the application layer through row-level security policies that filtered every query by organisation. That protection was real. But it had two structural gaps that this release closes permanently.
The first gap: several tenant-scoped tables had row-level security enabled but not forced. In PostgreSQL, the distinction matters precisely at the boundary where security is most needed — the database owner role. A session operating as the table owner bypasses RLS entirely unless FORCE is set. That means the role used to run migrations, perform administrative operations, or respond to a production incident could read or write any tenant’s data without the policies firing. This was not a theoretical risk. It was the default behaviour.
The second gap: every update policy in the schema was missing a WITH CHECK clause. The USING predicate on a policy controls which rows are visible to a query, but it does not re-evaluate after a mutation. Without WITH CHECK, an UPDATE that passes the visibility check can change the organisation_id of the row it touches — moving a financial record from one tenant to another. The row is visible to the session at the start of the update and belongs to a different tenant at the end. WITH CHECK closes this by re-evaluating the predicate against the post-mutation row state, making any organisation reassignment a fatal policy violation.
Both gaps are now closed mechanically at the database layer, with structural invariants inside the migration transaction that verify correctness and fail closed if any violation is detected.
This release also formalises the complete SpeyBooks kernel architecture as a locked specification — the design foundation all subsequent milestones build on.
Tenant Isolation Hardening
Row-Level Security: Enable and Force (Axiom 1.1)
Row-level security has been enabled and forced on all tenant-scoped tables across the schema. The categorical definition of a tenant-scoped table throughout this work — and encoded in the structural invariant that verifies it — is any table carrying an organisation_id column. This definition is derived from schema structure, not from a developer-maintained name list. A table added in a future migration that carries an organisation_id will be caught by the same invariant automatically.
FORCE ROW LEVEL SECURITY on all tenant tables Enabling RLS alone is insufficient. Without FORCE, the table owner role — which runs migrations, performs schema changes, and responds to incidents — bypasses all policies. FORCE makes RLS unconditional: it applies to every role, including superusers acting as the table owner. Both ENABLE and FORCE are now required and verified for every tenant-scoped table in the schema.
Two pre-existing schema defects resolved by invariant checks The structural invariant checks inside the migration surfaced two defects not visible in the original audit. Seventeen tables had been created under a different database role and were not owned by the schema owner, which would have prevented the DDL operations from completing. One table had RLS forced but not enabled — an anomalous and contradictory state that the invariant correctly rejected. The migration failed closed on both, the defects were resolved, and the migration was re-run cleanly. This is the intended behaviour: invariants that catch real problems in production are doing their job.
Update Policy Containment (Axiom 1.2)
WITH CHECK on all update policies Every update policy now carries a WITH CHECK clause that is identical to its USING predicate. The canonical predicate —
organisation_id = current_setting('app.current_org_id')::uuid— is evaluated both before and after every mutation. Any attempt to change theorganisation_idof a row will pass the pre-mutation check (the row belongs to the current tenant) and fail the post-mutation check (the row would now belong to a different tenant). The result is a fatal policy violation. This applies to all 25 tenant update policies across the schema.Policy name independence The structural invariant that verifies this property checks all UPDATE policies on all tenant-scoped tables, regardless of policy name. An ad-hoc policy added in future without a WITH CHECK clause is caught by the same invariant as any other. The protection is categorical, not name-coupled.
Axiomatic + Categorical Kernel Architecture — Specification Locked
The complete SpeyBooks kernel architecture has been formalised, adversarially reviewed by multiple independent reviewers, and locked as a specification. This is the design foundation all subsequent milestone work is built against. The specification does not describe aspirations — it describes the delivered and target state of the system, with every gap named, classified, and assigned to a milestone.
Design Paradigm: Axiomatic + Schema-Driven Categorical Design
SpeyBooks is a deterministic financial kernel — not a CRUD SaaS application. The design paradigm is Axiomatic + Schema-Driven Categorical Design, which requires two properties simultaneously:
- Mathematical honesty — no claim is made beyond what is mechanically enforced. Where an invariant is partially enforced, that partial status is named explicitly with its upgrade path to full mechanical enforcement stated. There are no security claims that rely on developer discipline.
- Mathematical closure — every threat class has a named axiom enforced at a minimum of two independent layers. Bypassing one layer does not bypass the other. This is the difference between defence-in-depth as a policy and defence-in-depth as a structural property.
Axiom Classification System
Architectural invariants are classified into two tiers that make the current enforcement state of every invariant explicit:
Class M — Mechanically enforced. Violation produces a fatal database exception. No developer action required at runtime. Includes: database constraints, triggers, row-level security policies, and verified build tooling.
Class O — Operationally enforced. Relies on external audit, policy, and incident process. Every Class O axiom carries a named upgrade path to Class M enforcement with a milestone assignment. Class O exists not as a permanent state but as an honest accounting of where enforcement is today.
Monetary Domain Partition
All monetary values in the system are partitioned into three formally distinct domains with no overlap. Domain A: monetary amounts — integer pence, no floating-point, no fractional currency. Domain B: rates — exact rationals reduced to integer pence via deterministic database functions before any value enters the ledger. Domain C: quantities — exact rationals with declared precision constraints. Sub-penny values are prohibited by schema constraint. The monetary domain is closed: no fractional penny can exist anywhere in the reduction chain.
Schema-Derived Type System
The database schema is the single source of truth for both the TypeScript type system and API input validation. Types and validation schemas are generated from the schema, not hand-authored. A dedicated codec layer is the sole location for the snake_case (PostgreSQL) to camelCase (TypeScript) data boundary translation — no business logic is permitted at that layer. Hand-authored types that can drift from the schema are a build failure.
Append-Only Cryptographic Ledger
The ledger architecture uses a per-tenant Merkle chain. Each entry carries a SHA-256 hash over a canonical encoding of its content and the hash of the previous entry. The canonical time axis is the ledger sequence number — a database-generated identity column assigned at commit, not at statement start. Timestamp columns are cosmetic. The chain can be verified from first principles: any tampered entry breaks the chain. This architecture arrives in Milestone 6 but is specified here because Milestones 1 through 5 are prerequisites.
Operational Impact
- Zero tenant-scoped tables with row-level security enabled but not forced — verified by structural schema introspection, not name lists
- Zero update policies on tenant-scoped tables missing or mismatching WITH CHECK — verified across all UPDATE policies regardless of policy name
- Organisation reassignment closed as a mutation pathway — WITH CHECK makes it a fatal policy violation
- Table owner RLS bypass closed — FORCE makes isolation unconditional for all roles
- Two pre-existing schema defects surfaced and resolved by invariant checks before commit — ownership drift across 17 tables, anomalous RLS state on one table
- All DDL changes transactional — full commit or full rollback, no partial application possible
- Fail-fast timeouts prevent hung deployments
- Adversarial review applied across two independent reviewers — four amendments incorporated
Files Changed
Backend:
api/db/migrations/068-milestone-1-tenant-hardening.sql— ENABLE and FORCE RLS on 5 tables, WITH CHECK on 25 update policies, two structural invariant validation blocks using schema introspection, three post-commit verification queries
Architecture:
CONST-KERNEL-v2.3-FINAL.md— Kernel constitution, locked. Eight ADRs, six delivery milestones, axiom classification system, monetary domain partition, ledger architecture, verification protocol.speybooks-dev-contract-v2.3-FINAL.md— Development contract, locked. Layer boundaries, forbidden patterns, codec layer specification, verification checklist.speybooks-schema-audit-report-v2.3-FINAL.md— Schema audit report, locked. 58 findings across six milestones, pre-flight queries, schema anchor sign-off table.
Milestone 1 establishes the tenant isolation foundation. Milestone 2 — immutability and state machine enforcement — follows next.