Supabase apps are often built in days, frequently with AI coding agents, and shipped with the browser talking straight to the database, so a single missing policy on a table, a storage bucket or a Realtime channel becomes a public data leak. The pattern is now familiar: a publishable key in front-end JavaScript, Row Level Security off or too broad, and anyone on the internet able to read or write the data. This guide covers the whole Supabase security surface beyond keys: how identity and access work across the Data API, Storage and Realtime, organisation roles and account security, Auth configuration, network restrictions and SSL enforcement, Security Advisor, logging and a production hardening baseline. It is for developers, platform engineers and security reviewers of Supabase projects. Key types, the service role and key rotation are covered in depth in the Supabase Keys Security Guide; this page links there rather than repeating it.
This is an independent guide from NHI Mgmt Group, based on Supabase's public documentation as of November 2026. It is not sponsored or reviewed by Supabase. Product features change, so confirm settings against Supabase's current documentation.
Key takeaways
- Policies protect every surface, not only tables. Tables, storage.objects and realtime.messages each need their own Row Level Security policies; a correct table policy does nothing for a public bucket or a public Realtime channel.
- Network restrictions do not cover the APIs. Supabase network restrictions apply to Postgres and the pooler only; the Data API, Storage and Auth over HTTPS stay reachable and rely on policies and keys.
- Auth defaults need tuning for production. Enable email confirmations, keep OTP expiry at 3,600 seconds or less, add CAPTCHA, set password rules and, on paid plans, leaked password protection.
- The account that owns the project is a privileged identity. Enforce MFA for the organisation, keep more than one Owner, and use Developer, Read-Only and project-scoped roles where your plan supports them.
- Run Security Advisor in CI, not once. Lints such as 0013 RLS disabled in public and 0010 security definer view catch the mistakes that cause most Supabase leaks, and they can run from the CLI with supabase db advisors.
How identity and access work in Supabase
A Supabase project is a Postgres database with services in front of it: the Data API (PostgREST), Auth, Storage, Realtime and Edge Functions. Requests from the browser carry the project's publishable key and, after sign-in, a user JWT. Postgres roles and Row Level Security decide what each request can do. That makes Postgres the policy engine for almost everything.
| Surface | How access is decided | What protects it | Typical gap |
|---|---|---|---|
| Data API (tables, views, functions) | Postgres role from the key and JWT (anon or authenticated), then RLS | RLS on every exposed table, careful grants, views and functions that respect RLS | RLS disabled on a table in the public schema |
| Storage | RLS policies on storage.objects, bucket public or private | Per-operation policies scoped by bucket, folder and owner | Bucket made public for convenience; listing allowed by a broad SELECT policy |
| Realtime Broadcast and Presence | RLS on realtime.messages for private channels | "Allow public access" disabled, select and insert policies per topic | Public channels carrying private data |
| Realtime Postgres Changes | The table's own RLS | RLS on replicated tables | Supabase notes RLS does not filter DELETE events, so sensitive changes should use Broadcast with authorisation |
| Direct Postgres and pooler | Database credentials, network restrictions, SSL | Network Restrictions, SSL Enforcement, strong database passwords | Open to 0.0.0.0/0 with a leaked connection string |
| Dashboard and Management API | Supabase account, organisation role, access tokens | Organisation MFA, roles, audit logs | Single Owner with a password-only account |
Row Level Security in one paragraph
Enable RLS on every table in an exposed schema, write one policy per operation, compare ownership columns to auth.uid() or auth.jwt() claims, take authorisation data from app metadata rather than user-editable metadata, and test both allowed and denied cases as the anon and authenticated roles. This is the Supabase form of preventing OWASP API1:2023 Broken Object Level Authorization and meets OWASP ASVS 5.0 V8.2.2 (data-specific access). Detailed patterns and the bypass routes through views and security definer functions are in the Supabase Keys Security Guide and the OWASP API Top 10 API1 Broken Object Level Authorization Guide.
Storage policies
Storage is deny by default: Supabase does not allow uploads to buckets without RLS policies on storage.objects. Points that catch teams out:
- Uploads: an upload needs only an INSERT policy, but
upsert(overwrite) also needs SELECT and UPDATE policies. - Scope by bucket and folder: restrict with
bucket_id = '...'and usestorage.foldername(name)to tie the first folder to the user's ID fromauth.jwt()->>'sub', giving each user a private prefix. - Ownership: let users read files they uploaded by checking
owner_id. - Listing versus reading: a broad SELECT policy also allows listing a bucket's contents. Supabase provides
storage.allow_only_operation()andstorage.allow_any_operation()so listing and object reads can have different rules. - Public buckets: anything in a public bucket is readable by anyone without a SELECT policy. Use public buckets only for genuinely public assets such as product images, never for uploads from users.
- Service key: a service or secret key bypasses Storage RLS entirely, so server-side upload handlers must enforce their own checks.
Realtime authorisation
For Broadcast and Presence, turn off "Allow public access" in Realtime Settings and create channels with config: { private: true }. Policies on realtime.messages decide who can receive (select) and send or track presence (insert), and can scope by realtime.messages.extension (broadcast or presence) and by realtime.topic(). Permissions are evaluated when a client joins and cached for the connection, so a user whose access is revoked keeps receiving messages until the JWT expires: keep JWT lifetimes short. RLS is already enabled on realtime.messages; adding an ENABLE ROW LEVEL SECURITY statement for it in a migration fails with error 42501.
Admin roles and privileged access
Whoever controls the Supabase organisation can read every table, download backups, change Auth settings and create keys. Treat dashboard access as privileged access.
| Organisation role | Access | Use for |
|---|---|---|
| Owner | Full access to organisation and project resources | Two or more named people, never a shared account |
| Administrator | Nearly full access; cannot change organisation settings, move projects out of the organisation or add Owners | Platform leads |
| Developer | Read-only on organisation resources; full content access to projects; cannot change project settings | Engineers who build features |
| Read-Only | View-only on organisation and project resources | Auditors and support |
| No-access plus project-scoped role | Only the projects explicitly granted | Contractors and agencies |
Availability: Supabase documents Read-Only, No-access and project-scoped roles on the Team and Enterprise plans, organisation MFA enforcement on Pro, Team and Enterprise, and Platform Audit Logs on Team and Enterprise.
- Account MFA: the production checklist asks for GitHub two-factor authentication if you sign in with GitHub, or Supabase MFA for email and password sign-in. Enforce MFA at organisation level where your plan allows, so every member needs an MFA-backed session.
- Owners: keep more than one Owner so you are not locked out, and review membership when staff or agencies change.
- Separate production: put production in its own project, with fewer members than development, and deploy schema changes through migrations rather than dashboard edits.
- AI coding agents: agents connected to a project through the Supabase MCP server or a personal access token act with that token's rights. Point them at development projects, scope their tokens to the minimum, and never give an agent production Owner rights. See the AI Coding Agents Security Guide.
Integrations, API tokens and service accounts
Supabase has several machine credentials, each with a different blast radius. Keys and rotation are covered in the keys guide; the summary for a security review is:
- Publishable (or legacy anon) key: public by design; safe only when every exposed surface has correct policies.
- Secret (or legacy service_role) key: bypasses RLS on tables and Storage. Server-side only, one per backend service, stored in a secrets manager.
- Database connection strings: full Postgres credentials. Pair with network restrictions and SSL enforcement, and use dedicated Postgres roles for each backend rather than the default
postgresuser. - Personal access tokens and Management API tokens: act with the user's organisation rights; scope who can create them and revoke unused ones.
- Edge Function secrets: third-party API keys used by functions; store them as function secrets or in Supabase Vault, never in client bundles.
- Custom SMTP credentials: custom SMTP makes Auth emails come from your domain and is part of the production checklist; protect the SMTP credential like any other mail-sending key.
Inventory these as non-human identities with owners. See the API Key Management Guide and the JWT Security Guide for the token side.
Auth configuration
| Setting | Production value | Why |
|---|---|---|
| Email confirmations | Enabled (Authentication, Sign In / Providers) | Stops sign-ups with addresses the user does not control |
| OTP expiry | 3,600 seconds or lower; increase OTP length for more entropy | Also shortens confirmation, recovery, email change and invitation links |
| Test phone numbers | Removed | Test numbers are a fixed login path |
| Password strength | Minimum length of at least 8 (Supabase discourages shorter); character requirements as policy needs | Existing weak passwords still sign in but return WeakPasswordError so you can prompt a change |
| Leaked password protection | Enabled where available (Pro Plan and above) | Rejects passwords found in the HaveIBeenPwned Pwned Passwords data |
| Secure password change | Require reauthentication (session newer than 24 hours, otherwise nonce) or current password | Stops a stolen session from changing the password |
| CAPTCHA | On sign-up, sign-in and password reset | Reduces automated account creation and credential stuffing (MITRE ATT&CK T1110) |
| Rate limits | Review defaults under Authentication, Rate Limits (for example MFA challenge and verify 15 per minute, token refresh 1,800 per hour) | Tune to real traffic; lower anonymous sign-ins if unused |
| MFA for app users | Enabled for admin and high-risk users at least | Supabase recommends it where the app needs a higher security level |
| Custom SMTP | Configured (Authentication, Emails, SMTP Settings) | Trusted sender domain, higher and controllable email limits |
Network restrictions and SSL
Turn on SSL Enforcement under Database, Settings, SSL Configuration so direct connections must use TLS. Network Restrictions limit which IPv4 and IPv6 CIDR ranges can reach Postgres and the pooler; they need Owner or Admin permission and are set separately for each address family. The CLI's update --db-allow-cidr replaces the whole list unless you add --append, so take a copy of the current list first. Two limits matter for design: restrictions do not cover HTTPS APIs (PostgREST, Storage, Auth), and Edge Functions lose direct database access once restrictions apply, so they should connect through supabase-js instead. Removing restrictions means setting 0.0.0.0/0 and ::/0, which is also the rollback if you lock out a critical service.
Logging and detection
Supabase has three logging layers. Platform audit logs record dashboard and Platform API actions such as creating projects, inviting members, changing Edge Functions and project settings, with actor IP, email and token type; they are on Team and Enterprise plans, cannot be exported from the dashboard and can be streamed with Audit Log Drains. Auth audit logs record authentication activity in each project. Project logs cover API, Postgres and other services, and Postgres connection logging is listed among the database security features.
| Signal | Source | Possible meaning |
|---|---|---|
| New Security Advisor findings after a deploy | supabase db advisors in CI, Studio /advisors/security | A migration added a table without RLS or a security definer view |
| Large reads by the anon role from a single IP | API logs | Scraping through a missing or broad policy (Data from Information Repositories, T1213) |
| Member invited, role changed, network restriction or Auth setting changed | Platform audit logs | Account takeover or unapproved change |
| Spike in sign-ups, failed sign-ins or OTP requests | Auth audit logs, rate limit hits | Bots, credential stuffing or SMS pumping |
| Bucket switched to public, new storage policy | Platform audit logs, migration history | Exposure of uploaded files |
| Direct database connections from unknown IPs | Postgres connection logs | Leaked connection string (Valid Accounts, T1078) |
Metrics:
- Tables in exposed schemas with RLS enabled and at least one tested policy (target 100%).
- Open Security Advisor security findings, by lint, and time to close.
- Buckets that are public, each with a documented reason.
- Realtime channels that are private where they carry user data.
- Organisation members with MFA, and number of Owners (target two or more).
- Projects with SSL Enforcement and Network Restrictions enabled.
Hardening baseline
- Run Security Advisor now: fix 0013 RLS disabled in public, 0007 policy exists RLS disabled, 0002 auth users exposed, 0010 security definer view and 0011 function search path mutable first. Some findings may be intentional, so confirm against your access model before changing.
- Policies on every surface: tables, storage.objects and realtime.messages; private buckets and channels by default. Write policies in migrations so they are reviewed and versioned.
- Test as an outsider: query every exposed table, bucket and channel with only the publishable key and as a second user, and confirm denials. Add these tests to CI and run supabase db advisors in the same pipeline.
- Auth settings: apply the table above; enable CAPTCHA and email confirmations; set OTP expiry; configure custom SMTP.
- Database access: SSL Enforcement on, Network Restrictions to known ranges, dedicated Postgres roles for backends, secret keys only on servers.
- Organisation: MFA enforced, two Owners, least-privilege roles, separate production project, agencies on project-scoped roles.
- Recovery: enable Point in Time Recovery where data matters (Supabase suggests it once the database is expected to exceed 4 GB, and notes free plan backups cannot be downloaded), and test a restore.
- Threat review: follow the checklist's advice to list how an attacker could abuse your app and review the OWASP Top 10.
Rollback: enabling RLS on a live table without policies blocks all API access to it. Roll out per table: write and test policies in a branch or staging project, enable RLS, watch API errors, and keep the policy migration revertible.
Common misconfigurations
- RLS off with the key in the browser: a publishable key in public JavaScript is expected, so turn on RLS for every exposed table; without it, anyone with the key can read and write those tables.
- Table policies right, bucket public: user uploads such as ID documents or invoices placed in a public bucket, readable by anyone with the URL.
- Broad storage SELECT policy: lets any signed-in user list every object in a bucket.
- Private data on public Realtime channels: "Allow public access" left on, so anyone with the publishable key can join a topic.
- Relying on network restrictions to protect the API: they only cover direct database connections.
- Single Owner, no MFA: one phished account controls every project, backup and key.
- AI-generated schema shipped unreviewed: tables created by a coding agent without policies or with
using (true). See the AI-Generated Code Review Guide.
Framework mapping
| Control area | NIST SP 800-53 Rev. 5 | OWASP or NIST CSF 2.0 |
|---|---|---|
| RLS on tables, Storage and Realtime | AC-3, AC-6 | API1:2023 BOLA; ASVS 5.0 V8.2.2, V8.2.3 |
| Auth settings, MFA, CAPTCHA, rate limits | IA-2, IA-5 | API2:2023 Broken Authentication |
| Network restrictions and SSL enforcement | SC-7, SC-8 | API8:2023 Security Misconfiguration |
| Organisation roles and project scoping | AC-2, AC-6 | ASVS 5.0 V8.2.1 |
| Security Advisor and policy tests in CI | SA-11 | API8:2023 Security Misconfiguration |
| Audit logs and drains | AU-6 | CSF PR.PS-04 |
| PITR and backups | CP-9 | CSF PR.DS-11 |
Practitioner checklist
- Run Security Advisor and clear all RLS-related findings; add supabase db advisors to CI.
- Confirm RLS and tested policies on every exposed table, storage.objects and realtime.messages.
- Make buckets and Realtime channels private unless the content is genuinely public.
- Enable email confirmations, CAPTCHA, OTP expiry of 3,600 seconds or less, password rules and leaked password protection where available.
- Turn on SSL Enforcement and set Network Restrictions to known IPv4 and IPv6 ranges.
- Enforce organisation MFA, keep two Owners and use least-privilege roles for everyone else.
- Keep secret keys and connection strings server-side and inventoried with owners.
- Stream audit logs where your plan allows and alert on member, setting and bucket changes.
- Enable PITR for production data and test a restore.
Standards and references
- Supabase: Production security checklist
- Supabase: Storage access control
- Supabase: Realtime Authorization
- Supabase: Database advisors
- Supabase: Network restrictions
- Supabase: Password security
- Supabase: Access control (organisation roles)
- OWASP: API Security Top 10 2023
Related NHI Mgmt Group resources: Supabase Keys Security Guide · AI Coding Agents Security Guide · OWASP API Top 10 API1 Broken Object Level Authorization Guide · JWT Security Guide