Supabase Integration Guide

Technology: supabase · Category: database · Last reviewed: 2026-08-23

Source: https://tech-stack.codeamanilabs.org/guide/supabase

Insight:

Supabase is Postgres + Auth + Storage + Edge Functions in one. Its superpower is RLS — push authorization into the database so multi-tenant isolation holds even when app code is wrong. It also ships pgvector, so relational data and RAG embeddings can live in the same DB.

███████╗██╗   ██╗██████╗  █████╗ ██████╗  █████╗ ███████╗███████╗
██╔════╝██║   ██║██╔══██╗██╔══██╗██╔══██╗██╔══██╗██╔════╝██╔════╝
███████╗██║   ██║██████╔╝███████║██████╔╝███████║███████╗█████╗
╚════██║██║   ██║██╔═══╝ ██╔══██║██╔══██╗██╔══██║╚════██║██╔══╝
███████║╚██████╔╝██║     ██║  ██║██████╔╝██║  ██║███████║███████╗
╚══════╝ ╚═════╝ ╚═╝     ╚═╝  ╚═╝╚═════╝ ╚═╝  ╚═╝╚══════╝╚══════╝

Supabase Integration Guide

Focus: Managing Postgres databases, Edge Functions, Auth, and Storage from Claude Code using the official Supabase MCP server and supabase CLI.

Overview

Supabase is an open-source Firebase alternative built on Postgres. It provides a hosted database, authentication, real-time subscriptions, edge functions, storage, and a REST/GraphQL API. The official @supabase/mcp-server-supabase lets Claude Code execute SQL, manage branches, deploy edge functions, read logs, apply migrations, and generate TypeScript types — all through natural language.

Here is the big picture — everything centers on one Postgres database, which makes Supabase easy to reason about:

flowchart TD
    APP["Your app<br/>supabase-js client"] --> CORE["Supabase<br/>built on Postgres"]
    CLAUDE["Claude Code<br/>via MCP server + CLI"] --> CORE
    CORE --> DB["Postgres database<br/>+ RLS policies"]
    CORE --> AUTH["Auth<br/>auth.users"]
    CORE --> STORE["Storage<br/>buckets"]
    CORE --> RT["Realtime<br/>postgres_changes"]
    CORE --> FN["Edge Functions<br/>Deno"]

Official Documentation

Resource URL
Supabase Docs https://supabase.com/docs
Supabase MCP Server https://supabase.com/docs/guides/getting-started/mcp
CLI Reference https://supabase.com/docs/reference/cli
JavaScript Client https://supabase.com/docs/reference/javascript/introduction
API Keys (publishable / secret) https://supabase.com/docs/guides/api/api-keys
Edge Functions https://supabase.com/docs/guides/functions
Database Migrations https://supabase.com/docs/guides/deployment/database-migrations

MCP Server Setup

Official Supabase MCP Server (hosted HTTP + OAuth)

The current official server is the hosted HTTP endpoint at https://mcp.supabase.com/mcp. Your MCP client logs in to Supabase over OAuth on first connect — no personal access token needed for local interactive use. (The self-hostable @supabase/mcp-server-supabase npm package still exists for CI and custom endpoints; see the CI note below.)

# Add via Claude Code CLI (streamable HTTP transport)
claude mcp add --scope project --transport http supabase "https://mcp.supabase.com/mcp"

.mcp.json Configuration

{
  "mcpServers": {
    "supabase": {
      "type": "http",
      "url": "https://mcp.supabase.com/mcp?project_ref=<project-ref>&read_only=true&features=database,docs"
    }
  }
}

Configuration options are passed as URL query params on the endpoint:

Param Purpose
project_ref=<ref> Project scoping — restrict the server to a single project (drops account-level tools)
read_only=true Read-only mode — excludes every mutating tool (no execute_sql writes, no apply_migration, no deploy_edge_function)
features=database,docs,... Restrict to specific feature groups (account, database, debugging, development, functions, branching, storage, docs)

Security (codeAmani default): never point the MCP server at a production project. Scope it (project_ref), run it read-only unless you are actively applying changes, and prefer a development branch. The server executes SQL as an elevated role, so an injected instruction in your data could otherwise mutate real rows. See the MCP security best practices.

CI / headless / self-host: use a personal access token instead of OAuth. Pass it as Authorization: Bearer ${SUPABASE_ACCESS_TOKEN} to the hosted endpoint, or run the @supabase/mcp-server-supabase npm package locally with --access-token. When running Supabase locally via the CLI, a limited MCP server is served at http://localhost:54321/mcp. Generate a PAT at: https://supabase.com/dashboard/account/tokens

Available MCP Tools

Grouped by feature (the features param toggles whole groups). Read-only mode hides the mutating tools.

