La autenticación es lo primero que necesitas en cualquier SaaS y lo que más gente atasca en Next.js — sobre todo desde que llegó el App Router y la sesión vive en cookies de servidor. Esta guía, en castellano y con código real, te lleva de cero a un sistema de auth completo con Supabase: registro, login, sesión en Server Components, middleware y, lo más importante, datos protegidos con Row Level Security.
✦ Antes de empezar
Olvida los tutoriales viejos que usan @supabase/auth-helpers-nextjs: está obsoleto. El paquete actual es @supabase/ssr, que gestiona la sesión por cookies y funciona igual en server y en cliente. Todo lo de abajo usa el enfoque nuevo.
Índice
Proyecto y variables
Crea un proyecto en Supabase, copia la URL y la anon key y instala los paquetes:
npm install @supabase/supabase-js @supabase/ssr
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
Clientes de navegador y servidor
Necesitas dos clientes: uno para Client Components (navegador) y otro para el servidor (Server Components, Route Handlers, Server Actions) que lee y escribe la sesión en cookies.
// lib/supabase/client.ts (navegador)
import { createBrowserClient } from "@supabase/ssr";
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
// lib/supabase/server.ts (servidor)
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return cookieStore.getAll(); },
setAll(list) {
try { list.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)); } catch {}
},
},
}
);
}
Middleware: refrescar la sesión
Sin middleware, la sesión caduca y el usuario se desloguea solo al navegar. El middleware la refresca en cada petición. No es opcional.
// middleware.ts
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function middleware(req: NextRequest) {
let res = NextResponse.next({ request: req });
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return req.cookies.getAll(); },
setAll(list) {
list.forEach(({ name, value, options }) =>
res.cookies.set(name, value, options));
},
},
}
);
await supabase.auth.getUser(); // refresca el token
return res;
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
Auth + RLS ya montados.
IndiePack trae esto preconfigurado: clientes, middleware, login social y políticas RLS de ejemplo. En castellano.
Ver el stack →Registro e inicio de sesión
Desde una Server Action — registro con email y contraseña:
// app/auth/actions.ts
"use server";
import { createClient } from "@/lib/supabase/server";
import { redirect } from "next/navigation";
export async function signUp(formData: FormData) {
const supabase = await createClient();
const { error } = await supabase.auth.signUp({
email: String(formData.get("email")),
password: String(formData.get("password")),
});
if (error) return redirect("/registro?error=" + error.message);
redirect("/dashboard");
}
export async function signIn(formData: FormData) {
const supabase = await createClient();
const { error } = await supabase.auth.signInWithPassword({
email: String(formData.get("email")),
password: String(formData.get("password")),
});
if (error) return redirect("/login?error=credenciales");
redirect("/dashboard");
}
¿Quieres login con Google o GitHub? Cambia a signInWithOAuth({ provider: "google" }) y maneja el callback en una Route Handler. Magic link: signInWithOtp. Mismo patrón.
Leer el usuario en Server Components
// app/dashboard/page.tsx (Server Component)
import { createClient } from "@/lib/supabase/server";
import { redirect } from "next/navigation";
export default async function Dashboard() {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) redirect("/login");
return <h1>Hola, {user.email}</h1>;
}
✦ Importante
Usa siempre supabase.auth.getUser() en el servidor, no getSession(). getUser() valida el token contra Supabase; getSession() se fía de la cookie sin verificar y no debe usarse para decisiones de seguridad.
Proteger rutas privadas
Dos capas: el middleware puede redirigir si no hay usuario, y cada página privada comprueba getUser() como red de seguridad. No confíes en una sola capa: la del servidor (la página) es la que de verdad protege.
Row Level Security: la clave
Aquí está lo que separa una app segura de una con un agujero. La auth identifica al usuario; RLS controla qué filas puede ver. Y se aplica en la base de datos, así que protege aunque tu API tenga un bug.
-- Activa RLS en la tabla
alter table proyectos enable row level security;
-- Cada usuario solo ve y edita SUS proyectos
create policy "propios proyectos"
on proyectos for all
using ( auth.uid() = user_id )
with check ( auth.uid() = user_id );
Con esto, un select * from proyectos desde la sesión de un usuario solo devuelve sus filas. No tienes que filtrar por user_id en cada query: la base de datos lo hace por ti. Por qué Postgres + RLS gana para SaaS lo explicamos en Supabase vs Firebase.
Errores comunes
- Olvidar el middleware. La sesión caduca y el usuario se desloguea solo. Pásalo siempre.
- Usar
getSession()para seguridad. No valida el token. UsagetUser()en el servidor. - No activar RLS. Con RLS desactivada, cualquiera con la anon key lee toda la tabla. Actívala en cada tabla.
- Usar la
service_rolekey en el cliente. Nunca. Esa clave salta RLS — solo en el servidor y nunca expuesta. - Confiar solo en el frontend. Ocultar un botón no es seguridad. La protección real está en RLS + checks de servidor.
Preguntas frecuentes
¿Qué paquete uso en App Router?
@supabase/ssr con @supabase/supabase-js. El antiguo auth-helpers está obsoleto.
¿Sirve RLS para todo?
Para acceso a datos, sí, es la base. Para acciones sensibles (pagos, cambios de rol), añade validación en el servidor además de RLS.
¿Cómo hago login con Google?
signInWithOAuth({ provider: "google" }) y un Route Handler en /auth/callback que intercambia el código por sesión.
Conclusión
Auth con Supabase en Next.js App Router son cinco piezas: clientes server/cliente con @supabase/ssr, middleware que refresca la sesión, Server Actions para registro/login, getUser() para leer al usuario, y RLS para que los datos estén seguros en la base de datos. Hazlo bien una vez y lo reutilizas en cada proyecto.
O empieza con todo montado: IndiePack trae auth completa, middleware, login social y políticas RLS de ejemplo ya configuradas — en castellano y por un pago único.
Login resuelto desde el primer commit.
Auth, sesión segura y RLS preconfigurados. Tú construyes el producto, no la fontanería.
Conseguir IndiePack — 250€ →