Skip to main content

Staging Database Access — Engineer Onboarding

Applies to: Any engineer who needs to run migrations, create tables, backfill data, or otherwise write to the Supabase staging branch. Does NOT apply to: Production access — production migrations are applied by an org Admin (currently @shain, @felipe, @jacob, @pliancy) after a merged PR. Engineers should never write to main directly.

Access policy (the rules this doc implements)

  1. Supabase Dashboard access is reserved for org admins. Owner + Administrators only. Admins are people who need to manage projects, billing, extensions, backups, or team membership — not people who need to write SQL.
  2. Engineers get scoped Postgres credentials, never dashboard access. Staging DB write access via a personal eng_<name> login (this doc). Prod DB access only if required for a specific task, scoped to what they need (see “prod schema reference” below), revoked when the task ends.
  3. The built-in Supabase “Read-Only” dashboard role is not used. Its underlying Postgres role (supabase_read_only_user) has pg_read_all_data + BYPASSRLS, which grants blanket SELECT on every table with no per-table restriction possible. This fails least privilege (SOC 2 CC6.3) for any workspace with sensitive tables like ai_chat_logs.
  4. If an engineer needs prod schema reference (rare, and usually avoidable by looking at migration files in Git), grant a custom read-only Postgres role on prod with USAGE on schema public but no table-level SELECT grants. Revoke when task is complete. This sidesteps the pg_read_all_data issue above.

Why this exists

Supabase’s built-in dashboard roles (Owner / Administrator / Developer / Read-Only) apply at the project level, not the branch level: There is no built-in role that grants write on staging but not on main. Giving an engineer the Developer role to unblock staging work would also give them write access to production, which fails least privilege (SOC 2 CC6.3) and is unnecessary for the work they need to do. Instead, we bypass the dashboard entirely for engineer write access and give each engineer a personal Postgres login on the staging branch database. They connect via their SQL client / migration tool of choice (Supabase CLI, psql, TablePlus, DBeaver, etc.) using a connection string. They cannot reach production because they don’t have credentials for it.

Why we do it this way (architecture)

Two-tier role setup on the staging DB:
Why the split (shared role + personal logins) matters:

One-time setup (already applied — reference only)

The shared staging_engineer role was created on the staging branch DB (evmxqxlurxfhzvwjxqrf) on 2026-09-30. You do NOT need to re-run this — it’s here so future maintainers understand what exists.
Important gotcha about ALTER DEFAULT PRIVILEGES: without an explicit FOR ROLE <role>, these defaults only apply to objects created by the role that ran the ALTER statement — in this case, postgres. When eng_<name> runs a migration that creates a new table, that table is owned by eng_<name>, and the defaults above do NOT fire. Other engineers would see no grants on it. That’s why the onboarding step below also runs ALTER DEFAULT PRIVILEGES FOR ROLE eng_<name> — once per engineer. Note: BYPASSRLS on staging_engineer is documentation-only — Postgres does NOT inherit this attribute through role membership. Every personal login role must also be granted BYPASSRLS directly (see “Add a new engineer” below). We keep it on staging_engineer anyway to make intent clear. Why we intentionally did NOT apply this via apply_migration: role changes are cluster-wide, not schema-scoped. Tracking this as a migration would mean it gets applied to main on branch merge, which we do not want (production has stricter access rules). Role setup is manual, one-off SQL and lives here in this doc as the source of truth.

How to add a new engineer

Prereqs:
  • The engineer needs a place to receive credentials (1Password shared vault).
  • You need postgres admin access to the staging DB (Shain / Owner + Admins). Supabase-hosted projects do not expose a true PostgreSQL superuser; the postgres role is the admin role we use.

Step 1 — Generate a strong password

Store this in 1Password under the vault entry Silvia Staging DB — Engineer Logins, item eng_<firstname>. Do not paste it into Slack, GitHub, or this doc.

Step 2 — Create their login role on staging

Connect to the staging DB via local psql as postgres. Using psql (not the Supabase SQL editor) is required because the SQL editor cannot run psql’s \password meta-command — and we need \password to avoid logging the cleartext password (see “Why \password” below). Get the staging postgres connection details from Supabase Dashboard → switch to staging branch → Project Settings → Database. Omit the password from the URI and pass -W so psql prompts for it interactively — this keeps the admin password out of your shell history (~/.zsh_history, ~/.bash_history) and out of process-list output (ps auxww on multi-user systems).
Then, inside the psql session, run:
Then set the password with \password (NOT with ALTER ROLE ... PASSWORD):
Why \password: staging has log_statement = 'ddl', which means PostgreSQL writes the full text of CREATE ROLE ... PASSWORD '...' and ALTER ROLE ... PASSWORD '...' statements to the server log — password and all. \password is a psql client-side meta-command that computes the SCRAM-SHA-256 hash locally and only sends the hash to the server, so the cleartext never appears in logs. Alternative for shared ownership of objects (migrations): if engineers want objects they create to be owned by the shared role (simplifies offboarding — see below), they can start their migration / session with SET ROLE staging_engineer;. Role membership (granted by IN ROLE staging_engineer above) is sufficient to SET ROLE to it — no extra grant needed. The ALTER DEFAULT PRIVILEGES block above is a belt-and-suspenders backstop for when engineers forget to SET ROLE.

