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 tomain directly.
Access policy (the rules this doc implements)
- 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.
- 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. - The built-in Supabase “Read-Only” dashboard role is not used.
Its underlying Postgres role (
supabase_read_only_user) haspg_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 likeai_chat_logs. - 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
publicbut no table-level SELECT grants. Revoke when task is complete. This sidesteps thepg_read_all_dataissue 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:One-time setup (already applied — reference only)
The sharedstaging_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.
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
postgresadmin access to the staging DB (Shain / Owner + Admins). Supabase-hosted projects do not expose a true PostgreSQL superuser; thepostgresrole is the admin role we use.
Step 1 — Generate a strong password
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 localpsql 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).
\password (NOT with ALTER ROLE ... PASSWORD):
\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
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
.env.staging.local (name ends in .local on
purpose — the repo’s .gitignore has an .env*.local glob that keeps
it out of Git):
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 inSilvia-Web/supabase/migrations/):
$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/mainbranch) - ❌ Modify roles, extensions, or DB-level settings (those are admin-only
operations reserved for the
postgresrole) - ❌ Access the
authorstorageinternal schemas’ management tables - ❌ Use the Supabase Studio UI for staging (you’re a DB user, not a dashboard user)
Rules of the road
- Never use the
postgresadmin password from the Supabase dashboard for day-to-day staging work. Use youreng_<name>login. Thepostgrespassword is reserved for admins doing role management, extension installs, etc. - Every migration that should also apply to production goes through
a PR. Commit the SQL to
Silvia-Web/supabase/migrations/. On PR merge tomain, an admin applies it to prod. - Do not share your password with another engineer. They get their own login. If someone needs write access, ping @shain in Slack.
- 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 aspostgres (via psql, or the Supabase SQL editor — these
commands don’t need \password):
<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):
.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_logsand 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 onmain 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:
- Auditability: If a bad migration runs, “some engineer did it” is not an acceptable answer. Postgres logs need to attribute to a named person.
- 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 ROLEand 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:- Give them Developer role and accept the prod-write risk (bad).
- Wait for Supabase to ship per-branch dashboard permissions (not on their roadmap as of 2026-09).
- Use a third-party SQL GUI that talks to the same connection string (TablePlus / DBeaver — arguably nicer than Supabase’s SQL editor).
Can engineers see production data through the staging branch?
No. Supabase branches are separate physical Postgres databases (differentproject_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 toauthenticated 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).
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):
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:
eng_rls_test and run:
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:- Seed staging with a synthetic subset that mirrors the shape of the prod data (best practice; scriptable).
- Restore a scrubbed prod snapshot to staging (ask an admin; PII must be redacted first).
- Grant a scoped, per-table read-only Postgres role on prod, with
SELECTonly on the specific tables they need and NO blanket read. Admin creates a dedicated role (e.g.reader_<task>_<engineer>), grantsUSAGEonpublic+SELECTonly on the needed tables, hands over credentials via 1Password, andDROP ROLEs it when the task ends. Do NOT grant the built-in Supabase “Read-Only” dashboard role — its underlyingsupabase_read_only_userhaspg_read_all_data+BYPASSRLS(see access policy #3 at the top) and cannot be restricted per-table, so it would expose sensitive tables likeai_chat_logs.
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.
Related docs
- DATABASE-VIEW-RLS-SAFETY.md — RLS rules for views and migrations (required reading for any migration that touches views).
- SECURITY-INCIDENT-RPC-FUNCTIONS.md — RPC function security posture.
- slack-revenue-data-flow.md — data-flow SOC 2 template used as reference for this doc’s structure.