Skip to main content
Technology & EngineeringBaas238 lines

Supabase

Supabase BaaS with Postgres, Auth, Realtime, Storage, and Edge Functions

Quick Summary30 lines
You are an expert in Supabase for rapid backend development, including its Postgres database, row-level security, Auth, Realtime subscriptions, Storage, and Edge Functions.

## Key Points

- Always enable RLS on every table — even if you add a permissive policy later, the default-deny stance protects against accidental data leaks.
- Use the `service_role` key only in server-side code; never expose it to the browser.
- Leverage database functions (`rpc`) for complex logic that should run atomically inside Postgres.
- Use Supabase CLI migrations (`supabase db diff`, `supabase migration new`) to version-control schema changes.
- Set `cacheControl` on storage uploads and use signed URLs for private buckets.
- Use `select()` after `insert`/`update`/`upsert` to return the resulting row in a single round trip.
- **Forgetting RLS**: Tables without RLS enabled are publicly readable/writable through the API when using the anon key.
- **Using service_role in the browser**: This bypasses RLS entirely and exposes full database access.
- **Not handling auth token refresh**: Use `onAuthStateChange` or the Supabase SSR helpers to keep sessions valid.
- **Ignoring Postgres connection limits**: Supabase uses connection pooling (PgBouncer) — avoid long-running transactions that hold connections open.
- **Large file uploads without chunking**: For files over 6 MB, use `uploadToSignedUrl` with resumable uploads.

## Quick Example

```
NEXT_PUBLIC_SUPABASE_URL=https://<project>.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=<anon-key>
SUPABASE_SERVICE_ROLE_KEY=<service-role-key>   # server-side only
```

```bash
# Deploy
supabase functions deploy hello
```
skilldb get baas-skills/SupabaseFull skill: 238 lines
Paste into your CLAUDE.md or agent config

Supabase — Backend as a Service

You are an expert in Supabase for rapid backend development, including its Postgres database, row-level security, Auth, Realtime subscriptions, Storage, and Edge Functions.

Core Philosophy

Overview

Supabase is an open-source Firebase alternative built on top of PostgreSQL. It provides a full backend stack: a Postgres database with a REST and GraphQL API (via PostgREST), authentication, real-time subscriptions via websockets, file storage, and serverless Edge Functions powered by Deno. Projects can be self-hosted or run on Supabase Cloud.

Setup & Configuration

Creating a Project

# Install the CLI
npm install -g supabase

# Initialize a local project
supabase init

# Link to a remote project
supabase link --project-ref <project-id>

# Start local development stack (Postgres, Auth, Storage, etc.)
supabase start

Client Initialization

import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

Environment Variables

NEXT_PUBLIC_SUPABASE_URL=https://<project>.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=<anon-key>
SUPABASE_SERVICE_ROLE_KEY=<service-role-key>   # server-side only

Core Patterns

Authentication

// Sign up
const { data, error } = await supabase.auth.signUp({
  email: 'user@example.com',
  password: 'secure-password',
});

// Sign in
const { data, error } = await supabase.auth.signInWithPassword({
  email: 'user@example.com',
  password: 'secure-password',
});

// OAuth (e.g., GitHub)
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'github',
});

// Get current session
const { data: { session } } = await supabase.auth.getSession();

// Listen to auth state changes
supabase.auth.onAuthStateChange((event, session) => {
  console.log(event, session);
});

Database Queries

// Select with filters
const { data, error } = await supabase
  .from('posts')
  .select('id, title, author:profiles(name)')
  .eq('published', true)
  .order('created_at', { ascending: false })
  .limit(10);

// Insert
const { data, error } = await supabase
  .from('posts')
  .insert({ title: 'New Post', body: 'Content', user_id: userId })
  .select()
  .single();

// Update
const { data, error } = await supabase
  .from('posts')
  .update({ title: 'Updated Title' })
  .eq('id', postId)
  .select()
  .single();

// Delete
const { error } = await supabase
  .from('posts')
  .delete()
  .eq('id', postId);

// RPC — call a Postgres function
const { data, error } = await supabase.rpc('search_posts', {
  query_text: 'supabase',
});

Row-Level Security (RLS)

-- Enable RLS on a table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Users can read published posts
CREATE POLICY "Public read" ON posts
  FOR SELECT USING (published = true);

-- Users can insert their own posts
CREATE POLICY "Insert own" ON posts
  FOR INSERT WITH CHECK (auth.uid() = user_id);

-- Users can update their own posts
CREATE POLICY "Update own" ON posts
  FOR UPDATE USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);

-- Users can delete their own posts
CREATE POLICY "Delete own" ON posts
  FOR DELETE USING (auth.uid() = user_id);

Realtime Subscriptions

const channel = supabase
  .channel('posts-changes')
  .on(
    'postgres_changes',
    { event: '*', schema: 'public', table: 'posts' },
    (payload) => {
      console.log('Change received:', payload);
    }
  )
  .subscribe();

// Cleanup
supabase.removeChannel(channel);

Storage

// Upload a file
const { data, error } = await supabase.storage
  .from('avatars')
  .upload(`${userId}/avatar.png`, file, {
    cacheControl: '3600',
    upsert: true,
  });

// Get a public URL
const { data } = supabase.storage
  .from('avatars')
  .getPublicUrl(`${userId}/avatar.png`);

// Download a file
const { data, error } = await supabase.storage
  .from('avatars')
  .download(`${userId}/avatar.png`);

Edge Functions

// supabase/functions/hello/index.ts
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';

serve(async (req) => {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  );

  const { name } = await req.json();
  return new Response(JSON.stringify({ message: `Hello ${name}` }), {
    headers: { 'Content-Type': 'application/json' },
  });
});
# Deploy
supabase functions deploy hello

Best Practices

  • Always enable RLS on every table — even if you add a permissive policy later, the default-deny stance protects against accidental data leaks.
  • Use the service_role key only in server-side code; never expose it to the browser.
  • Leverage database functions (rpc) for complex logic that should run atomically inside Postgres.
  • Use Supabase CLI migrations (supabase db diff, supabase migration new) to version-control schema changes.
  • Set cacheControl on storage uploads and use signed URLs for private buckets.
  • Use select() after insert/update/upsert to return the resulting row in a single round trip.

Common Pitfalls

  • Forgetting RLS: Tables without RLS enabled are publicly readable/writable through the API when using the anon key.
  • Using service_role in the browser: This bypasses RLS entirely and exposes full database access.
  • Not handling auth token refresh: Use onAuthStateChange or the Supabase SSR helpers to keep sessions valid.
  • Ignoring Postgres connection limits: Supabase uses connection pooling (PgBouncer) — avoid long-running transactions that hold connections open.
  • Large file uploads without chunking: For files over 6 MB, use uploadToSignedUrl with resumable uploads.

Anti-Patterns

Over-engineering for hypothetical requirements. Building for scenarios that may never materialize adds complexity without value. Solve the problem in front of you first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide wastes time and introduces risk.

Premature abstraction. Creating elaborate frameworks before having enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at system boundaries. Internal code can trust its inputs, but boundaries with external systems require defensive validation.

Skipping documentation. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add baas-skills

Get CLI access →