Supabase + Express
Contexto
Sección titulada «Contexto»Escenario típico: el frontend (una SPA separada) usa supabase.auth directo para login, y Express es la API que sirve datos — necesita verificar el token que el frontend le manda, y leer/escribir en las tablas con permisos elevados.
Paso 1: instalar y configurar
Sección titulada «Paso 1: instalar y configurar»pnpm add express @supabase/supabase-jsbun add express @supabase/supabase-jsnpm install express @supabase/supabase-jsSUPABASE_URL=https://tuproyecto.supabase.coSUPABASE_SERVICE_ROLE_KEY=tu-service-role-keyimport { createClient } from "@supabase/supabase-js"
export const supabaseAdmin = createClient( process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!)Paso 2: middleware que verifica el token del frontend
Sección titulada «Paso 2: middleware que verifica el token del frontend»import type { NextFunction, Request, Response } from "express"import { supabaseAdmin } from "../lib/supabase"
declare global { namespace Express { interface Request { user?: { id: string; email?: string } } }}
export async function requireAuth( req: Request, res: Response, next: NextFunction) { const token = req.headers.authorization?.replace("Bearer ", "") if (!token) return res.status(401).json({ error: "No autenticado" })
const { data: { user }, error } = await supabaseAdmin.auth.getUser(token) if (error || !user) return res.status(401).json({ error: "Token inválido" })
req.user = { id: user.id, email: user.email } next()}Paso 3: endpoint que usa el user.id para filtrar datos propios
Sección titulada «Paso 3: endpoint que usa el user.id para filtrar datos propios»import express from "express"import { supabaseAdmin } from "./lib/supabase"import { requireAuth } from "./middlewares/requireAuth"
const app = express()app.use(express.json())
app.get("/mis-posts", requireAuth, async (req, res) => { const { data, error } = await supabaseAdmin .from("posts") .select("*") .eq("author_id", req.user!.id) // filtra explícito, no depende de RLS aquí (service role la salta)
if (error) return res.status(500).json({ error: "Error al leer posts" }) res.json(data)})
app.post("/posts", requireAuth, async (req, res) => { const { data, error } = await supabaseAdmin .from("posts") .insert({ title: req.body.title, author_id: req.user!.id }) .select() .single()
if (error) return res.status(500).json({ error: "Error al crear post" }) res.status(201).json(data)})Por qué el filtro .eq('author_id', ...) es explícito aquí
Sección titulada «Por qué el filtro .eq('author_id', ...) es explícito aquí»Con la SERVICE_ROLE_KEY, RLS no aplica — así que a diferencia de un cliente frontend con la ANON_KEY (donde RLS filtraría solo por confiar en las políticas), aquí el propio código de Express es responsable de esa lógica. Olvidar el .eq() expondría posts de cualquier usuario, no solo del autenticado.
Consideraciones
Sección titulada «Consideraciones»- Este patrón (frontend hace login directo contra Supabase, backend solo verifica el token) es común cuando Supabase Auth ya resuelve todo el login/registro y Express solo necesita saber “quién es” para aplicar su propia lógica.
- Alternativa: si Express también maneja el login (en vez del frontend), usar JWT manual o better-auth en su lugar, y Supabase solo como base de datos (sin usar
supabase.authen absoluto).