Supabase en Express
Supabase es una base de datos Postgres gestionada con una capa de servicios encima: auth (usuarios, sesiones, providers OAuth), storage (archivos), y una API REST/realtime autogenerada sobre las tablas — todo accesible desde un único client JS.
Instalación
Sección titulada «Instalación»pnpm add @supabase/supabase-jsbun add @supabase/supabase-jsnpm install @supabase/supabase-jsConfiguración rápida — de cero a un endpoint funcionando
Sección titulada «Configuración rápida — de cero a un endpoint funcionando»1. Crear el proyecto en supabase.com y copiar la URL y las keys desde Project Settings → API.
2. Variables de entorno:
SUPABASE_URL=https://tuproyecto.supabase.coSUPABASE_SERVICE_ROLE_KEY=tu-service-role-keyUn backend Express, corriendo en un entorno confiable, usa la service role key (no la anon key que usaría un frontend) — se salta Row Level Security por completo, porque el servidor mismo es responsable de aplicar la autorización correcta antes de tocar los datos.
3. El client:
import { createClient } from "@supabase/supabase-js"
export const supabaseAdmin = createClient( process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY! // NUNCA exponer esta key al cliente)4. Un endpoint real:
import express from "express"import { supabaseAdmin } from "./lib/supabase"
const app = express()app.use(express.json())
app.get("/posts", async (req, res) => { const { data, error } = await supabaseAdmin.from("posts").select("*") if (error) return res.status(500).json({ error: "Error al leer posts" }) res.json(data)})
app.listen(3000)CRUD con la API autogenerada
Sección titulada «CRUD con la API autogenerada»const { data, error } = await supabaseAdmin .from('posts') .select('*') .eq('published', true) .order('created_at', { ascending: false });
const { data, error } = await supabaseAdmin .from('posts') .insert({ title: 'Nuevo post', author_id: userId }) .select() .single();
const { data, error } = await supabaseAdmin .from('posts') .update({ published: true }) .eq('id', postId);
const { error } = await supabaseAdmin.from('posts').delete().eq('id', postId);Cada llamada devuelve { data, error } en vez de lanzar — el chequeo de error reemplaza al try/catch.
Row Level Security (RLS)
Sección titulada «Row Level Security (RLS)»RLS son políticas SQL en Postgres que deciden, fila por fila, qué puede leer/escribir cada usuario — es lo que hace seguro exponer la ANON_KEY en un cliente (frontend). Sin una política explícita, una tabla con RLS activado rechaza todo por defecto.
create policy "Los usuarios ven sus propios posts"on posts for selectusing (auth.uid() = author_id);Con la service role key (como en este backend Express), RLS no aplica — el código del servidor es responsable de esa lógica, por ejemplo filtrando explícitamente:
app.get("/mis-posts", requireAuth, async (req, res) => { const { data, error } = await supabaseAdmin .from("posts") .select("*") .eq("author_id", req.user!.id) // filtro explícito — la service role no lo hace sola
if (error) return res.status(500).json({ error: "Error al leer" }) res.json(data)})Auth incluida (Supabase Auth)
Sección titulada «Auth incluida (Supabase Auth)»Si el login lo maneja Supabase directamente (en vez de JWT manual, ver JWT en Express, o better-auth/Auth.js):
const { data, error } = await supabaseAdmin.auth.signUp({ email, password });const { data, error } = await supabaseAdmin.auth.signInWithPassword({ email, password });Verificar el token que manda un frontend que ya hizo login con Supabase Auth:
app.get("/perfil", async (req, res) => { const token = req.headers.authorization?.replace("Bearer ", "") const { data: { user }, error } = await supabaseAdmin.auth.getUser(token)
if (error || !user) return res.status(401).json({ error: "No autenticado" }) res.json({ userId: user.id })})auth.uid() (usado en las políticas RLS de arriba) es justamente el id del usuario autenticado en la sesión actual — la auth y las políticas de datos están pensadas para trabajar juntas cuando no se usa la service role key.
Storage
Sección titulada «Storage»const { data, error } = await supabaseAdmin.storage.from('avatars').upload('user-1.png', buffer);const { data } = supabaseAdmin.storage.from('avatars').getPublicUrl('user-1.png');Capacidades de Supabase
Sección titulada «Capacidades de Supabase»| API | Qué hace |
|---|---|
createClient(url, serviceRoleKey) | Instancia el client del lado del servidor |
.from('tabla').select/insert/update/delete() | CRUD vía la API REST autogenerada |
{ data, error } | Cada llamada devuelve esto, no lanza |
RLS (create policy ...) | Controla qué fila puede tocar cada usuario, en la base misma — no aplica con service role |
supabase.auth.* | Registro, login, verificar sesión |
supabase.storage.* | Archivos |
Service role, RLS y ownership
Sección titulada «Service role, RLS y ownership»- Con la service role key, el servidor es responsable de toda la lógica de autorización que RLS normalmente haría — un bug aquí expone datos de cualquier usuario, no solo el propio.
SUPABASE_SERVICE_ROLE_KEYes tan sensible como cualquier secreto de base de datos — ver Variables de entorno en Node.- No es mutuamente excluyente con Prisma — es común usar Prisma para queries complejas con relaciones y tipos generados, y el client de Supabase solo para lo que Prisma no cubre (auth, storage, realtime). Ambos apuntan a la misma base Postgres.