Group Tools
Account list_projects, get_project, create_project, pause_project, restore_project, list_organizations, get_organization, get_cost, confirm_cost
Database list_tables, list_extensions, list_migrations, apply_migration, execute_sql
Debugging query_logs, get_advisors
Development get_project_url, get_publishable_keys, generate_typescript_types
Functions list_edge_functions, get_edge_function, deploy_edge_function
Branching create_branch, list_branches, delete_branch, merge_branch, reset_branch, rebase_branch
Storage list_storage_buckets, get_storage_config, update_storage_config
Docs search_docs

Naming has changed since older guides: logs are now query_logs (not get_logs), get_advisors surfaces security/performance lints, search_docs queries the Supabase docs, and get_publishable_keys returns the new publishable API keys (see below). There is no longer a standalone describe_table_schema tool — list_tables returns column types and constraints.


CLI Integration

Installation

# npm (global)
npm install -g supabase

# macOS (brew)
brew install supabase/tap/supabase

# Windows (Scoop)
scoop bucket add supabase https://github.com/supabase/scoop-bucket.git
scoop install supabase

Authentication

supabase login
# Or set token:
export SUPABASE_ACCESS_TOKEN=sbp_...

Key Commands

# Link to an existing project
supabase link --project-ref your-project-ref

# Start local Supabase stack (Docker required)
supabase start

# Stop local stack
supabase stop

# Check local status
supabase status

# Database migrations
supabase migration new add_users_table
supabase db push                    # push migrations to linked project
supabase db pull                    # pull remote schema to local
supabase db reset                   # reset local DB and re-apply migrations
supabase db diff                    # show diff between local and remote

# Generate TypeScript types from schema
supabase gen types typescript --linked > src/types/database.ts

# Edge Functions
supabase functions new my-function
supabase functions serve my-function  # local dev
supabase functions deploy my-function --no-verify-jwt

# Storage (CLI v2 — these subcommands are behind --experimental)
supabase storage ls --experimental --linked
supabase storage cp ./file.pdf ss:///my-bucket/file.pdf --experimental --linked

# Logs
supabase logs --project-ref your-ref

Client SDK Integration

JavaScript / TypeScript

npm install @supabase/supabase-js
import { createClient } from "@supabase/supabase-js";
import type { Database } from "./types/database"; // generated types

const supabase = createClient<Database>(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_ANON_KEY!
);

// Query with full type safety
const { data, error } = await supabase
  .from("users")
  .select("id, email, created_at")
  .eq("active", true)
  .order("created_at", { ascending: false })
  .limit(10);

// Insert
const { error: insertError } = await supabase
  .from("posts")
  .insert({ title: "Hello", content: "World", user_id: userId });

// Real-time subscription
const channel = supabase
  .channel("db-changes")
  .on("postgres_changes", { event: "INSERT", schema: "public", table: "messages" }, (payload) => {
    console.log("New message:", payload.new);
  })
  .subscribe();

Environment Variables

New API keys (2025 → current). Supabase has replaced the JWT-based anon / service_role keys with publishable (sb_publishable_..., browser-safe) and secret (sb_secret_..., server-only) keys. The legacy JWT keys still work but are scheduled for deprecation by end of 2026 — new projects should adopt the new keys now. The two schemes run side by side, so you can migrate incrementally. Generate the new keys in Dashboard → Project Settings → API Keys.

# Client-side (safe to expose in the frontend)
NEXT_PUBLIC_SUPABASE_URL=https://your-ref.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_...   # NEW — browser-safe, RLS-respecting
# NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...                    # legacy JWT key (still valid; sunset end of 2026)

# Server-side only (never expose in the frontend)
SUPABASE_SECRET_KEY=sb_secret_...                          # NEW — bypasses RLS; replaces service_role
# SUPABASE_SERVICE_ROLE_KEY=eyJ...                         # legacy JWT key (still valid; sunset end of 2026)
SUPABASE_DB_PASSWORD=...

# Direct Postgres connection string (for migrations/scripts)
DATABASE_URL=postgresql://postgres:[password]@db.your-ref.supabase.co:5432/postgres
DIRECT_URL=postgresql://postgres:[password]@db.your-ref.supabase.co:5432/postgres

# CLI/MCP authentication (CI / headless only — interactive MCP uses OAuth)
SUPABASE_ACCESS_TOKEN=sbp_...

The publishable key is the browser client's key and still respects RLS (it is auth.uid() = NULL until a user signs in). The secret key carries the elevated, RLS-bypassing role — treat it exactly like the old service_role key: server-only, never committed, never shipped to the browser.


Automation Workflows

Claude Code Hook: Auto-generate Types After Migration

.claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -q 'supabase db push\\|migration'; then supabase gen types typescript --linked > src/types/database.ts && echo 'Types regenerated'; fi"
          }
        ]
      }
    ]
  }
}

Slash Command: Database Inspection

.claude/commands/db-inspect.md:

Inspect the Supabase database for table $ARGUMENTS.

