# 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`, and `55432` were 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: - `citext` - `ltree` - `pg_trgm` - `pgcrypto` 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_admin` could query `auth.users`. - `supabase_storage_admin` could query `storage.objects`. - `authenticator` could still `SET ROLE anon` and `SET 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: ```text Linting schema: public Linting schema: app Linting schema: app_private No schema errors found ``` The command used was: ```bash 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.