Step 3 — Verify

Expect: rolcanlogin: true, rolbypassrls: true, memberships: staging_engineer.

Step 4 — Hand off credentials

Send the engineer a message with:
  • 1Password link to the vault entry
  • The connection string template (below, with their username filled in)
  • A link to this doc
  • The “Rules of the road” section (below)

How the engineer uses it

Connection string template

Store in your local .env.staging.local (name ends in .local on purpose — the repo’s .gitignore has an .env*.local glob that keeps it out of Git):
Never commit this to Git. Verify locally with git check-ignore .env.staging.local before your first commit on a new machine. Never share your personal password with another engineer — if they need access, they get their own login.

Running migrations

Supabase CLI (preferred for anything tracked in Silvia-Web/supabase/migrations/):
Raw psql (for one-off SQL not committed as a migration):
Any Node / Python migration tool (Prisma, dbmate, Alembic, etc.) that accepts a connection URL — swap in $STAGING_DB_URL.

Interactive querying

Any Postgres-compatible SQL client works — TablePlus, DBeaver, Postico, psql. Connect using the values from the connection string above.

What you can do

  • ✅ Full CRUD on any table in public
  • ✅ CREATE TABLE, ALTER TABLE, DROP TABLE, indexes, views, functions
  • ✅ Backfill / cleanup data as needed for your feature
  • ✅ Read every table on staging, including RLS-protected ones — your role has BYPASSRLS. This is intentional for staging (see FAQ). Do not rely on this to test RLS behavior — see the FAQ “Why does the engineer login have BYPASSRLS?” below for the three supported ways to actually test RLS policies.

What you cannot do

  • ❌ Anything on the production DB (byoneafwuigeyehkoacw / main branch)
  • ❌ Modify roles, extensions, or DB-level settings (those are admin-only operations reserved for the postgres role)
  • ❌ Access the auth or storage internal schemas’ management tables
  • ❌ Use the Supabase Studio UI for staging (you’re a DB user, not a dashboard user)

Rules of the road

  1. Never use the postgres admin password from the Supabase dashboard for day-to-day staging work. Use your eng_<name> login. The postgres password is reserved for admins doing role management, extension installs, etc.
  2. Every migration that should also apply to production goes through a PR. Commit the SQL to Silvia-Web/supabase/migrations/. On PR merge to main, an admin applies it to prod.
  3. Do not share your password with another engineer. They get their own login. If someone needs write access, ping @shain in Slack.
  4. Suspicious activity, lost laptop, quit the company? Ping @shain immediately — we’ll rotate or drop your role.

How to remove an engineer (offboarding)

When an engineer leaves or moves off engineering work, run this on the staging DB as postgres (via psql, or the Supabase SQL editor — these commands don’t need \password):
Other engineers are unaffected. Then delete the 1Password entry for their login and strike through their row in the current-roster table below (keep the row so the audit trail remains). If they had any prod dashboard access (via <email>@cfosilvia.com), remove it from Supabase Dashboard → Team → Manage access as well.

Password rotation

Recommended annually or on any suspicion of compromise. Generate a new password (see Step 1 above), then rotate it the same way you set it originally — via psql’s \password, NOT ALTER ROLE ... PASSWORD (staging’s log_statement = 'ddl' would write the new cleartext password to the server log):
Update the 1Password entry and notify the engineer to update their local .env.staging.local. Their old sessions will remain connected until they disconnect, so if this is a compromise response, also run:

Current roster

Update this table on every add / remove / suspension.

SOC 2 posture

This pattern satisfies the following Trust Services Criteria for the staging database access surface: Related gaps still to close (tracked separately):
  • Enable pgaudit with object-audit on ai_chat_logs and other sensitive tables (production).
  • Enable Log Drains → S3 for 12-month Postgres log retention.
  • Quarterly access review cadence with screenshot evidence.

FAQ

Why not just give engineers the Developer dashboard role?

