6.8 KiB
Clean-room migration audit - 2026-07-12
Audit scope
- Isolated project ID:
tiku-clean-final-20260712-v2 - Database image: official Supabase PostgreSQL
15.8 - Isolated database endpoint used during verification:
127.0.0.1:55522 - Migration role: standard non-superuser
postgres - Privileged bootstrap/test role:
supabase_admin, used for the runtime-role bootstrap and controlled role-boundary probes - The existing databases on ports
5432,54322, and55432were explicitly excluded and were not modified.
The clean-room startup log reported that no supabase/seed.sql matched. The seed was therefore not executed. At the end of migration verification, public.tenants, app_private.environment_safety, and auth.users all contained zero rows.
Migration result
All 78 migrations were applied successfully in filename order to the empty database. supabase migration list --local subsequently showed every local version paired with the applied version through 202607120019.
Migration history verification returned:
- Rows:
78 - Distinct versions:
78 - Duplicate versions:
0 - Maximum version:
202607120019
The privileged scripts/deploy/sql/bootstrap-backend-runtime-roles.sql bootstrap ran before the normal migrations. Migrations 202607120013_backend_runtime_roles.sql, 202607120018_auth_user_reference_boundary.sql, and 202607120019_production_migration_history_boundary.sql were later replayed directly by the non-superuser postgres role. All three replays succeeded. A normalized fingerprint covering role attributes, memberships, schema/table/sequence/function ACLs, default ACLs, and security-definer attributes remained 1691|7aee08a0b05a30fc49fd278748d4c4e3 before and after replay.
Extensions and function ACLs
The required extensions were installed in the extensions schema:
citextltreepg_trgmpgcrypto
No client-facing role can execute a function exposed through public. The final extension-function matrix was:
| Role | citext |
ltree |
pg_trgm |
pgcrypto |
|---|---|---|---|---|
anon |
0/45 | 0/78 | 0/31 | 0/36 |
authenticated |
0/45 | 0/78 | 0/31 | 0/36 |
tiku_api |
45/45 | 78/78 | 31/31 | 0/36 |
tiku_worker |
45/45 | 78/78 | 31/31 | 0/36 |
citext also owns two aggregates; when all pg_proc extension members are counted, API and worker have all 47 required citext members while client roles still have zero.
Runtime roles and Auth boundary
Both backend roles were verified as:
LOGIN NOINHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION BYPASSRLS- No parent-role memberships
search_path=pg_catalog, public, extensions
Runtime identity checks confirmed that both roles can execute citext comparisons, ltree operators, and extensions.similarity().
The API role could execute exactly five reviewed app functions:
app.auth_user_exists(uuid)app.production_migration_history(text)app.uuid_array_from_jsonb(jsonb)app.public_question_bank_grant_allows(uuid[], uuid[], uuid, uuid[])app.public_question_bank_subscription_allows(jsonb, jsonb, uuid, uuid, uuid[])
Direct tiku_api access to supabase_migrations.schema_migrations was denied. The API-only app.production_migration_history('202607120019') boundary returned latest_version=202607120019, applied_count=78, distinct_version_count=78, and expected_version_applied=true. Worker and anonymous execution were denied.
The worker has no app schema usage and can execute no app functions. Both API-only boundary functions are stable SECURITY DEFINER functions with an empty search path. app.auth_user_exists(uuid) returned false for an absent UUID and true for a temporary clean-room Auth user; that user was deleted immediately after the probe. Worker and anonymous execution were denied. Direct reads of auth.users were denied for both API and worker.
Supabase internal compatibility checks also passed:
supabase_auth_admincould queryauth.users.supabase_storage_admincould querystorage.objects.authenticatorcould stillSET ROLE anonandSET ROLE authenticated.
RLS, constraints, and indexes
All 140 public tables had RLS enabled. Across public and app_private, all 134 tables containing a tenant_id column had RLS enabled.
The only checked internal table without RLS was app_private.environment_safety. It is not tenant data, is intentionally fail-closed, and explicitly revokes access from public, anon, and authenticated.
The project tenant-foreign-key audit matched all 189/189 expected relations and its expected SHA-256 fingerprint, found all three reviewed exceptions, found no unvalidated relations, and reported 0 data violations.
All 22 explicitly checked critical foreign keys, unique constraints, and check constraints introduced by migrations 202607120010 through 202607120017 existed with convalidated=true. All 16 explicitly checked critical indexes existed with indisvalid=true and indisready=true, including tenant-safe question/version relations, answer semantics, tenant student keyset/search indexes, SMS reservation limits, audit-log capacity indexes, and import-job lease indexes.
Supabase lint completed successfully:
Linting schema: public
Linting schema: app
Linting schema: app_private
No schema errors found
The command used was:
npx --no-install supabase db lint \
--local \
--workdir /tmp/tiku-clean-final-20260712-v2 \
--schema public,app,app_private \
--level error \
--fail-on error
Residual foreign-key index risk
The application schemas (public and app_private) contain 465 foreign keys. The structural audit found:
- 146 with an unconditional complete left-prefix index
- 6 covered only by a partial left-prefix index
- 313 without a complete left-prefix index
The newly hardened 202607120015 and 202607120017 hot paths are substantially covered. The remaining count is a capacity and operations backlog, not a migration correctness or tenant-isolation failure.
Adding 313 indexes blindly is not recommended. Every index increases storage, write amplification, vacuum work, cache pressure, migration time, and lock risk. Some foreign keys are low-volume, rarely joined, never cascaded in normal operations, or already served by a more useful query-specific index. Production indexing should therefore be prioritized from representative capacity tests, cascade/delete behavior, slow-query evidence, and pg_stat_statements, then introduced in controlled batches.
Cleanup
After all checks passed, the isolated Supabase project was stopped with --no-backup. The following were verified absent:
/tmp/tiku-clean-final-20260712-v2- Clean-room containers
- Clean-room Docker volumes
- Clean-room Docker networks
The pre-existing databases on ports 5432, 54322, and 55432 remained running after cleanup.