Security Checklist and Production Readiness
The idea in one sentence
Section titled “The idea in one sentence”A production-ready Supabase project is not any single setting done right — it is Row Level Security, deliberate Data API exposure, a service role key kept server-side, indexed policy columns, Supavisor in the right mode, migrations flowing through CI, and backups with point-in-time recovery, all holding together at once around one Postgres database.
The checklist
Section titled “The checklist”Each of these was covered earlier in the course. On its own, skipping any one of them does not necessarily break anything visibly — that is exactly what makes them easy to skip, and exactly why they belong on a checklist rather than left to memory.
- Row Level Security is enabled on every table holding user data, with policies for every operation you actually need. Enabling RLS with only a
selectpolicy silently makes every insert, update, and delete fail for ordinary users, since a missing policy for an operation denies it by default — covered in the Auth and RLS module. - Data API exposure is deliberate, per table and per function, not left on by default without thought. A table that does not need to be reachable over the REST or RPC interface should not be, since anything exposed there is reachable by anyone who can craft a valid request — covered in the Auto-Generated API module.
- The
service_rolekey never appears in client-side code. It bypasses RLS entirely, so it belongs only in trusted server-side contexts like Edge Functions, never shipped to a browser or mobile app bundle — covered in the Foundations and Edge Functions modules. - Indexes exist on columns referenced by RLS policies, and on other frequently filtered or joined columns. An unindexed policy column runs a full evaluation pass over every row on every query, which is fine on a small table and a production incident on a large one — covered in the previous lesson.
- Supavisor runs in transaction mode for serverless and edge database access, with direct or session-mode connections reserved for cases that genuinely need them. Serverless and Edge Function traffic opens far more concurrent connections than Postgres can hold directly, which is exactly the problem the pooler exists to solve — covered in the first lesson of this module.
- Migrations are tracked in version control and applied through CI, not run ad hoc against production. A migration file merged to the main branch should deploy the same way every time, through the same pipeline that already tested it on a preview branch — covered in the previous lesson.
- Backups and point-in-time recovery are enabled for anything that matters in production. Point-in-time recovery, or PITR, lets you restore a project to any specific moment within its retention window, not just to the most recent daily backup, which matters when the incident you need to undo happened hours after the last snapshot was taken.
-- A quick self-check: any table with RLS enabled but zero policies for a given command-- denies that command entirely rather than allowing it, so gaps here fail closed, not openselect relname as table_name, relrowsecurity as rls_enabledfrom pg_classwhere relnamespace = 'public'::regnamespace and relkind = 'r';One project, all of it at once
Section titled “One project, all of it at once”Picture a real app: a task manager with a todos table, RLS enabled with policies scoped to auth.uid() = user_id, and an index on that same user_id column so the policy check stays fast as the table grows. Only the todos table and the create_todo RPC function are exposed through the Data API — nothing else in the schema is reachable over REST. The app’s serverless backend connects through Supavisor in transaction mode, so a burst of concurrent requests never comes close to exhausting Postgres’s connection limit. An Edge Function handles the one operation that genuinely needs elevated access — say, an admin cleanup job — using the service_role key, which exists only in that function’s environment and nowhere in the client bundle. Every schema change, from the original create table to the index that came later, is a migration file that went through a preview branch and was applied to production by CI, not by someone pasting SQL into a dashboard. And point-in-time recovery is enabled, so a bad migration or an accidental bulk delete is a restore away from being undone, not a permanent loss.
None of these pieces is exotic on its own. What makes the project production-ready is that they hold together at the same time, around the same database, instead of being true in isolation while the rest of the checklist quietly does not apply.
flowchart TD
db[("Postgres database")]
rls["RLS policies scoped to auth.uid()"] --> db
idx["Index on RLS policy columns"] --> db
api["Data API - only todos table and create_todo RPC exposed"] --> db
pool["Serverless backend via Supavisor - transaction mode"] --> db
fn["Edge Function using service_role key for admin tasks"] --> db
ci["Migrations via preview branch and CI"] --> db
pitr["Backups with point-in-time recovery"] --> db