Skip to main content

Supabase Workflow

Supabase project workflow — auth, storage, edge functions, migrations, RLS, and MCP integration. Use when setting up Supabase in a project, writing migrations, configuring auth, or working with edge functions. Complements supabase-postgres-best-practices (query/schema optimization) with project-level workflow patterns.

Supabase Workflow

Project-level Supabase patterns for Framework projects. For Postgres query and schema optimization, see supabase-postgres-best-practices.

Locked Decision

Supabase is the Framework's infrastructure platform (database, auth, storage, edge functions). Do not substitute with Firebase, PlanetScale, Neon, or raw Postgres hosting.

MCP Integration

Supabase MCP is configured in .mcp.json (gitignored, copy from .mcp.json.example). Available tools:

Tool When to Use
execute_sql Ad-hoc queries, debugging, data inspection
apply_migration Schema changes (always use migrations, never raw DDL in production)
list_tables Explore existing schema
get_logs Debug edge functions, auth errors, API issues
list_extensions Check available Postgres extensions
deploy_edge_function Deploy serverless functions

Auth Patterns

Setup Checklist

  1. Enable desired auth providers in Supabase Dashboard > Auth > Providers
  2. Configure redirect URLs (localhost for dev, production domain for prod)
  3. Use @supabase/ssr for Next.js server-side auth (not @supabase/auth-helpers-nextjs — deprecated)
  4. Create middleware for session refresh (middleware.ts)
  5. Protect routes via middleware, not client-side checks

Client Creation

// lib/supabase/client.ts — browser client
import { createBrowserClient } from "@supabase/ssr";
export const createClient = () =>
  createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  );

// lib/supabase/server.ts — server client (RSC, Server Actions, Route Handlers)
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export const createClient = async () => {
  const cookieStore = await cookies();
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    { cookies: { getAll: () => cookieStore.getAll(), setAll: (cookiesToSet) => { cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options)); } } }
  );
};

Row-Level Security (RLS)

Every table exposed via the API MUST have RLS enabled. No exceptions.

-- Enable RLS
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;

-- Users can only read their own profile
CREATE POLICY "Users read own profile"
  ON public.profiles FOR SELECT
  USING (auth.uid() = user_id);

-- Users can only update their own profile
CREATE POLICY "Users update own profile"
  ON public.profiles FOR UPDATE
  USING (auth.uid() = user_id);

Common mistake: Creating a table without RLS. The API exposes it to all authenticated users by default. Always enable RLS immediately after creating a table.

Migrations

Workflow

  1. Create migration via MCP: apply_migration with descriptive name
  2. Write SQL in the migration file
  3. Test locally with supabase db reset (resets local DB, runs all migrations)
  4. Push to Supabase via supabase db push or branch deploy

Rules

  • One concern per migration — don't mix schema changes with data migrations
  • Always reversible — include a comment with the rollback SQL even if Supabase doesn't auto-rollback
  • Never modify existing migrations — create a new migration to fix issues
  • Use transactions — wrap multi-statement migrations in BEGIN; ... COMMIT;

Naming Convention

20260312143000_create_profiles_table.sql
20260312143100_add_avatar_to_profiles.sql

Edge Functions

When to Use

  • Webhook handlers (Stripe, Linear, GitHub)
  • Server-side logic that can't run in Next.js (long-running, needs Deno APIs)
  • Scheduled tasks (via pg_cron or external trigger)

When NOT to Use

  • Simple CRUD — use Next.js Server Actions + Supabase client
  • Auth logic — use Supabase Auth hooks
  • Real-time — use Supabase Realtime channels

Deploy via MCP

Use deploy_edge_function tool. Edge functions live in supabase/functions/.

Storage Buckets (rajababa-assets)

All Framework projects share a centralized Supabase project (rajababa-assets) for binary asset storage. Application-specific Supabase projects handle database, auth, and edge functions -- but all binary assets (images, diagrams, screenshots, avatars) go to rajababa-assets.

Bucket Configuration

Bucket Visibility RLS Policies Purpose
assets Private (default) INSERT, UPDATE, DELETE Linear attachments, Obsidian assets, internal diagrams
public Public INSERT, UPDATE, DELETE Internet-facing content (site images, presentations)