Because Developer grants write on main too (see the table at the top). Giving Developer to unblock staging work would give an engineer the ability to accidentally DROP TABLE users on production. Least privilege says: give them exactly what they need, nothing more.

Why not give them a shared engineer login instead of per-person?

Two reasons:
  1. Auditability: If a bad migration runs, “some engineer did it” is not an acceptable answer. Postgres logs need to attribute to a named person.
  2. Offboarding: A shared password means every departure requires rotating the shared password and re-distributing to everyone still employed. Per-person logins mean one DROP ROLE and done.

What if they need to use Supabase Studio UI for staging?

They can’t with this setup — Studio requires org membership. If someone genuinely needs Studio (e.g. to use the visual Table Editor rather than a SQL client), the options are:
  1. Give them Developer role and accept the prod-write risk (bad).
  2. Wait for Supabase to ship per-branch dashboard permissions (not on their roadmap as of 2026-09).
  3. Use a third-party SQL GUI that talks to the same connection string (TablePlus / DBeaver — arguably nicer than Supabase’s SQL editor).
Option 3 is what we do.

Can engineers see production data through the staging branch?

No. Supabase branches are separate physical Postgres databases (different project_ref, different hosts, different data). The staging branch is created from prod’s schema but its own data. Engineers with eng_<name> credentials cannot reach production even by manipulating the connection string.

Why does the engineer login have BYPASSRLS? Isn’t that dangerous?

On staging, yes intentionally. Every table on staging has RLS enabled (inherited from the prod schema clone) with policies scoped to authenticated role checking clerk_id. Without BYPASSRLS, an engineer connecting as eng_<name> would see zero rows on virtually every table — they’d be unable to inspect data, verify a migration worked, or debug a query. The SOC 2 tradeoff:
  • Risk: staging PII (if any) is visible to engineers with a login.
  • Mitigation 1: Never copy raw prod PII into staging. Use synthetic seed data or scrubbed snapshots.
  • Mitigation 2: Engineers have per-user logins, so any access to data on staging is attributed to a named person in Postgres logs.
  • Mitigation 3: BYPASSRLS is granted ONLY on staging. Production engineers never have BYPASSRLS on the prod DB (they don’t have DB credentials at all).
If you need to test RLS behavior itself (i.e. “does my policy correctly block user B from seeing user A’s rows?”), do NOT use your engineer login — your BYPASSRLS attribute defeats the test, AND your login is not a member of the authenticated role, so a bare SET ROLE authenticated would also fail with “permission denied to set role”. Test RLS using one of these methods instead: Option 1 — Local Supabase stack (recommended; fully reproducible, exercises the real JWT → PostgREST → policy path):
Setup: https://supabase.com/docs/guides/local-development Option 2 — Staging PostgREST with a crafted JWT: hit the staging REST endpoint with Authorization: Bearer <jwt>. Craft the JWT with sub set to a clerk_id that exists in staging. Same end-to-end path as production — the realest test short of prod itself. Option 3 — Dedicated test role in psql (for ad-hoc policy probing): Create a one-time test login role (as postgres, via psql + \password per Step 2 above) that is a member of authenticated and does NOT have BYPASSRLS:
Connect as eng_rls_test and run:
Because eng_rls_test is a member of authenticated and lacks BYPASSRLS, Postgres evaluates your RLS policies as a real authenticated user would.

An engineer needs to test something that requires production data.

Options in order of preference:
  1. Seed staging with a synthetic subset that mirrors the shape of the prod data (best practice; scriptable).
  2. Restore a scrubbed prod snapshot to staging (ask an admin; PII must be redacted first).
  3. Grant a scoped, per-table read-only Postgres role on prod, with SELECT only on the specific tables they need and NO blanket read. Admin creates a dedicated role (e.g. reader_<task>_<engineer>), grants USAGE on public + SELECT only on the needed tables, hands over credentials via 1Password, and DROP ROLEs it when the task ends. Do NOT grant the built-in Supabase “Read-Only” dashboard role — its underlying supabase_read_only_user has pg_read_all_data + BYPASSRLS (see access policy #3 at the top) and cannot be restricted per-table, so it would expose sensitive tables like ai_chat_logs.
Never copy raw prod data into staging without scrubbing PII first (SOC 2 CC6.6).

Can I use the pooler URL instead of the direct connection?

For migrations (DDL, transactions, pg_dump): use the direct connection at port 5432. The Supabase transaction pooler at port 6543 does not preserve session state across statements and can break migration tools. For app-style connections (short-lived queries, many concurrent connections): the pooler is fine. Get the pooler URL from the Supabase Dashboard → Settings → Database → Connection Pooling.