# Database development and testing Read this for schema design, migrations, data changes, seeds, Row Level Security, pgTAP, generated types, or migration-history repair. ## Pick one schema authority ### Declarative schemas Use `supabase/schemas/*.sql` as desired state. Order files deliberately when objects depend on one another. Edit the schema files first, then: ```sh supabase db diff -f descriptive-name supabase db reset ``` Commit schema files and generated migration together. `db diff` does not read ad hoc local database changes when declarative schemas are configured. ### Imperative migrations Create timestamped forward migrations directly: ```sh supabase migration new descriptive-name supabase db reset ``` If using local Studio as a schema editor, capture changes with `supabase db diff -f descriptive-name`, inspect the result, then reset from migrations to prove reproducibility. Do not maintain both an imported baseline migration and a duplicate declarative representation without an explicit transition plan. Two sources of truth drift. ## Migration review Generated diffs are drafts. Review for: - Destructive `DROP`, truncation, type conversion, and constraint operations. - Redundant or over-broad grants and revoke/re-grant churn. - Extensions added or removed because local and remote defaults differ. - View/function ownership and `security definer` search paths. - RLS policy drops/recreates or renames that the diff engine cannot express safely. - DML omitted entirely: inserts, updates, deletes, backfills, and data transformations must be authored deliberately. - Lock duration, table rewrites, index creation, and compatibility with rolling application deployments. Prefer roll-forward corrections after production deployment. Down migrations are often destructive and should not be generated or run mechanically. ## Seeds Seeds run after migrations on first local start and on `db reset`. Keep schema changes in migrations and data inserts in seed files. Default: `supabase/seed.sql`. Multiple ordered files can be configured in `config.toml`: ```toml [db.seed] enabled = true sql_paths = ['./seeds/00-reference.sql', './seeds/10-scenarios.sql'] ``` Glob matches are lexically sorted. Hand-written deterministic seeds are the default. Never commit production PII, real tokens, password hashes, customer objects, or other sensitive dumps as development fixtures. ## RLS design For every table in an exposed schema: ```sql alter table public.todos enable row level security; ``` Then grant only intended operations to API roles and define policies for each operation. Key distinctions: - `USING` controls which existing rows are visible/targetable. - `WITH CHECK` controls which new or changed rows are allowed. - `auth.uid()` can be `null` for anonymous requests; write policies with explicit identity assumptions. - A `service_role` request bypasses RLS and cannot validate policy behavior. - Views, functions, storage metadata, and custom JWT claims introduce separate privilege/ownership paths. Use private schemas for data that should never be reachable through the Data API. Qualify object names and secure `search_path` in privileged functions. ## Database tests with pgTAP Create tests with the CLI and run them transactionally: ```sh supabase test new todos_rls.test supabase test db ``` RLS suites should cover: 1. Anonymous access allowed and denied. 2. Authenticated user can read/write their own rows. 3. A different authenticated user cannot read/write those rows. 4. Insert/update `WITH CHECK` prevents changing ownership or restricted fields. 5. Deletes and RPC/functions follow the intended policy path. 6. Privileged functions do not expose a bypass through ownership or mutable `search_path`. Use `begin`/`rollback`, set the local role, and set request JWT claims deliberately. Require negative assertions, not only happy paths. ## Application-level tests Test the client/gateway path in addition to pgTAP. Use unique users and object names so tests can run independently; clean up what they create. Test the publishable key plus user sessions. Keep the secret key in server-only setup/cleanup code and do not use it for behavior under test. An update blocked by RLS may affect zero rows without returning a dramatic error. Query again as the owning user to prove no unauthorized change occurred. ## Generated types Generate from the same target whose schema the application will use: ```sh supabase gen types --lang typescript --local > database.types.ts supabase gen types --lang typescript --linked > database.types.ts supabase gen types typescript --db-url "$DATABASE_URL" --schema public > database.types.ts ``` Do not put password-bearing URLs in shell history or CI logs. Regenerate after schema changes and fail CI when committed types drift from the replayed schema. ## Remote rollout Before push: ```sh supabase migration list supabase db push --dry-run ``` Review order and target, take a recovery-capable backup for consequential changes, apply with `db push`, then verify migration history, schema, representative reads/writes, and application compatibility. If remote schema changed outside migrations, use `db pull` to capture drift and replay locally. If only migration history is wrong, inspect `supabase migration list` and current CLI guidance before `migration repair`; history repair changes bookkeeping and must not be used to hide unapplied SQL.