The database has its own test suite, written in SQL with pgTAP. It lives in apps/web/supabase/tests/database/: 23 test files plus 00000-test-helpers.sql, which installs the shared helpers. Each test file runs inside a transaction that is rolled back.
With the local stack running (pnpm supabase:web:start):
pnpm supabase:web:test
The tests sign in as the users from seed.sql. If they stop early with missing users, your local database was seeded differently: pnpm supabase:web:reset rebuilds it (and wipes local data).
| Area | Files |
|---|---|
| Every table has RLS on | schema-conditions.test.sql |
Default TRUNCATE, REFERENCES and TRIGGER privileges are gone | privileges.test.sql |
| Accounts and teams | personal-accounts, team-accounts, account-slug, account-permissions, transfer-ownership |
| Memberships | memberships, update-membership, delete-membership |
| Invitations | invitations |
| Billing | personal-billing-subscriptions, personal-billing-orders, team-billing-subscriptions, team-billing-orders |
| Notifications, files, one-time codes | notifications, storage, otp |
| Super admin | super-admin, super-admin-edge-cases |
| Schema and triggers | schema, triggers-timestamps-simple, triggers-user-tracking-accounts |
13 of the 23 files sign in as one user and try to read or change another account's data, and assert that it fails: account-permissions, memberships, personal-accounts, notifications, delete-membership, invitations, storage, super-admin, team-accounts and the four billing files. The other files test behaviour: triggers, slugs, one-time codes and similar.
From 00000-test-helpers.sql (built on the Basejump Supabase test helpers):
| Helper | Does |
|---|---|
tests.create_supabase_user(id, email) | Creates a throwaway user |
test_helpers.set_identifier(id, email) | Gives a seeded user a short name |
test_helpers.authenticate_as(id) | Runs the next statements as that user |
test_helpers.get_account_id_by_slug(slug) | Looks up an account id |
test_helpers.set_mfa_factor(id), test_helpers.set_session_aal(level), test_helpers.set_super_admin() | Set up two-step sign-in and super-admin cases |
Put a new file next to the others. A cross-account check for a projects table:
begin;
select no_plan();
select tests.create_supabase_user('stranger', 'stranger@example.com');
-- someone outside the team must not see its projects
select test_helpers.authenticate_as('stranger');
select is_empty(
$$ select * from public.projects
where account_id = test_helpers.get_account_id_by_slug('acme') $$,
'A stranger cannot read another team''s projects'
);
select * from finish();
rollback;
Test writes as well as reads: an insert, an update that changes account_id, and a delete, each as a user who should be refused. The /rls-review agent skill writes tests like these for you and is required by AGENTS.md after any database change.
Playwright tests in apps/e2e/tests/ drive the real web app: sign-in and sign-up, password reset, account settings, team accounts and invitations, two-step sign-in for invited members, billing, the admin panel and the live demo. Run them against a running app with pnpm --filter web-e2e test. Billing specs need ENABLE_BILLING_TESTS=true and Stripe set up; the demo spec needs E2E_DEMO_MODE=true.
.github/workflows/workflow.yml has a job that starts Supabase, builds and starts the app, then runs the Playwright tests and pnpm --filter web supabase:test. It runs only when the repository variable ENABLE_E2E_JOB is true.
The live demo has its own tests, outside this suite: see Live demo.