Skip to content
Skip to main content
Novel Systems home
Decision log
D-078August 5, 2026

The tenant-isolation job never ran, and the reason was a signing key it does not use

Decided

the CI job "API — tenant isolation against Postgres" now sets JWT_SECRET inline in its env: block. Until this change it did not, and the consequence was not a flaky job or a slow one — it was that the job had never executed a single assertion since the day it was added.

Affects: .github/workflows/ci.yml, and by implication every claim this repository makes about tenant isolation.

What actually happened. test/tenant-isolation.test.ts imports withTenant and prisma. src/db/prisma.ts imports src/config/env.ts, which validates the entire environment at module scope and calls process.exit(1) on the first missing value. JWT_SECRET is required there with a 32-character minimum and no default. The job supplied RLS_ADMIN_URL and OWNER_URL — everything the test touches — and no signing key, because the test authenticates nobody and signs nothing. So the process died at import, before the fixture, before the first assert, and the runner reported not ok 1 - test/tenant-isolation.test.ts with # JWT_SECRET: Required in the diagnostic block above it.

Why that is worse than an ordinary red job. Every other check in this pipeline fails loudly in a way that blocks the thing it guards. This one failed in a way that looked identical to a genuine isolation failure while proving nothing about isolation either way, next to five green jobs, on a workflow whose overall state has been "5/6 passed" on every recent commit. The claim it exists to back — that a tenant cannot read, update, delete or write across the boundary, and that the database refuses the write even when the application layer is bypassed — has been sitting on the site and in docs/ with a CI job pointed at it that had never once run. That is exactly the failure mode the test's own header warns about: a file that looks like coverage and provides none.

Alternative rejected: put it in a repository secret. Same reason RLS_ADMIN_URL is inline. A value in a secret carries an implicit claim that it protects something; this one protects nothing, is used by no assertion, and exists solely to satisfy a module graph. Hiding it would make the next reader assume the isolation tests depend on a real signing key, which is the opposite of true.

Alternative rejected: break the import chain instead. The honest fix in the abstract is that a test of database policies should not transitively import a config validator. But prisma.ts is the module that installs the tenant-scoped client, and the test is meant to exercise the *application's* path to the database rather than a hand-rolled one — that is the entire point of the file. Swapping in a bare PrismaClient to dodge the import would make the test pass while testing something the application never does.

This decision is wrong if the env validator ever stops being import-time and fail-fast. It should not: a process that starts with a missing secret and discovers it at the first request is a worse outcome than one that refuses to start. The cost of that design is this class of surprise in test harnesses, and the price is a comment in the workflow saying why the variable is there.