1. Use the Supabase MCP tool `list_tables` to get the schema (column types and constraints) for table $ARGUMENTS
2. Use `execute_sql` to run: SELECT COUNT(*) FROM $ARGUMENTS
3. Use `execute_sql` to get a sample of 5 rows: SELECT * FROM $ARGUMENTS LIMIT 5
4. Report: column names/types, row count, sample data, and any missing indexes or constraints

Usage: /project:db-inspect users

Migration-Safe Database Changes

The example below enables RLS so users only see their own posts. Here is how that check plays out on every query — once it is in place, your authorization holds even if app code slips:

sequenceDiagram
    participant C as "Client"
    participant P as "Postgres + RLS"
    participant T as "posts table"
    C->>P: "select on posts as auth.uid"
    P->>P: "check policy auth.uid = user_id"
    alt "policy passes"
        P->>T: "read matching rows"
        T-->>C: "return user own posts"
    else "policy blocks"
        P-->>C: "return no rows"
    end
-- supabase/migrations/20250512000000_add_posts.sql
CREATE TABLE IF NOT EXISTS posts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  title TEXT NOT NULL CHECK (char_length(title) <= 200),
  content TEXT,
  published_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Enable Row Level Security
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Policy: users can only see their own posts
CREATE POLICY "users_own_posts" ON posts
  FOR ALL USING (auth.uid() = user_id);

-- Index for performance
CREATE INDEX IF NOT EXISTS posts_user_id_idx ON posts (user_id);
CREATE INDEX IF NOT EXISTS posts_published_at_idx ON posts (published_at DESC);

Apply: supabase db push or via MCP apply_migration.


Auth & sessions: how auth.uid() is populated

RLS policies like auth.uid() = user_id only work if a user JWT reaches Postgres. Here is the chain: a user signs in, Supabase Auth issues a JWT, and supabase-js sends it as the Authorization: Bearer header (or a session cookie in SSR). PostgREST decodes that JWT into the request's auth.uid() and auth.jwt(), which your policies then evaluate. The key you ship matters: the anon key (new name: publishable key, sb_publishable_...) is a public, RLS-respecting key safe for the browser — auth.uid() is NULL until a user signs in. The service_role key (new name: secret key, sb_secret_...) carries an elevated claim that bypasses RLS entirely, so it is server-only and treats every row as accessible.

sequenceDiagram
    participant B as "Browser<br/>anon key + user JWT"
    participant PR as "PostgREST"
    participant PG as "Postgres + RLS"
    B->>PR: "request with Bearer JWT"
    PR->>PG: "set role · auth.uid from JWT"
    PG->>PG: "evaluate policy auth.uid = user_id"
    PG-->>B: "only this user's rows"

On the server (Next.js App Router), use @supabase/ssr's createServerClient with the anon key plus cookie accessors so the user's session flows from cookies into queries — keeping RLS in force. Always verify identity with supabase.auth.getUser(), which contacts the Auth server to revalidate the JWT, never getSession(), which only reads cookies and returns an unverified user that a malicious client could spoof.

// lib/supabase-server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";

export async function createSupabaseServerClient() {
  const cookieStore = await cookies();

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, // anon key, NOT service_role
    {
      cookies: {
        getAll: () => cookieStore.getAll(),
        setAll: (cookiesToSet) => {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options),
            );
          } catch {
            // Called from a Server Component — cookie writes are handled by middleware.
          }
        },
      },
    },
  );
}

// In a Server Component / Route Handler:
const supabase = await createSupabaseServerClient();
const {
  data: { user },
  error,
} = await supabase.auth.getUser(); // verifies the JWT with the Auth server

if (!user) {
  // not authenticated — redirect or return 401
}
// Queries run as this user; auth.uid() now drives RLS automatically.
const { data: posts } = await supabase.from("posts").select("*");

Security gotcha: Never expose SUPABASE_SERVICE_ROLE_KEY to the client or use it in code that runs in the browser — it bypasses every RLS policy. Reserve it for trusted server-only admin scripts (cron jobs, webhooks). For user-facing server code, use the anon key + getUser() so RLS stays in effect.

Canonical docs: Server-Side Auth (Next.js) · Creating a server client


Common Use Cases

Use Case Approach
Schema changes supabase migration new + db push
Type generation supabase gen types typescript --linked
Edge Function deploy supabase functions deploy or MCP
Debug slow queries MCP execute_sql with EXPLAIN ANALYZE
Branch for feature dev MCP create_branch + merge_branch
Read production logs MCP query_logs

Troubleshooting

Issue Fix
supabase start fails Ensure Docker Desktop is running
Migration conflict Run supabase db pull to sync first
RLS blocking queries Use service_role key for admin scripts
Types out of sync Re-run supabase gen types typescript --linked
Connection refused Check supabase status — local stack may be stopped
PAT expired Regenerate at supabase.com/dashboard/account/tokens

Official docs: