Tutorial · Auth

Autenticación con Supabase en Next.js

IndiePack IndiePack
· · 12 min lectura

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

  1. Proyecto y variables de entorno
  2. Clientes de navegador y servidor
  3. Middleware: refrescar la sesión
  4. Registro e inicio de sesión
  5. Leer el usuario en Server Components
  6. Proteger rutas privadas
  7. Row Level Security: la clave
  8. Errores comunes
  9. Preguntas frecuentes
  10. Conclusión

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

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€ →