supabase

Core Supabase CLI, migrations, RLS, Edge Functions

INSTALLATION
npx skills add https://github.com/alinaqi/claude-bootstrap --skill supabase
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

Supabase Core Skill

Core concepts, CLI workflow, and patterns common to all Supabase projects.

Sources: Supabase Docs | Supabase CLI

Core Principle

Local-first, migrations in version control, never touch production directly.

Develop locally with the Supabase CLI, capture all changes as migrations, and deploy through CI/CD.

Supabase Stack

Service

Purpose

Database

PostgreSQL with extensions

Auth

User authentication, OAuth providers

Storage

File storage with RLS

Edge Functions

Serverless Deno functions

Realtime

WebSocket subscriptions

Vector

AI embeddings (pgvector)

CLI Setup

Install & Login

# macOS

brew install supabase/tap/supabase

# npm (alternative)

npm install -g supabase

# Login

supabase login

Initialize Project

# In your project directory

supabase init

# Creates:

# supabase/

# ├── config.toml      # Local config

# ├── seed.sql         # Seed data

# └── migrations/      # SQL migrations

Link to Remote

# Get project ref from dashboard URL: https://supabase.com/dashboard/project/<ref>

supabase link --project-ref <project-id>

# Pull existing schema

supabase db pull

Start Local Stack

supabase start

# Output:

# API URL: http://localhost:54321

# GraphQL URL: http://localhost:54321/graphql/v1

# DB URL: postgresql://postgres:postgres@localhost:54322/postgres

# Studio URL: http://localhost:54323

# Anon key: eyJ...

# Service role key: eyJ...

Migration Workflow

Option 1: Dashboard + Diff (Quick Prototyping)

# 1. Make changes in local Studio (localhost:54323)

# 2. Generate migration from diff

supabase db diff -f <migration_name>

# 3. Review generated SQL

