In Supabase the public key is meant to be public, so the only thing standing between an anonymous visitor and your tables is Row Level Security. Teams that treat the anon or publishable key as a secret, or that forget RLS on a single table, end up with a database that anyone can read and write through the auto-generated API. This guide explains the publishable and secret key model, the legacy anon and service_role JWTs being retired, how keys map to Postgres roles, and how to write, test and monitor RLS. It also covers the other credentials in a Supabase project: the postgres password, JWT signing keys and Management API tokens. It is for developers, platform engineers and NHI owners running Supabase in production.
Key takeaways
- Publishable keys are not secrets. A
sb_publishable_key (or legacyanonJWT) ships in every browser and maps to theanonorauthenticatedPostgres role; RLS is what protects data. - Secret keys bypass every policy. A
sb_secret_key (or legacyservice_roleJWT) runs asservice_role, which has theBYPASSRLSattribute, so it belongs only in backend code. - Move off the legacy JWT keys. Supabase is deprecating the long-lived
anonandservice_roleJWTs by the end of 2026, and they stay valid until you deactivate them. - RLS has two layers. Postgres checks table grants before policies, and Supabase projects can grant every privilege on new
publictables toanonandauthenticatedby default. - Test as an attacker would. Run the Security Advisor and pgTAP tests for each role and operation before release, and use one secret key per backend component so one leak means one rotation.
How identity and access work in Supabase
A Supabase project is a Postgres database fronted by APIs: PostgREST for tables, Auth, Storage, Realtime and Edge Functions. Each API request carries an API key in the apikey header and, if a user is signed in, that user's access token. The API gateway uses them to pick a Postgres role, and Postgres enforces grants and RLS policies for that role. The key therefore decides the starting role, not what data the request sees.
| Key | Format | Postgres role | RLS | Where it may live |
|---|---|---|---|---|
| Publishable key | sb_publishable_... | anon, or authenticated with a user session | Enforced | Browsers, mobile apps, CLIs, source code |
| Secret key | sb_secret_... | service_role | Bypassed when no user token is present | Backend services, workers, Edge Functions via @supabase/server |
Legacy anon key | Long-lived JWT (eyJ...) signed with the legacy JWT secret | anon | Enforced | Being deprecated; replace with publishable |
Legacy service_role key | Long-lived JWT signed with the legacy JWT secret | service_role | Bypassed | Being deprecated; replace with secret keys |
| User access token | JWT issued by Supabase Auth | authenticated | Enforced, auth.uid() set | Client session |
Supabase documents several protections for secret keys: they are rejected with HTTP 401 when the request User-Agent looks like a browser, you can create several secret keys so each backend component has its own, and they are sent in the apikey header rather than Authorization: Bearer because they are not JWTs. The browser check is a guard rail, not a boundary: a secret key copied into a script still works. See the API Key Management Guide.
The other Postgres roles
postgres: the admin role whose password you set at project creation; dashboard SQL runs as this role and is tagged-- source: dashboardin logs.authenticator: validates the JWT and switches to the role it names.- Service roles:
supabase_auth_admin,supabase_storage_adminandsupabase_adminare platform roles scoped to their schemas or tasks; do not reuse them. - Custom roles: Supabase recommends a separate login role per service connecting directly to Postgres instead of sharing
postgres. A custom role givenBYPASSRLSis as sensitive as a secret key.
Row Level Security done properly
Once RLS is enabled on a table, no rows are returned to publishable key requests until you add policies, so the safe default is closed. Missing RLS is a broken object level authorisation flaw by another name. See the OWASP API Top 10 API1 Broken Object Level Authorization Guide.
- Enable RLS in migrations:
alter table public.reports enable row level security;in a versioned migration, so every environment matches. - Fix grants first: revoke default privileges from
anon,authenticatedandservice_roleon tables in exposed schemas and grant back only what each role needs. A missing grant fails with error42501before any policy runs, which helps debugging. - Write one policy per operation: separate
select,insert,updateanddeletepolicies, scoped withTO authenticatedorTO anon.usingfilters rows that can be seen or touched;with checkvalidates new or changed rows. - Check identity explicitly:
auth.uid()returns null for unauthenticated requests, so addauth.uid() is not null, and wrap it as(select auth.uid())so Postgres evaluates it once per statement. - Take authorisation claims from app metadata: read roles and tenant IDs from
raw_app_meta_dataviaauth.jwt(); users can editraw_user_meta_data. Claims stay stale until the token refreshes. - Close the bypass routes: views bypass RLS by default, so create them
with (security_invoker = true)on Postgres 15 and later.security definerfunctions run with their creator's rights: setsearch_path = '', schema-qualify names and keep them out of exposed schemas. - Index policy columns: a policy acts like a
WHEREclause on every query, so index the columns it filters on. - Test allow and deny cases: write pgTAP tests in
supabase/tests/and runsupabase test dbfor every role and operation in CI.
Common policy patterns
| Need | Policy approach | Pitfall |
|---|---|---|
| Users see only their own rows | using ((select auth.uid()) = user_id) for select, update, delete; with check on insert | Forgetting with check lets users insert rows owned by others |
| Multi-tenant SaaS | Compare a tenant column to a claim from raw_app_meta_data or a membership table | Trusting user-editable metadata |
| Public read, owner write | select policy TO anon, authenticated using (true); write policies tied to auth.uid() | A single for all using (true) policy opens writes too |
| Admin back office | Server-side with a dedicated secret key, or an authenticated policy keyed to an app metadata role | Shipping the secret key in an admin SPA |
| Anonymous sign-in users | Treat them as authenticated with an is_anonymous claim check where needed | Confusing the anon role with anonymous users |
Admin roles and privileged access
Dashboard access is a credential plane of its own: anyone who can open the project can read the secret keys and change the database password.
| Organisation role | Access | Treat as |
|---|---|---|
| Owner | Full access to the organisation and every project | Tier 0; at least two named owners, MFA |
| Administrator | Full access except organisation settings, transferring projects out and adding owners | Privileged |
| Developer | Read-only on organisation resources, content access to projects, no project settings | Data access; can usually see data and keys used in development |
| Read-Only | Read-only on organisation and project resources | Standard, review quarterly |
| No-access plus project-scoped role | Access only to assigned projects (Team and Enterprise plans) | Use to separate production from development |
- Enforce MFA: the Supabase production checklist asks for MFA on every account (and 2FA on GitHub where GitHub sign-in is used), suggests organisation-wide MFA enforcement, and more than one owner.
- Use SSO where available: invites from a SAML SSO account can only be accepted by an account from the same identity provider, which stops invitations to unmanaged accounts.
- Split production: give developers project-scoped roles on development projects and keep production to a small group.
Integrations, API tokens and service accounts
Beyond the project keys, three more credential types reach a Supabase project. See the Secrets Management Guide.
| Credential | Grants | Lifetime and controls | Practice |
|---|---|---|---|
| Management API personal access token, scoped | Selected organisations, projects and permissions | Custom expiry; shares a 120 requests per minute limit per user | Use for CI and automation; one per pipeline |
| Classic personal access token (shown as Legacy) | Same privileges as the user account | Custom expiry | Replace with scoped tokens |
| OAuth2 token for a Supabase integration | Scopes the user approved | Short-lived | Review connected apps; revoke unused |
postgres or custom role password | Direct Postgres or pooler connection | Static; changing it causes no downtime for Supabase services | Vault it; per-service roles; rotate after staff changes |
| JWT signing key | Signs user access tokens (and, for the legacy secret, the legacy keys) | Asymmetric RS256 or ES256 recommended; HS256 not recommended for production | Rotate through standby, current, previously used, revoked |
Migrating from legacy keys and the JWT secret
- Create new keys: generate a publishable key and one secret key per backend component in Settings > API Keys.
- Replace in code: swap the legacy
anonkey for the publishable key in clients and theservice_rolekey for the right secret key in each service. In Edge Functions use the@supabase/serverSDK rather than reading keys from environment variables. - Move to asymmetric signing keys: add an ES256 (P-256) or RS256 key as standby, then rotate it to current. Verifiers can fetch public keys from
/auth/v1/.well-known/jwks.json, which may be cached for up to 20 minutes combined. - Deactivate the legacy keys: watch API gateway logs until no requests use them, then deactivate. Deactivation is reversible, which is your rollback.
- Revoke the legacy JWT secret: only after
anonandservice_roleare disabled, and, with one hour access tokens, at least 1 hour 15 minutes after rotation unless you are mid-incident.
Rotating a leaked secret key
Create a replacement secret key, deploy it to the affected component, confirm the old key no longer appears in logs, then delete the old key. Deletion is permanent, so do it last. Because each component holds its own key, the others keep running. Never log keys: if you must, keep no more than six characters after the prefix or a SHA-256 hash.
Logging and detection
| Source | Records | Notes |
|---|---|---|
| Logs Explorer (API gateway, PostgREST, Postgres, Auth, Storage, Realtime, Edge Functions, pooler) | Requests, status, method, path, user, database events | Retention depends on plan; export continuously with log drains |
| Platform Audit Logs | Dashboard and Management API actions by members: actor IP, email, token type, action, target | Team and Enterprise only; stream with audit log drains |
| Auth audit logs | Sign-in and account events inside the project | Separate from platform logs |
| Security Advisor | Lints such as 0013 RLS disabled in public, 0007 policy exists RLS disabled, 0010 security definer view, 0002 auth users exposed, 0011 function search path mutable | Run in Studio, supabase db advisors or the Management API |
Detections worth building
- Bulk reads with the publishable key: large or paginated
selectresponses from theanonrole to sensitive tables (T1213 Data from Information Repositories). - Secret key use from unexpected sources:
service_rolerequests from new IPs or user agents (T1078.004 Valid Accounts: Cloud Accounts). - Key and setting changes: new secret keys, deactivated keys, signing key rotation, network restriction changes and member invitations in Platform Audit Logs.
- Advisor regressions: fail CI when a migration produces a new RLS or security definer lint.
- Leaked keys: scan repositories, client bundles and logs for
sb_secret_andservice_roleJWTs (T1552.001 Credentials In Files).
Metrics
- RLS coverage: percentage of tables in exposed schemas with RLS enabled and at least one policy. Target 100%.
- Advisor findings: open security lints per project, with time to resolve.
- Legacy key retirement: projects still accepting legacy
anonorservice_rolekeys. Target zero before Supabase's deprecation. - Secret key sprawl: secret keys per project with a named owner and component; target one per component, none unowned.
- Policy test coverage: percentage of tables with pgTAP allow and deny tests in CI.
Hardening baseline
- Data API: RLS on every table, default grants revoked, views with
security_invoker, no security definer functions in exposed schemas. - Database connections: enable SSL enforcement and network restrictions. Restrictions cover Postgres and the pooler only, not PostgREST, Auth or Storage, and Edge Functions then lose direct database access. Manage them with
supabase network-restrictions update --db-allow-cidr, remembering thatupdatereplaces the list unless you add--append. - Auth: email confirmations on, OTP expiry at 3,600 seconds or less, custom SMTP, user MFA where risk warrants it.
- Account: MFA for all members, two or more owners, scoped PATs only.
- Keys: publishable plus per-component secret keys, legacy keys deactivated, asymmetric JWT signing.
For small teams on a single project, the order that pays off fastest is RLS and advisors, then secret key separation, then account MFA. Larger organisations should add project-scoped roles, Platform Audit Logs with drains and a policy test gate in CI.
Common misconfigurations
- RLS off on a table behind a public key: the Moltbook database exposure combined a publishable key in client JavaScript with no RLS, giving anyone read and write access to tables holding about 1.5 million AI agent API tokens. The key was working as designed; the missing policies were the flaw.
- Service role key in the frontend: often introduced to "fix" a permissions error, it removes every policy for every visitor.
- Policy without
with check: users can update a row to belong to someone else or insert rows under another user ID. - Authorisation from
user_metadata: users can set their own role or tenant claim. - Views and security definer functions in
public: both bypass RLS unless configured otherwise. - One shared secret key everywhere: a leak from a single worker forces rotation across every service, so teams delay it.
Framework mapping
| Control area | NIST SP 800-53 Rev 5 | NIST CSF 2.0 | CIS Controls v8.1 | ISO/IEC 27001:2022 Annex A |
|---|---|---|---|---|
| Key and token lifecycle | IA-5, AC-2 | PR.AA-01 | 5 Account Management | A.5.17 |
| RLS and least privilege | AC-3, AC-6 | PR.AA-05 | 6 Access Control Management, 3 Data Protection | A.5.15, A.8.3 |
| Network restrictions and SSL | SC-7, SC-8 | PR.IR-01, PR.DS-02 | 12 Network Infrastructure Management | A.8.20, A.8.24 |
| Admin roles | AC-6 | PR.AA-05 | 6 Access Control Management | A.8.2 |
| Logging and review | AU-2, AU-6 | DE.CM-03 | 8 Audit Log Management | A.8.15, A.8.16 |
Practitioner checklist
- Enable RLS on every table in exposed schemas through migrations and revoke default grants.
- Write separate policies per operation with
usingandwith check, and an explicitauth.uid() is not null. - Run the Security Advisor and pgTAP policy tests in CI and fail builds on new findings.
- Replace legacy
anonandservice_roleJWTs with publishable and per-component secret keys, then deactivate the legacy keys. - Move to asymmetric JWT signing keys and revoke the legacy JWT secret.
- Keep secret keys out of browsers, bundles, URLs, logs and repositories; scan for
sb_secret_values. - Enforce MFA for all members, keep two or more owners and use project-scoped roles for production.
- Use scoped personal access tokens with expiry for automation; retire classic tokens.
- Enable SSL enforcement and network restrictions, and drain logs and Platform Audit Logs to your SIEM.
Standards and references
- Supabase: Understanding API Keys
- Supabase: Row Level Security
- Supabase: Postgres Roles
- Supabase: JWT Signing Keys
- Supabase: Performance and Security Advisors
- Supabase: Access Control
- Supabase: Production Checklist
- Supabase: Management API Reference
Related NHI Mgmt Group resources: API Key Management Guide · Firebase Credentials Security Guide · MongoDB Atlas Credentials Security Guide · Database Credential and Access Security Guide · Secrets Management Guide