From supabase-pack
Executes Supabase production checklist: enforces RLS on tables, key separation, connection pooling, backups, monitoring, Edge Functions, Storage policies, indexes, migrations. For prod deployments or audits.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin supabase-packThis skill is limited to using the following tools:
Actionable 14-step checklist for taking a Supabase project to production. Covers RLS enforcement, key separation, connection pooling (Supavisor), backups/PITR, network restrictions, custom domains, auth emails, rate limits, monitoring, Edge Functions, Storage policies, indexes, and migrations. Based on Supabase's official [production guide](https://supabase.com/docs/guides/deployment/going-into...
Deploys Supabase projects to production: database migrations, Edge Functions, secrets management, zero-downtime rollouts, blue/green branching, rollbacks, health checks.
Manages core Supabase workflows with CLI: local stack setup, migrations, RLS policies, Edge Functions, and deployment.
Provides Supabase best practices: verify current docs, enable RLS by default, security checklists for auth/JWT/sessions/storage, test implementations for Database/Auth/Edge Functions/Realtime.
Share bugs, ideas, or general feedback.
Actionable 14-step checklist for taking a Supabase project to production. Covers RLS enforcement, key separation, connection pooling (Supavisor), backups/PITR, network restrictions, custom domains, auth emails, rate limits, monitoring, Edge Functions, Storage policies, indexes, and migrations. Based on Supabase's official production guide.
@supabase/supabase-js v2+ installednpx supabase --version)RLS is the single most critical production requirement. Without it, any client with your anon key can read/write every row.
-- Audit: find tables WITHOUT RLS enabled
-- This query MUST return zero rows before going live
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public' AND rowsecurity = false;
-- Enable RLS on a table
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
-- Create a basic read policy (authenticated users see own rows)
CREATE POLICY "Users can view own profile"
ON public.profiles
FOR SELECT
USING (auth.uid() = user_id);
-- Create an insert policy
CREATE POLICY "Users can insert own profile"
ON public.profiles
FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Create an update policy
CREATE POLICY "Users can update own profile"
ON public.profiles
FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
USING (true) without intent (public read tables only)The anon key is safe for client-side code. The service_role key bypasses RLS entirely and must never leave server-side environments.
// Client-side — ONLY use anon key
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! // Safe for browsers
);
// Server-side only — service_role key (API routes, webhooks, cron jobs)
import { createClient } from '@supabase/supabase-js';
const supabaseAdmin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // NEVER expose to client
{ auth: { autoRefreshToken: false, persistSession: false } }
);
NEXT_PUBLIC_ prefix)grep -r "service_role" dist/)Supabase uses Supavisor for connection pooling. Serverless functions (Vercel, Netlify, Cloudflare Workers) MUST use the pooled connection string to avoid exhausting the database connection limit.
# Direct connection (migrations, admin tasks only)
postgresql://postgres:[PASSWORD]@db.[REF].supabase.co:5432/postgres
# Pooled connection via Supavisor (application code — USE THIS)
# Port 6543 = Supavisor pooler (vs 5432 direct)
postgresql://postgres.[REF]:[PASSWORD]@aws-0-us-east-1.pooler.supabase.com:6543/postgres
// For serverless environments — use pooled connection
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{
db: { schema: 'public' },
// Supavisor handles pooling at port 6543
// No need to configure pgBouncer settings in the client
}
);
transaction for serverless, session for long-lived connectionsSupabase provides automatic daily backups on Pro plan. Point-in-time recovery (PITR) enables granular restores.
supabase/migrations/ directory)npx supabase db push tested against a fresh project to verify migrations replay cleanlyRestrict database access to known IP addresses. This prevents unauthorized direct database connections even if credentials leak.
A custom domain replaces the default *.supabase.co URLs with your brand domain for API and auth endpoints.
Default Supabase auth emails show generic branding. Customize them so users see your domain and brand.
Supabase enforces rate limits that vary by plan. Hitting these in production causes 429 errors.
| Resource | Free | Pro | Team |
|---|---|---|---|
| API requests | 500/min | 1,000/min | 5,000/min |
| Auth emails | 4/hour | 30/hour | 100/hour |
| Realtime connections | 200 concurrent | 500 concurrent | 2,000 concurrent |
| Edge Function invocations | 500K/month | 2M/month | 5M/month |
| Storage bandwidth | 2GB/month | 250GB/month | Custom |
| Database size | 500MB | 8GB | 50GB |
Supabase provides built-in monitoring. Review these before launch to establish baselines.
// Health check endpoint — deploy this to your application
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
);
export async function GET() {
const start = Date.now();
const { data, error } = await supabase
.from('_health_check') // Create a small table for this
.select('id')
.limit(1);
const latency = Date.now() - start;
return Response.json({
status: error ? 'unhealthy' : 'healthy',
latency_ms: latency,
timestamp: new Date().toISOString(),
supabase_reachable: !error,
}, { status: error ? 503 : 200 });
}
Edge Functions run on Deno Deploy. Environment variables must be set via the Supabase CLI or Dashboard, not hardcoded.
# Set secrets for Edge Functions
npx supabase secrets set STRIPE_SECRET_KEY=sk_live_...
npx supabase secrets set RESEND_API_KEY=re_...
# List current secrets
npx supabase secrets list
# Deploy all Edge Functions
npx supabase functions deploy
# Deploy a specific function
npx supabase functions deploy process-webhook
// supabase/functions/process-webhook/index.ts
import { createClient } from '@supabase/supabase-js';
Deno.serve(async (req) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! // Available automatically
);
const body = await req.json();
// Process webhook payload...
return new Response(JSON.stringify({ received: true }), {
headers: { 'Content-Type': 'application/json' },
});
});
npx supabase functions deploy)npx supabase secrets set (not hardcoded)SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY available automatically (no need to set)npx supabase functions serve locally before deployingStorage buckets need explicit policies, similar to RLS on tables. Without policies, buckets are inaccessible (default deny).
-- Check storage bucket configurations
SELECT id, name, public, file_size_limit, allowed_mime_types
FROM storage.buckets;
-- Check existing storage policies
SELECT policyname, tablename, cmd, qual
FROM pg_policies
WHERE schemaname = 'storage';
-- Example: Allow authenticated users to upload to their own folder
CREATE POLICY "Users can upload own files"
ON storage.objects
FOR INSERT
WITH CHECK (
bucket_id = 'avatars'
AND auth.uid()::text = (storage.foldername(name))[1]
);
-- Example: Allow public read access to a bucket
CREATE POLICY "Public read access"
ON storage.objects
FOR SELECT
USING (bucket_id = 'public-assets');
file_size_limit in bucket config)allowed_mime_types)auth.uid() to prevent overwritesMissing indexes are the leading cause of slow queries after launch. Add indexes on foreign keys, filter columns, and sort columns.
-- Find missing indexes on foreign keys
SELECT
tc.table_name, kcu.column_name,
CASE WHEN i.indexname IS NULL THEN '** MISSING INDEX **' ELSE i.indexname END AS index_status
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
LEFT JOIN pg_indexes i
ON i.tablename = tc.table_name
AND i.indexdef LIKE '%' || kcu.column_name || '%'
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = 'public';
-- Find slow queries (requires pg_stat_statements extension)
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
SELECT query, calls, mean_exec_time::numeric(10,2) AS avg_ms,
total_exec_time::numeric(10,2) AS total_ms
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
-- Check table bloat (dead tuples from updates/deletes)
SELECT relname, n_live_tup, n_dead_tup,
round(n_dead_tup::numeric / greatest(n_live_tup, 1) * 100, 1) AS dead_pct
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC;
-- Create indexes on commonly filtered columns
CREATE INDEX idx_profiles_user_id ON public.profiles(user_id);
CREATE INDEX idx_orders_created_at ON public.orders(created_at DESC);
CREATE INDEX idx_posts_status ON public.posts(status) WHERE status = 'published'; -- Partial index
-- Set query timeout for the authenticated role
ALTER ROLE authenticated SET statement_timeout = '10s';
pg_stat_statements enabled for ongoing query monitoringstatement_timeout set for authenticated role to prevent runaway queriesnpx supabase db pushAll schema changes must go through migration files, never manual Dashboard edits in production.
# Generate a migration from local changes
npx supabase db diff --use-migra -f add_indexes
# Apply migrations to production (linked project)
npx supabase db push
# Verify migration history
npx supabase migration list
# If a migration fails, create a rollback
npx supabase migration new rollback_bad_change
supabase/migrations/ directory (version controlled)npx supabase db push tested against a fresh projectnpx supabase migration list)# Verify RLS status one final time
npx supabase inspect db table-sizes --linked
# Check that the project is linked to production
npx supabase status
# Verify connection string works
npx supabase db ping --linked
supabase-load-scale)npx supabase db push| Issue | Cause | Solution |
|---|---|---|
403 Forbidden on all API calls | RLS enabled but no policies created | Add SELECT/INSERT/UPDATE/DELETE policies for each role |
429 Too Many Requests | Plan rate limit exceeded | Upgrade plan or implement client-side backoff with retry |
| Connection timeout under load | Using direct connection in serverless | Switch to pooled connection string (port 6543) |
| Auth emails not delivered | Default SMTP rate-limited | Configure custom SMTP provider (SendGrid, Resend, Postmark) |
PGRST301 permission denied | Service role key used where anon expected | Check client initialization — use anon key for client-side |
| Edge Function cold starts | First invocation after idle period | Pre-warm with scheduled pings or accept ~200ms cold start |
| Storage upload fails | Missing bucket policy or size limit exceeded | Add INSERT policy and check file_size_limit on bucket |
| Slow queries after launch | Missing indexes on filter/join columns | Run Performance Advisor and add indexes per Step 12 |
| Migration conflicts | Manual Dashboard edits diverged from migration files | Run npx supabase db diff to capture drift, then commit |
// lib/supabase/client.ts — browser (anon key)
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
// lib/supabase/server.ts — server only (service role)
export const supabaseAdmin = createClient<Database>(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{ auth: { autoRefreshToken: false, persistSession: false } }
);
ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Public read published" ON public.posts
FOR SELECT USING (status = 'published');
CREATE POLICY "Authors manage own" ON public.posts
FOR ALL USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);
npx supabase migration new rollback_bad_change # Create reversal SQL
npx supabase db push # Apply rollback
# For data: Dashboard > Database > Backups > PITR
# For app: vercel rollback / netlify deploy --prod
For complete examples including health checks, storage policies, and Edge Functions, see examples.md.
@supabase/supabase-js Reference — Client SDK docssupabase-upgrade-migrationsupabase-load-scalesupabase-monitoringsupabase-edge-functions