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:

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:

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:

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.