What Is Supabase?
Supabase is an open-source backend-as-a-service (BaaS) built on top of PostgreSQL. You get a managed database, user authentication, file storage, auto-generated APIs, and real-time data subscriptions — all configured through a web dashboard and accessed via client libraries for JavaScript, Python, Dart, Swift, and Kotlin.
Under the hood, Supabase composes several open-source tools into a single platform:
- PostgreSQL — the core relational database, with full SQL support including joins, transactions, views, and extensions like
pgvectorfor embeddings - GoTrue — handles authentication and user management: email/password, OAuth providers, magic links, and multi-factor auth
- PostgREST — reads your database schema and generates a full REST API automatically, so every table becomes an API endpoint
- Realtime — delivers database changes to connected clients over WebSockets, enabling live updates
- Storage — an S3-compatible file storage system with its own access policies
- Edge Functions — serverless functions running on Deno for custom backend logic like webhooks, payment processing, or third-party API calls
- pg_graphql — an optional GraphQL layer that sits on top of your tables
How the Security Model Works
Every Supabase project comes with two keys: an anon key and a service_role key. The anon key is meant to be used in frontend code — it's public. The service_role key has full access to everything and is meant to stay on the server.
Because the anon key is public, your database is directly accessible from the browser. The security layer that controls what each user can actually do is Row Level Security (RLS) — a PostgreSQL feature that lets you write policies defining exactly which rows a given user can read, insert, update, or delete.
This is the foundation of Supabase security. Every table needs RLS enabled with explicit policies. The API, Realtime, and Storage all respect these policies.
What Is Supabase Used For?
Supabase works well for a wide range of applications because it gives you a full Postgres database with a managed infrastructure layer on top:
- MVPs and prototypes — a complete backend in minutes: auth, database, and API ready to go
- SaaS products — user management, subscription data, and tenant isolation with RLS policies
- Mobile apps — first-class SDKs for Flutter, React Native, Swift, and Kotlin
- AI/ML projects — store and query vector embeddings with
pgvector, build RAG pipelines backed by Postgres - Real-time apps — chat, live dashboards, and collaborative editors using Realtime subscriptions
- Internal tools — admin panels and back-office apps backed by Postgres with row-level access control
Security Risks and How to Handle Them
1. Tables Without Row Level Security
When you create a new table, RLS is disabled by default. With RLS off, the auto-generated API gives full read and write access to every row in that table to anyone who has the anon key — which is everyone, since it's public.
Enable RLS on every table and write specific policies that match your access requirements. A good starting point: each user can only access their own rows.
-- Enable RLS
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
-- Each user can only access their own documents
CREATE POLICY "users own documents" ON documents
FOR ALL USING (auth.uid() = owner_id)
WITH CHECK (auth.uid() = owner_id);
For tables with shared data, write more granular policies — separate SELECT, INSERT, UPDATE, and DELETE policies with the minimum access each operation needs.
2. Service Role Key Exposure
The service_role key bypasses all RLS policies. It belongs in server-side code only: Edge Functions, backend API routes, or CI/CD pipelines.
Store it in your hosting provider's secret management (Vercel environment
variables, Fly.io secrets, etc.). Add .env to your
.gitignore. Audit your client bundle to confirm the key
is absent — build tools sometimes inline environment variables that start
with certain prefixes.
3. Storage Bucket Policies
Supabase Storage uses the same policy system as database tables. Each bucket needs policies that scope file access to the right users. A practical pattern is to organize files into per-user folders and write a policy that enforces it:
-- Users can only access files in their own folder
CREATE POLICY "user files" ON storage.objects
FOR ALL USING (
bucket_id = 'uploads'
AND (storage.foldername(name))[1] = auth.uid()::text
);
For shared assets like public profile pictures, create a separate bucket with a read-only policy and restrict uploads to authenticated users.
4. Input Validation
The PostgREST API accepts any data that satisfies your column types. For stronger guarantees, add validation at the database level using PostgreSQL's built-in tools:
CHECKconstraints for value ranges and format rulesNOT NULLfor required fieldsUNIQUEconstraints to prevent duplicates- Foreign keys for referential integrity
- Custom domains and types for reusable validation
For more complex validation (cross-field checks, external lookups), run the logic in an Edge Function and have the client call that function instead of inserting directly.
5. Realtime Data Access
Realtime broadcasts database changes to subscribed clients over WebSockets. It respects RLS policies — a user will only receive change events for rows their policies allow them to read. This works correctly as long as your RLS policies are in place.
Test this explicitly: connect as two different users and verify that each only receives events for their own data. Pay special attention to tables where you've written broader SELECT policies, since those determine what gets broadcast.
6. Edge Function Authentication
Edge Functions are accessible via a public URL. To restrict access, verify the JWT from the Authorization header inside the function:
import { createClient } from "@supabase/supabase-js";
Deno.serve(async (req) => {
const authHeader = req.headers.get("Authorization");
if (!authHeader) return new Response("Unauthorized", { status: 401 });
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_ANON_KEY")!,
{ global: { headers: { Authorization: authHeader } } }
);
const { data: { user }, error } = await supabase.auth.getUser();
if (error || !user) return new Response("Unauthorized", { status: 401 });
// Authenticated — proceed with logic
});
This pattern creates a Supabase client scoped to the calling user, so any database queries inside the function also respect that user's RLS policies.
7. Use the Security Advisor
The Supabase dashboard includes a Security Advisor that scans your project and flags tables without RLS, exposed schemas, and other misconfigurations. Run it regularly — especially before deploying to production and after adding new tables.
Closing Thoughts
Supabase gives solo developers and small teams a production-grade backend with very little setup. The key to using it well is understanding that your database sits behind a public API, and Row Level Security is what controls access. Enable it on every table, scope your policies tightly, keep the service_role key on the server, and let the Security Advisor catch anything you miss.