Supabase Authorization and Production Operations
Supabase puts a PostgreSQL database directly behind an auto-generated API, so browsers and mobile apps can query tables with nothing in between but the database's own permissions. That is what makes it fast to build with — and why authorization lives in the database. Every table reachable through the API must be protected by row-level security (RLS) policies and correct grants; a single table without them can expose every row to anyone holding the public API key, which by design ships in every client.
Production readiness goes beyond policies: tested migrations, controlled privileges for functions and views, storage access rules, connection pooling, backups that have actually been restored, and careful handling of the keys that bypass all of it.
TL;DR
- Enable RLS on every table in exposed schemas and write explicit policies per operation (
SELECT,INSERT,UPDATE,DELETE). - Base policies on
auth.uid()and trusted JWT claims (such asapp_metadata), never on user-editable metadata. - The service role (secret) key bypasses RLS — use it only on servers, never in clients.
- Lock down default privileges: revoke broad grants, expose functions explicitly, and make views
security_invoker. - Protect Storage with policies on
storage.objects, and prefer private buckets with signed URLs. - Test policies with pgTAP (
supabase test db), manage schema with CLI migrations, use the pooler, and drill restores.
Quick Example
Organization-scoped projects: members can read, only admins can update.
A pgTAP test that impersonates a non-admin member and proves both policies:
Core Concepts
Roles and Keys
The publishable key is public by design; security comes from RLS and grants. Secret keys must stay in server-side secrets, be rotatable, and never appear in client bundles, mobile apps, or logs.
Writing Policies
- One policy per operation and role, with
USINGcontrolling which existing rows are visible or modifiable andWITH CHECKcontrolling what new or updated rows may look like. - Use
auth.uid()for the user's ID, wrapped as(SELECT auth.uid())so PostgreSQL evaluates it once per statement. - Use
auth.jwt()for claims set by the server, such as roles inapp_metadata. Never authorize onuser_metadata, which users can change. - Index columns used in policies (
memberships.user_id,org_id), or policies become slow full scans. - Prefer membership tables for multi-tenant access. See Row-Level Security and Authorization Models.
Grants, Functions, and Views
RLS only filters rows; grants decide which tables, columns, and functions the API roles can touch at all. Default privileges in exposed schemas are broad, so harden them:
- Revoke broad defaults and grant only the tables, columns, and operations clients need — for example, allow updates to specific columns only.
- Functions are callable through the API when granted to
anonorauthenticated; revokeEXECUTEby default and grant intentionally. TreatSECURITY DEFINERfunctions as privileged code: set a fixedsearch_path, validate inputs, and check permissions inside. - Views run with their owner's privileges by default, bypassing RLS on underlying tables; create them with
security_invoker = true(PostgreSQL 15+) so callers' policies apply, and grant onlySELECT. - Keep internal tables in schemas not exposed through the API.
Storage Policies
Files live in buckets whose access is controlled by RLS policies on storage.objects. Common patterns: private buckets with per-user folders ((storage.foldername(name))[1] = (SELECT auth.uid())::text), size and type limits per bucket, and short-lived signed URLs for downloads. Public buckets are appropriate only for genuinely public assets. See Object Storage and Signed Download URLs.
Production Operations
Migrations
Keep the schema in version control as migrations: create them with supabase migration new, generate diffs from local changes with supabase db diff, test them against a local stack and CI, and apply with supabase db push or a deployment pipeline. Avoid editing production through the dashboard, which creates drift between environments. See Database Migrations.
Connections
Serverless functions and many short-lived clients exhaust PostgreSQL connections quickly. Use the connection pooler (transaction mode) for those workloads, direct connections for migrations and long-lived services, and monitor connection counts. See PostgreSQL Connection Pooling.
Backups and Recovery
Managed projects include daily backups, and point-in-time recovery is available as an add-on for finer recovery points. Also take independent logical backups (for example supabase db dump) for critical data, store them outside the project, and practice restores into a separate project, measuring recovery time. Remember Storage objects need their own backup strategy. See PostgreSQL Backup and Recovery.
Hardening Checklist
- RLS enabled and tested on every exposed table; Security Advisor and
supabase db lintfindings resolved. - Secret keys stored in a secrets manager, rotated, and scanned for in repositories.
- Network restrictions and SSL enforcement for direct database access.
- Multi-factor authentication and least privilege for dashboard access.
- Auth settings reviewed: email confirmation, rate limits, redirect URL allowlists, custom SMTP.
- Monitoring and alerts on errors, slow queries, connection saturation, and disk usage.
Best Practices
Deny by Default
Enable RLS before adding data, start with no policies (no access), and add the narrowest policies each feature needs.
Test Policies Like Code
Write pgTAP tests impersonating anonymous users, members, admins, and users from other tenants, and run them in CI for every migration.
Keep Privileged Work on the Server
Operations that cross tenants or need elevated access belong in server code or Edge Functions using the service role, with their own authorization checks.
Treat Grants and Policies as One Surface
Review table grants, column grants, function EXECUTE grants, view security, and RLS together whenever the schema changes.
Common Mistakes
Forgetting RLS on a New Table
Authorizing on user_metadata
Views That Bypass RLS
FAQ
Is it safe to expose the Supabase publishable key?
Yes, it is designed to be public. Safety depends on row-level security policies and grants that restrict what the anon and authenticated roles can access.
What does the service role key do?
It authenticates as a role that bypasses row-level security. It must only be used in trusted server environments and never shipped to browsers or mobile apps.
How do I test Supabase RLS policies?
Write pgTAP tests that set the role and JWT claims to simulate different users, then assert which rows are visible and which changes succeed. Run them locally and in CI with supabase test db.
Do views respect row-level security?
Not by default: views execute with the owner's privileges. Create them with security_invoker = true so the caller's permissions and policies apply.
How should I back up a Supabase project?
Use the platform's daily backups and point-in-time recovery for the database, keep independent logical dumps of critical data elsewhere, back up Storage objects separately, and test restores regularly.
Related Topics
- Supabase — The platform overview
- Row-Level Security — PostgreSQL RLS in depth
- PostgreSQL Security — Roles, grants, and hardening
- Authorization Models — RBAC, ABAC, and relationship-based access
- Database Migrations — Evolving schemas safely
- PostgreSQL Backup and Recovery — Backups and point-in-time recovery