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
- Enable desired auth providers in Supabase Dashboard > Auth > Providers
- Configure redirect URLs (localhost for dev, production domain for prod)
- Use
@supabase/ssrfor Next.js server-side auth (not@supabase/auth-helpers-nextjs— deprecated) - Create middleware for session refresh (
middleware.ts) - 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
- Create migration via MCP:
apply_migrationwith descriptive name - Write SQL in the migration file
- Test locally with
supabase db reset(resets local DB, runs all migrations) - Push to Supabase via
supabase db pushor 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:
Is the asset referenced only in Linear issues, Obsidian, or internal docs?
- Yes: Upload to
assetsbucket (private). Use vendor-rooted path (linear/...orobsidian/...).
- Yes: Upload to
Does the asset need unauthenticated internet access (embedded on a website, in a public presentation, shared via direct link)?
- Yes: Upload to
publicbucket. Use content-type-rooted path. - No: Default to
assetsbucket.
- Yes: Upload to
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).
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.costorage 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).