Chapter 10 · Foundations

How this series is verified

The harness behind every later claim: a real Postgres instance, two tenants in every fixture, a fake clock, the payment processor's test mode, and the four tests that run before anything else.

7 min read

Every number and every assertion in the next 140 chapters comes out of the setup described here. It is written early because a claim about isolation or proration means nothing without knowing what checked it.

Rule 1: a real database

Row-level security is enforced by Postgres. A mocked database layer returns whatever the mock was told to return, so a suite built on mocks will pass with every policy deleted.

// Per-suite schema against a real instance. Fast enough because the schema is // created once and truncated between tests, rather than migrated per test. export async function withDatabase(fn: (db: Db) => Promise<void>) { const schema = `test_${randomUUID().replaceAll('-', '')}` await root.execute(sql`create schema ${sql.identifier(schema)}`) try { const db = connect({ searchPath: schema }) await migrate(db) await fn(db) } finally { await root.execute(sql`drop schema ${sql.identifier(schema)} cascade`) } }

The irony is deliberate and worth noticing: schema-per-tenant was rejected for production in Chapter 2 and is exactly right for test isolation, because the tenant count is bounded by the number of concurrent test workers and nothing has to be migrated twice.

Rule 2: every fixture creates two tenants

A fixture with one tenant cannot fail an isolation test. The default factory makes two, and the second one's rows exist solely to be absent from results.

// The default. Asking for one tenant is the thing you have to do explicitly. export async function twoTenants(db: Db) { const [acme, globex] = await Promise.all([ createTenant(db, { slug: 'acme' }), createTenant(db, { slug: 'globex' }), ]) // Globex is the control. Every assertion about Acme's data is also an // assertion that Globex's rows did not appear. await seedRealisticData(db, globex) return { acme, globex } }

This single convention is what makes the isolation guarantee continuous rather than a one-off test. Every feature test written against twoTenants is incidentally an isolation test for the feature it covers.

Rule 3: the clock is an argument

Proration, billing periods, trial expiry and seat changes mid-cycle are all time arithmetic. A test that calls the system clock is a test that fails on the 31st of a month, or during a daylight-saving change, and then gets marked flaky and skipped.

// Time enters the system through this and nowhere else. export interface Clock { now(): Date } export const systemClock: Clock = { now: () => new Date() } export const fixedClock = (iso: string): Clock => ({ now: () => new Date(iso) }) // Which makes the hard billing cases ordinary tests. const clock = fixedClock('2026-01-31T12:00:00Z') // month-end const clock = fixedClock('2026-03-08T06:30:00Z') // a DST boundary const clock = fixedClock('2028-02-29T00:00:00Z') // a leap day

Chapter 84 leans on this entirely. Without an injectable clock, the interesting proration cases are untestable and the reported numbers are whatever happened to be true on the day the test was written.

Rule 4: payments run against test mode, not a mock

The payment processor's test mode is a real API with real state machines. Mocking it means mocking the thing most likely to surprise you — the verification states, the ordering of webhook events, the exact shape of a prorated invoice.

What a mock cannot tell youWhat test mode does
That a restricted account rejects a chargeReturns the real error
That webhooks arrive out of orderThey genuinely do
What a mid-cycle seat change actually invoicesComputes it
That an unverified account cannot be paidEnforces it

The cost is that these tests are slower and need network access, so they run as a separate suite. Chapter 116 replays captured real events against the handler, which is the fast half of the same coverage.

The four tests that run first

In this order, because each one makes the next meaningful:

1. schema-drift       Does the migrated schema match the committed definition?
2. tenant-coverage    Does every tenant-scoped table appear in the isolation list?
3. isolation          Does reading as A ever return B's rows, on any table?
4. rls-enabled        Is RLS actually enabled on every table in that list?

Test 2 is the one that is easy to omit and is the reason the suite stays correct as the schema grows. It derives the table list from the database catalogue and fails when a table carrying a tenant_id column is not registered for isolation testing.

-- What test 2 asks the database, rather than trusting a hand-written list. select c.relname from pg_class c join pg_attribute a on a.attrelid = c.oid where a.attname = 'tenant_id' and c.relkind = 'r' and c.relnamespace = 'public'::regnamespace;

Compare that result against the registered list; any difference fails the build. A new table is then isolation-tested from the commit that creates it, rather than from the commit where somebody remembers.

A guarantee that depends on a person remembering to add a test is not a guarantee. Derive the list from the schema and the suite grows itself.

What the numbers in this series mean

Where a later chapter quotes a figure — a query time, a proration amount, a drift count — it comes from this harness, on a seeded dataset of 50 tenants with a deliberately long-tailed row distribution: one tenant with roughly 400 times the median row count, because Chapter 3 established that is the realistic shape and a uniform fixture hides every performance problem worth knowing about.

Key points

  • Tests run against real Postgres. A mocked data layer passes with every RLS policy deleted.
  • Schema-per-suite is the right isolation for tests and the wrong one for production, for the same reason: bounded count, no repeated migrations.
  • Every fixture creates two tenants by default. The second one's rows exist to be absent, which makes every feature test incidentally an isolation test.
  • Time enters through an injectable clock, so month-end, DST boundaries and leap days are ordinary test cases instead of flaky ones.
  • Payments run against the processor's test mode. A mock cannot reproduce verification states, webhook ordering or a real prorated invoice.
  • Four tests run first: schema drift, tenant coverage, isolation, RLS enabled.
  • Derive the isolation table list from pg_attribute rather than maintaining it by hand, so a new table is covered from the commit that creates it.
  • Performance figures come from a long-tailed fixture — one tenant at ~400× the median — because a uniform one hides the problems.

Next: Section 2 — The tenancy model. Section 1 settled what you are building and what cannot be changed later; Section 2 makes a tenant exist on every request.