Ir al contenido
angel.library

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.

Ventana de terminal
pnpm add @supabase/supabase-js

Configuració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:

.env
SUPABASE_URL=https://tuproyecto.supabase.co
SUPABASE_SERVICE_ROLE_KEY=tu-service-role-key

Un 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:

lib/supabase.ts
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:

app.ts
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)
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.

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 select
using (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)
})

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.

const { data, error } = await supabaseAdmin.storage.from('avatars').upload('user-1.png', buffer);
const { data } = supabaseAdmin.storage.from('avatars').getPublicUrl('user-1.png');
APIQué 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
  • 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_KEY es 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.