Codea Bien Logo
From MVP to Production: Supabase in the Real World
Supabase

From MVP to Production: Supabase in the Real World

Kevin Dávila

Kevin Dávila

supabase productionsupabase scalingsupabase best practicesdatabase optimizationsupabase security

The day your MVP shows up on Product Hunt unannounced is the day you find out which parts of your stack were toys. Database connections run out, the query that worked with 10 users chokes on 1,000, and the RLS policy that "was fine as it was" becomes your worst nightmare. All at the same time, with real traffic.

The good part: almost everything can be fixed before launch. In this post you get the lessons from taking Supabase to real production: connection pooling, indexes, security, and scaling.

Connection pooling

Supabase limits simultaneous connections to PostgreSQL. For production, you need pooling.

Use Supavisor

Supabase includes Supavisor, their connection pooler. Configure your client to use it:

// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js'

// For normal queries (uses pooler)
export const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_ANON_KEY!
)

// For operations that need direct connections (migrations, etc.)
export const supabaseDirect = createClient(
  process.env.SUPABASE_URL!.replace('.supabase.co', '-pooler.supabase.co'),
  process.env.SUPABASE_ANON_KEY!
)

Dashboard configuration

Go to Settings > Database > Connection pooling:

  • Mode: Transaction (recommended for most cases)

  • Pool size: 15-25 (free tier), 50+ (pro)

  • Default timeout: 10 seconds

Performance

Indexes

Create indexes on columns you filter frequently:

-- Index on user_id (very common)
CREATE INDEX idx_tasks_user_id ON tasks(user_id);

-- Composite index for common queries
CREATE INDEX idx_tasks_user_completed ON tasks(user_id, completed);

-- Index for date sorting
CREATE INDEX idx_tasks_created_at ON tasks(created_at DESC);

Analyze slow queries

-- View slow queries
SELECT * FROM pg_stat_statements 
ORDER BY mean_exec_time DESC 
LIMIT 10;

-- Analyze a specific query
EXPLAIN ANALYZE 
SELECT * FROM tasks 
WHERE user_id = 'uuid' 
AND completed = false 
ORDER BY created_at DESC;

Pagination

Don't fetch all data at once:

// src/pagination.ts
const PAGE_SIZE = 20

export async function getTasks(page: number = 1) {
  const from = (page - 1) * PAGE_SIZE
  const to = from + PAGE_SIZE - 1

  const { data, error, count } = await supabase
    .from('tasks')
    .select('*', { count: 'exact' })
    .range(from, to)
    .order('created_at', { ascending: false })

  return {
    data,
    total: count,
    page,
    totalPages: Math.ceil((count || 0) / PAGE_SIZE)
  }
}

Production security

Review RLS policies

Before going to production, audit your policies:

-- View all policies
SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual 
FROM pg_policies 
WHERE schemaname = 'public';

Service Role Key

Never expose the service role key on the client. Keep it in server environment variables:

// src/lib/supabase-admin.ts (SERVER ONLY)
import { createClient } from '@supabase/supabase-js'

export const supabaseAdmin = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

Rate limiting

Supabase doesn't have built-in rate limiting. Implement in your Edge Function or API:

// supabase/functions/api/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'

const rateLimit = new Map<string, { count: number; reset: number }>()

serve(async (req) => {
  const ip = req.headers.get('x-forwarded-for') || 'unknown'
  const now = Date.now()
  const limit = rateLimit.get(ip)

  if (limit && limit.count > 100 && now < limit.reset) {
    return new Response('Rate limit exceeded', { status: 429 })
  }

  if (!limit || now > limit.reset) {
    rateLimit.set(ip, { count: 1, reset: now + 60000 })
  } else {
    limit.count++
  }

  // Your logic here
})

Migrations

Supabase CLI

// Create migration
supabase migration new add_tasks_table

// Apply migrations locally
supabase db reset

// Apply to production
supabase db push

Migration structure

supabase/
  migrations/
    20240101000000_create_users.sql
    20240102000000_create_tasks.sql
    20240103000000_add_rls_policies.sql

Example migration

-- supabase/migrations/20240102000000_create_tasks.sql
CREATE TABLE tasks (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
  title TEXT NOT NULL,
  completed BOOLEAN DEFAULT false,
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);

-- Enable RLS
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;

-- Policies
CREATE POLICY "Users can view own tasks" ON tasks FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own tasks" ON tasks FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own tasks" ON tasks FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "Users can delete own tasks" ON tasks FOR DELETE USING (auth.uid() = user_id);

-- Indexes
CREATE INDEX idx_tasks_user_id ON tasks(user_id);
CREATE INDEX idx_tasks_created_at ON tasks(created_at DESC);

Monitoring

Logs in the dashboard

Go to Logs to see:

  • API logs

  • Auth logs

  • Database logs

  • Edge Function logs

Important metrics

Monitor:

  • Active connections: Should not reach the limit

  • Query duration: Queries > 100ms need optimization

  • Error rate: Increase indicates problems

  • Storage usage: Don't exceed the plan

Alerts

Configure alerts in your hosting provider (Vercel, Netlify, etc.) for:

  • 500 errors

  • High latency

  • Service downtime

Backups

Supabase backups

  • Free tier: Daily backups, 7-day retention

  • Pro: Daily backups, 28-day retention

  • Team/Enterprise: Point-in-time recovery

Manual backup

// Backup database
supabase db dump > backup.sql

// Restore
psql -h db.xxx.supabase.co -U postgres -d postgres < backup.sql

Costs

Free tier limits

Resource

Limit

Database

500 MB

Storage

1 GB

Auth users

50,000

Bandwidth

500 MB

Edge Functions

500K invocations

Optimize costs

  1. Database: Use indexes, optimize queries, archive old data

  2. Storage: Compress images, use CDN, delete unused files

  3. Bandwidth: Use image transformations, cache responses

  4. Edge Functions: Reduce invocations, use batch operations

When to upgrade

  • Pro ($25/month): When you need more than 500MB DB or better backups

  • Team ($599/month): When you need SOC2, HIPAA, or priority support

Production checklist

Before launching:

  • [ ] RLS enabled on all tables

  • [ ] Service role key only on server

  • [ ] Indexes on frequently queried columns

  • [ ] Connection pooling configured

  • [ ] Versioned migrations

  • [ ] Backups configured

  • [ ] Monitoring active

  • [ ] Rate limiting implemented

  • [ ] Solid error handling

  • [ ] Centralized logs

What's next

In the next (and final) article we cover Supabase for Mobile: how to use Supabase with Flutter and React Native for native apps.