Path Conventions

Private assets bucket -- vendor-rooted paths:

linear/{ISSUE-ID}/filename.ext        # Linear issue attachments
obsidian/{path}/filename.ext           # Obsidian vault assets (avatars, images)

Public public bucket -- content-type-rooted paths:

sites/{site-name}/filename.ext         # Website assets
presentations/{slug}/filename.ext      # Presentation materials
assets/{category}/filename.ext         # General public assets

RLS Details

  • Path regex -- ^[a-z][a-z0-9-]*/.+$ (any valid vendor prefix, open pattern)
  • Operations gated -- INSERT, UPDATE, DELETE require a valid anon key via RLS
  • SELECT on assets -- Requires signed URL (private by default)
  • SELECT on public -- Open read access (public bucket)

Agent Upload Decision Tree

When an agent needs to store a binary asset, follow this flow:

  1. Is the asset referenced only in Linear issues, Obsidian, or internal docs?

    • Yes: Upload to assets bucket (private). Use vendor-rooted path (linear/... or obsidian/...).
  2. Does the asset need unauthenticated internet access (embedded on a website, in a public presentation, shared via direct link)?

    • Yes: Upload to public bucket. Use content-type-rooted path.
    • No: Default to assets bucket.
  3. Is it an SVG?

    • Allowed in both buckets. XSS is mitigated by Supabase domain isolation (SVGs served from a separate domain, not the application domain).
  4. Is it a text/code file?

    • Text and code belong in git, not Supabase Storage. Only binary assets go to storage.

Signed URLs (Private Bucket)

Generate a signed URL with a 1-hour expiry for private assets:

curl -s -X POST \
  "${SUPABASE_URL}/storage/v1/object/sign/assets/<vendor>/<path>/<filename>" \
  -H "Authorization: Bearer ${SUPABASE_ANON_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"expiresIn": 3600}'

Linear: Signed URLs work because Linear auto-proxies external images to uploads.linear.app on embed, making the image permanent regardless of URL expiry.

Obsidian: Download the file locally via signed URL, then reference as a local vault file path.

Upload Pattern

# CLI upload (private bucket)
curl -s -X POST \
  "${SUPABASE_URL}/storage/v1/object/assets/linear/${ISSUE_ID}/<filename>" \
  -H "Authorization: Bearer ${SUPABASE_ANON_KEY}" \
  -H "Content-Type: image/png" \
  -H "x-upsert: true" \
  --data-binary @<local-file-path>
// TypeScript upload (private bucket)
const { data, error } = await supabase.storage
  .from("assets")
  .upload(`linear/${issueId}/screenshot.png`, file, {
    upsert: true,
    contentType: "image/png",
  });

Security Boundaries

  • Anon key -- Safe to embed in agent scripts and CLI tools. It only enables RLS-gated operations; it cannot bypass row-level security or access data outside policy rules.
  • Service role key -- Bypasses RLS entirely. Never use for storage uploads from agents or client code. Reserved for server-side admin operations only.
  • SVG domain isolation -- Supabase serves uploaded SVGs from a separate domain (*.supabase.co storage domain, not the application domain). This prevents XSS attacks from SVG content affecting the application.

Configuration

Credentials for rajababa-assets live in ~/.claude/supabase-assets.json, symlinked into each repo's .claude/ directory:

{
  "supabaseUrl": "https://<project-ref>.supabase.co",
  "supabaseAnonKey": "eyJ..."
}

This replaces the old per-project .claude/supabase.json pattern. All repos point to the same centralized storage project.

Environment Variables

Required in every project using Supabase:

NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...  # Server-only, never expose to client

Critical: SUPABASE_SERVICE_ROLE_KEY bypasses RLS. Never import it in client code. Only use in Server Actions, Route Handlers, or Edge Functions.

Monitoring

Use get_logs MCP tool to inspect:

  • Auth logs — failed logins, token refresh errors
  • API logs — slow queries, RLS policy denials
  • Edge function logs — runtime errors, cold starts

For production monitoring, Sentry captures client-side errors and Axiom captures structured server logs (see Locked Decisions).

Search Framework Explorer

Search agents, skills, and standards