cat supabase/migrations/*_<migration_name>.sql

# 4. Reset to test

supabase db reset

Option 2: Write Migrations Directly (Recommended)

# 1. Create empty migration

supabase migration new create_users_table

# 2. Edit the migration file

# supabase/migrations/<timestamp>_create_users_table.sql

# 3. Apply locally

supabase db reset

Option 3: ORM Migrations (Best DX)

Use Drizzle (TypeScript) or SQLAlchemy (Python) - see framework-specific skills.

Deploy Migrations

# Push to remote (staging/production)

supabase db push

# Check migration status

supabase migration list

Database Patterns

Enable RLS on All Tables

-- Always enable RLS

ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;

-- Default deny - must create policies

CREATE POLICY "Users can view own profile"

  ON public.profiles

  FOR SELECT

  USING (auth.uid() = id);

Common RLS Policies

-- Public read

CREATE POLICY "Public read access"

  ON public.posts FOR SELECT

  USING (true);

-- Authenticated users only

CREATE POLICY "Authenticated users can insert"

  ON public.posts FOR INSERT

  WITH CHECK (auth.role() = 'authenticated');

-- Owner access

CREATE POLICY "Users can update own records"

  ON public.posts FOR UPDATE

  USING (auth.uid() = user_id);

-- Admin access (using custom claim)

CREATE POLICY "Admins have full access"

  ON public.posts FOR ALL

  USING (auth.jwt() ->> 'role' = 'admin');

Link to auth.users

-- Profile table linked to auth

CREATE TABLE public.profiles (

  id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,

  username TEXT UNIQUE NOT NULL,

  avatar_url TEXT,

  created_at TIMESTAMPTZ DEFAULT NOW()

);

-- Auto-create profile on signup

CREATE OR REPLACE FUNCTION public.handle_new_user()

RETURNS TRIGGER AS $$

BEGIN

  INSERT INTO public.profiles (id, username)

  VALUES (NEW.id, NEW.email);

  RETURN NEW;

END;

$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER on_auth_user_created

  AFTER INSERT ON auth.users

  FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();

Seed Data

supabase/seed.sql

-- Runs on `supabase db reset`

-- Use ON CONFLICT for idempotency

INSERT INTO public.profiles (id, username, avatar_url)

VALUES

  ('d0e1f2a3-b4c5-6d7e-8f9a-0b1c2d3e4f5a', 'testuser', null),

  ('a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d', 'admin', null)

ON CONFLICT (id) DO NOTHING;

Environment Variables

Required Variables

# Public (safe for client-side)

SUPABASE_URL=https://xxxxx.supabase.co

SUPABASE_ANON_KEY=eyJ...

# Private (server-side only - NEVER expose)

SUPABASE_SERVICE_ROLE_KEY=eyJ...

DATABASE_URL=postgresql://postgres.[ref]:[password]@aws-0-region.pooler.supabase.com:6543/postgres

Local vs Production

# .env.local (local development)

SUPABASE_URL=http://localhost:54321

SUPABASE_ANON_KEY=<from supabase start>

DATABASE_URL=postgresql://postgres:postgres@localhost:54322/postgres

# .env.production (remote)

SUPABASE_URL=https://xxxxx.supabase.co

SUPABASE_ANON_KEY=<from dashboard>

DATABASE_URL=<connection pooler URL>

Connection Pooling

# Transaction mode (recommended for serverless)

# Add ?pgbouncer=true to URL

DATABASE_URL=postgresql://...@pooler.supabase.com:6543/postgres?pgbouncer=true

# Session mode (for migrations, long transactions)

DATABASE_URL=postgresql://...@pooler.supabase.com:5432/postgres

Edge Functions

Create Function

supabase functions new hello-world

Basic Structure

// supabase/functions/hello-world/index.ts

import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';

serve(async (req) => {

  const { name } = await req.json();

  return new Response(

    JSON.stringify({ message: `Hello ${name}!` }),

    { headers: { 'Content-Type': 'application/json' } }

  );

});

With Auth Context

import { serve } from 'https://deno.land/std@0.168.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_ANON_KEY') ?? '',

    {

      global: {

        headers: { Authorization: req.headers.get('Authorization')! },

      },

    }

  );

  const { data: { user } } = await supabase.auth.getUser();

  if (!user) {

    return new Response('Unauthorized', { status: 401 });

  }

  return new Response(JSON.stringify({ user_id: user.id }));

});

Deploy

# Serve locally

supabase functions serve

# Deploy single function

supabase functions deploy hello-world

# Deploy all

supabase functions deploy

Storage

Create Bucket (in migration)

INSERT INTO storage.buckets (id, name, public)

VALUES ('avatars', 'avatars', true);

-- Storage policies

CREATE POLICY "Avatar images are publicly accessible"

  ON storage.objects FOR SELECT

  USING (bucket_id = 'avatars');

CREATE POLICY "Users can upload own avatar"

  ON storage.objects FOR INSERT

  WITH CHECK (

    bucket_id = 'avatars' AND

    auth.uid()::text = (storage.foldername(name))[1]

  );

CLI Quick Reference

# Lifecycle

supabase start                   # Start local stack

supabase stop                    # Stop local stack

supabase status                  # Show status &#x26; credentials

# Database

supabase db reset                # Reset + migrations + seed

supabase db push                 # Push to remote

supabase db pull                 # Pull remote schema

supabase db diff -f <name>       # Generate migration from diff

supabase db lint                 # Check for issues

# Migrations

supabase migration new <name>    # Create migration

supabase migration list          # List migrations

supabase migration up            # Apply pending (remote)

# Functions

supabase functions new <name>    # Create function

supabase functions serve         # Local dev

supabase functions deploy        # Deploy all

# Types

supabase gen types typescript --local > types/database.ts

# Project

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

supabase projects list           # List projects

CI/CD Template

# .github/workflows/supabase.yml

name: Supabase CI/CD

on:

  push:

    branches: [main]

  pull_request:

env:

  SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}

  SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }}

  SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID }}

jobs:

  test:

    runs-on: ubuntu-latest

    steps:

      - uses: actions/checkout@v4

      - uses: supabase/setup-cli@v1

      - name: Start Supabase

        run: supabase start

      - name: Run migrations

        run: supabase db reset

      - name: Lint database

        run: supabase db lint

  deploy:

    needs: test

    if: github.ref == 'refs/heads/main'

    runs-on: ubuntu-latest

    steps:

      - uses: actions/checkout@v4

      - uses: supabase/setup-cli@v1

      - name: Link project

        run: supabase link --project-ref $SUPABASE_PROJECT_ID

      - name: Push migrations

        run: supabase db push

      - name: Deploy functions

        run: supabase functions deploy

Anti-Patterns

  • Direct production changes - Always use migrations
  • Disabled RLS - Enable on all user-data tables
  • Service key in client - Never expose service role key
  • No connection pooling - Use pooler for serverless
  • Committing .env - Add to .gitignore
  • Skipping migration review - Always check generated SQL
  • No seed data - Use seed.sql for consistent local dev
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card