Supabase en Producción: La Arquitectura que el 90% de Developers Ignora
Supabase production architecture guide: Row Level Security, Realtime, Edge Functions, and Next.js integration patterns that actually scale.
La Mayoría Usa Supabase Como si Fuera un Google Sheets con API
Conectas la base de datos. Haces un `select('*')` desde el cliente. Y llamas eso producción.
Es el error más común — y el más caro en términos de seguridad y rendimiento.
*El verdadero Supabase no es tu base de datos. Es tu backend completo.*
Postgres + autenticación + Realtime + Storage + Edge Functions. Todo integrado. Todo tipado. Todo con RLS nativo.
Si solo usas Supabase para hacer queries desde el cliente sin Row Level Security activado, estás exponiendo tu base de datos entera a cualquier usuario autenticado.
Este artículo te muestra la arquitectura correcta.
---
El Error Arquitectónico que Destruye Proyectos en Producción
El patrón malo es sorprendentemente común:
❌ Approach incorrecto:
```typescript
// Desde el cliente — NUNCA hagas esto sin RLS
const { data } = await supabase
.from('users')
.select('*')
// Cualquier usuario autenticado ve todos los registros
// No hay filtro por user_id
// Datos de otros usuarios expuestos
```
✅ Approach correcto con RLS:
```sql
-- En Supabase SQL Editor
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
CREATE POLICY "users_own_data" ON users
FOR ALL
USING (auth.uid() = user_id);
```
```typescript
// Ahora la query es segura por defecto
const { data } = await supabase
.from('users')
.select('*')
// Supabase filtra automáticamente por auth.uid()
// El usuario solo ve sus propios registros
```
Row Level Security es la característica más importante de Supabase. Y la más ignorada.
No es opcional. No es una optimización. Es la diferencia entre una app segura y una brecha de datos.
---
La Arquitectura de Autenticación que Realmente Funciona
La mayoría implementa auth de Supabase como si fuera Firebase Auth: login, logout, y poco más.
*El auth real de Supabase es un sistema de identidad completo con JWT custom claims.*
Paso 1: Configura el cliente con tipado completo
```typescript
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
import { Database } from './database.types' // generado con supabase gen types
export const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
```
Paso 2: Genera los tipos automáticamente
```bash
npx supabase gen types typescript --project-id tu-project-id > lib/database.types.ts
```
Esto es crítico. Con los tipos generados, TypeScript te avisa en compile time cuando una query es incorrecta.
Paso 3: Server-side auth en Next.js con SSR
```typescript
// app/api/protected/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
export async function GET() {
const supabase = createRouteHandlerClient({ cookies })
const { data: { session } } = await supabase.auth.getSession()
if (!session) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
// El usuario está autenticado — la query respeta RLS automáticamente
const { data } = await supabase
.from('posts')
.select('id, title, created_at')
.order('created_at', { ascending: false })
return Response.json(data)
}
```
El patrón de `createRouteHandlerClient` vs `createClient` no es un detalle menor. Es la diferencia entre auth que funciona en producción y auth que falla silenciosamente con SSR.
---
Realtime: El Caso de Uso Correcto (y los Incorrectos)
Supabase Realtime es potente. También es el origen del 80% de los problemas de rendimiento en apps de producción.
El real problema del Realtime no es técnico. Es que los developers lo usan para todo cuando debería usarse para poco.
❌ Cuándo NO usar Realtime:
→ Dashboards con datos que cambian cada hora
→ Perfiles de usuario que el propio usuario edita
→ Listas de productos en un e-commerce
→ Cualquier cosa que no necesite actualización en tiempo real
✅ Cuándo SÍ usar Realtime:
→ Chat y mensajería
→ Notificaciones push en tiempo real
→ Colaboración simultánea (tipo Figma o Notion)
→ Indicadores de presencia ("3 usuarios leyendo esto")
```typescript
// Implementación correcta de Realtime — acotada a lo necesario
useEffect(() => {
const channel = supabase
.channel('room-messages')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: `room_id=eq.${roomId}` // SIEMPRE filtra — nunca escuches toda la tabla
},
(payload) => {
setMessages(prev => [...prev, payload.new as Message])
}
)
.subscribe()
return () => {
supabase.removeChannel(channel) // Limpia siempre en el cleanup
}
}, [roomId])
```
El `filter` en Realtime no es opcional. Sin filtro, cada INSERT en la tabla dispara el listener para todos los usuarios conectados. Eso es un problema de rendimiento garantizado.
---
Edge Functions: Tu Backend Sin Servidor
Las Supabase Edge Functions corren en Deno. No en Node.js. Ese detalle importa.
El caso de uso correcto: lógica que no debe ejecutarse en el cliente y no necesita un servidor dedicado.
```typescript
// supabase/functions/send-welcome-email/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (req) => {
const { user_id } = await req.json()
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! // Service role para bypass de RLS
)
const { data: user } = await supabase
.from('profiles')
.select('email, name')
.eq('id', user_id)
.single()
// Envía el email con Resend u otro provider
await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${Deno.env.get('RESEND_API_KEY')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
from: 'no-reply@tuapp.com',
to: user.email,
subject: `Bienvenido, ${user.name}`,
html: '<p>Gracias por registrarte.</p>'
})
})
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' }
})
})
```
Nota el uso de `SUPABASE_SERVICE_ROLE_KEY` en las Edge Functions. Esta key bypassa RLS intencionalmente — úsala solo en server-side, nunca en el cliente.
---
Optimización de Queries: Postgres es tu Ventaja
El real Supabase no es Firebase con SQL. Es Postgres completo.
Eso significa que puedes usar todo lo que Postgres ofrece: índices parciales, funciones, triggers, views materializadas.
```sql
-- Índice parcial para queries frecuentes
CREATE INDEX idx_posts_published
ON posts (created_at DESC)
WHERE published = true;
-- View materializada para dashboards
CREATE MATERIALIZED VIEW user_stats AS
SELECT
user_id,
COUNT(*) as total_posts,
MAX(created_at) as last_post_at
FROM posts
GROUP BY user_id;
-- Refresca la view cuando necesites
REFRESH MATERIALIZED VIEW user_stats;
```
La mayoría de developers que usan Supabase nunca toca SQL directo. Dejan rendimiento enorme sobre la mesa.
---
El Stack Completo: Cómo Conectar Todo
Supabase funciona mejor como núcleo de un stack específico:
→ Next.js 15+ para el frontend con App Router
→ Supabase Auth para autenticación con OAuth y magic links
→ Supabase Storage para archivos con políticas RLS nativas
→ Supabase Edge Functions para webhooks y lógica server-side
→ Resend para emails transaccionales desde Edge Functions
→ Supabase Realtime solo donde necesites updates en tiempo real
Este stack elimina la necesidad de un backend separado en el 90% de los proyectos SaaS.
---
Takeaways Clave
RLS no es opcional. Actívalo en cada tabla desde el primer día.
Genera los tipos con `supabase gen types`. TypeScript te salva de errores en producción antes de que lleguen a producción.
Usa `createRouteHandlerClient` en Next.js. El cliente genérico no funciona correctamente con SSR.
Filtra siempre en Realtime. Un listener sin filtro en una tabla grande destruye el rendimiento.
`SUPABASE_SERVICE_ROLE_KEY` solo en server-side. Nunca en el cliente. Nunca en variables `NEXT_PUBLIC_`.
Aprovecha Postgres completo. Índices, triggers, views materializadas. Eso es lo que te diferencia de Firebase.
*Supabase no es una base de datos en la nube. Es el backend completo que no tienes que construir.*
La diferencia entre un proyecto que escala y uno que colapsa bajo carga está en estos patrones. No en el framework. No en el hosting. En cómo usas las herramientas que ya tienes.
Lee el artículo completo en brianmenagomez.com


