Qué es Supabase y por qué elegirlo
Supabase es una alternativa open-source a Firebase construida sobre PostgreSQL. A diferencia de Firebase, que usa una base de datos NoSQL propietaria, Supabase te da el poder completo de una base de datos relacional con todas las ventajas de un Backend-as-a-Service: autenticación, almacenamiento de archivos, funciones edge, suscripciones en tiempo real y un panel de administración intuitivo.
Lo que hace especial a Supabase es que no reinventa la rueda. Usa tecnologías probadas: PostgreSQL para la base de datos, GoTrue para autenticación, PostgREST para la API REST automática, y Realtime para suscripciones WebSocket. Esto significa que tu conocimiento de SQL es directamente aplicable, y si algún día decides migrar, tus datos están en una base de datos PostgreSQL estándar.
Combinado con Next.js 15 y el App Router, Supabase permite construir aplicaciones fullstack con Server Components, Server Actions, autenticación basada en cookies, y tipado end-to-end, todo con una experiencia de desarrollo excepcional.
Crear un proyecto en Supabase
El primer paso es crear una cuenta en supabase.com y crear un nuevo proyecto. Supabase ofrece un tier gratuito generoso que incluye dos proyectos gratis, 500MB de base de datos, 1GB de almacenamiento de archivos y 50.000 usuarios autenticados.
Una vez creado el proyecto, necesitas dos credenciales clave que encontrarás en Settings > API:
- Project URL: La URL base de tu proyecto, por ejemplo
https://abcdefghij.supabase.co - Anon Key: Una clave pública segura para usar en el cliente. Esta clave solo permite acceso a datos que las políticas RLS permitan.
- Service Role Key: Una clave privada con acceso total a la base de datos, sin restricciones RLS. Nunca la expongas en el cliente.
Añade estas credenciales a tu archivo .env.local:
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://abcdefghij.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Importante: La
SUPABASE_SERVICE_ROLE_KEYnunca debe llevar el prefijoNEXT_PUBLIC_. Solo debe usarse en el servidor (Server Components, Server Actions, Route Handlers).
Instalación de dependencias
Supabase proporciona dos paquetes principales para Next.js: el cliente base y el paquete SSR que maneja cookies y sesiones automáticamente con el App Router:
npm install @supabase/supabase-js @supabase/ssr
El paquete @supabase/ssr es esencial para Next.js 15 porque gestiona la autenticación a través de cookies HTTP-only, lo que permite que tanto Server Components como Client Components accedan a la sesión del usuario de forma segura.
Configuración del cliente Supabase
En una aplicación Next.js con App Router necesitas dos variantes del cliente Supabase: una para componentes del servidor y otra para componentes del cliente. Cada una maneja las cookies de forma diferente.
Cliente para Server Components
El cliente del servidor lee las cookies directamente desde los headers de la request:
// lib/supabase/server.ts
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(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch {
// setAll puede fallar en Server Components (read-only)
// Esto es esperado cuando se llama desde un Server Component
}
},
},
}
);
}Cliente para Client Components
El cliente del navegador usa cookies del documento para mantener la sesión:
// lib/supabase/client.ts
import { createBrowserClient } from "@supabase/ssr";
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}Middleware para renovación de sesión
El middleware es crucial para mantener las sesiones activas. Renueva el token en cada request antes de que llegue a tu aplicación:
// lib/supabase/middleware.ts
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request });
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
request.cookies.set(name, value)
);
supabaseResponse = NextResponse.next({ request });
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
);
},
},
}
);
// Renovar la sesión si es necesario
await supabase.auth.getUser();
return supabaseResponse;
}
// middleware.ts
import { updateSession } from "@/lib/supabase/middleware";
import type { NextRequest } from "next/server";
export async function middleware(request: NextRequest) {
return await updateSession(request);
}
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|.*\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};Autenticación completa
Supabase Auth soporta múltiples métodos de autenticación: email y contraseña, magic links, proveedores OAuth (Google, GitHub, Apple, etc.) y autenticación por teléfono. Veamos cómo implementar los más comunes.
Registro e inicio de sesión
// app/auth/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { createClient } from "@/lib/supabase/server";
export async function signUp(formData: FormData) {
const supabase = await createClient();
const data = {
email: formData.get("email") as string,
password: formData.get("password") as string,
};
const { error } = await supabase.auth.signUp({
...data,
options: {
emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
},
});
if (error) {
return { error: error.message };
}
revalidatePath("/", "layout");
redirect("/auth/verify-email");
}
export async function signIn(formData: FormData) {
const supabase = await createClient();
const { error } = await supabase.auth.signInWithPassword({
email: formData.get("email") as string,
password: formData.get("password") as string,
});
if (error) {
return { error: error.message };
}
revalidatePath("/", "layout");
redirect("/dashboard");
}
export async function signOut() {
const supabase = await createClient();
await supabase.auth.signOut();
revalidatePath("/", "layout");
redirect("/");
}Proveedores OAuth
Para iniciar sesión con Google, GitHub u otros proveedores, necesitas configurar las credenciales OAuth en el panel de Supabase (Authentication > Providers) y luego implementar el flujo en tu aplicación:
// app/auth/oauth/route.ts
import { createClient } from "@/lib/supabase/server";
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const provider = searchParams.get("provider") as "google" | "github";
const supabase = await createClient();
const { data, error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
},
});
if (error) {
return NextResponse.redirect(
`${process.env.NEXT_PUBLIC_SITE_URL}/auth/error`
);
}
return NextResponse.redirect(data.url);
}Callback de autenticación
// app/auth/callback/route.ts
import { createClient } from "@/lib/supabase/server";
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
const next = searchParams.get("next") ?? "/dashboard";
if (code) {
const supabase = await createClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) {
return NextResponse.redirect(`${origin}${next}`);
}
}
return NextResponse.redirect(`${origin}/auth/error`);
}Gestión de sesión en componentes
// app/dashboard/page.tsx
import { createClient } from "@/lib/supabase/server";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
redirect("/auth/login");
}
return (
<main>
<h1>Bienvenido, {user.email}</h1>
<p>ID de usuario: {user.id}</p>
</main>
);
}Operaciones de base de datos
Supabase genera automáticamente una API REST a partir de tu esquema PostgreSQL. Esto significa que cualquier tabla que crees en la base de datos es inmediatamente accesible a través del cliente JavaScript con tipado completo.
Crear tablas
Puedes crear tablas desde el panel SQL Editor de Supabase o con migraciones locales:
-- Crear una tabla de proyectos
CREATE TABLE projects (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
description TEXT,
status TEXT DEFAULT 'draft' CHECK (status IN ('draft', 'active', 'completed', 'archived')),
is_public BOOLEAN DEFAULT false,
metadata JSONB DEFAULT '{}'::jsonb
);
-- Crear un trigger para actualizar updated_at automáticamente
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$ LANGUAGE plpgsql;
CREATE TRIGGER projects_updated_at
BEFORE UPDATE ON projects
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();Operaciones CRUD
// lib/actions/projects.ts
"use server";
import { createClient } from "@/lib/supabase/server";
import { revalidatePath } from "next/cache";
// INSERTAR un nuevo proyecto
export async function createProject(formData: FormData) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error("No autenticado");
const { data, error } = await supabase
.from("projects")
.insert({
user_id: user.id,
title: formData.get("title") as string,
description: formData.get("description") as string,
status: "draft",
})
.select()
.single();
if (error) throw new Error(error.message);
revalidatePath("/dashboard/projects");
return data;
}
// CONSULTAR proyectos con filtros y paginación
export async function getProjects(page = 1, limit = 10) {
const supabase = await createClient();
const from = (page - 1) * limit;
const to = from + limit - 1;
const { data, error, count } = await supabase
.from("projects")
.select("*", { count: "exact" })
.order("created_at", { ascending: false })
.range(from, to);
if (error) throw new Error(error.message);
return {
projects: data,
total: count ?? 0,
totalPages: Math.ceil((count ?? 0) / limit),
};
}
// ACTUALIZAR un proyecto
export async function updateProject(id: string, formData: FormData) {
const supabase = await createClient();
const { error } = await supabase
.from("projects")
.update({
title: formData.get("title") as string,
description: formData.get("description") as string,
status: formData.get("status") as string,
})
.eq("id", id);
if (error) throw new Error(error.message);
revalidatePath("/dashboard/projects");
}
// ELIMINAR un proyecto
export async function deleteProject(id: string) {
const supabase = await createClient();
const { error } = await supabase
.from("projects")
.delete()
.eq("id", id);
if (error) throw new Error(error.message);
revalidatePath("/dashboard/projects");
}Consultas avanzadas
// Consultas con relaciones (joins)
const { data } = await supabase
.from("projects")
.select(`
id,
title,
status,
user:user_id (
id,
email,
raw_user_meta_data->>full_name
),
tasks (
id,
title,
completed
)
`)
.eq("is_public", true)
.order("created_at", { ascending: false });
// Búsqueda full-text con PostgreSQL
const { data } = await supabase
.from("projects")
.select("*")
.textSearch("title", "react nextjs", {
type: "websearch",
config: "spanish",
});
// Filtros avanzados
const { data } = await supabase
.from("projects")
.select("*")
.gte("created_at", "2025-01-01")
.in("status", ["active", "completed"])
.not("description", "is", null)
.order("updated_at", { ascending: false })
.limit(20);Row Level Security (RLS)
RLS es la capa de seguridad más importante de Supabase. Sin RLS, cualquier persona con tu clave anónima podría acceder a todos los datos de tu base de datos. Las políticas RLS definen quién puede leer, insertar, actualizar o eliminar cada fila de una tabla.
Regla de oro: Siempre habilita RLS en todas las tablas que contengan datos de usuario. Sin excepciones.
-- Habilitar RLS en la tabla projects
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Política: los usuarios solo pueden ver sus propios proyectos
CREATE POLICY "Users can view own projects"
ON projects
FOR SELECT
USING (auth.uid() = user_id);
-- Política: los usuarios pueden ver proyectos públicos de otros
CREATE POLICY "Anyone can view public projects"
ON projects
FOR SELECT
USING (is_public = true);
-- Política: los usuarios solo pueden insertar sus propios proyectos
CREATE POLICY "Users can insert own projects"
ON projects
FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Política: los usuarios solo pueden actualizar sus propios proyectos
CREATE POLICY "Users can update own projects"
ON projects
FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- Política: los usuarios solo pueden eliminar sus propios proyectos
CREATE POLICY "Users can delete own projects"
ON projects
FOR DELETE
USING (auth.uid() = user_id);
La función auth.uid() devuelve el ID del usuario autenticado de la sesión actual. Esto permite que PostgreSQL filtre automáticamente los resultados sin que necesites añadir filtros .eq("user_id", userId) en cada consulta, aunque sigue siendo buena práctica hacerlo para mayor claridad.
Almacenamiento de archivos
Supabase Storage permite subir y servir archivos (imágenes, documentos, vídeos) con políticas de acceso configurables. Cada bucket puede tener sus propias reglas de RLS.
Configuración de un bucket
-- Crear un bucket público para avatares
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', true);
-- Política: los usuarios pueden subir su propio avatar
CREATE POLICY "Users can upload own avatar"
ON storage.objects
FOR INSERT
WITH CHECK (
bucket_id = 'avatars'
AND auth.uid()::text = (storage.foldername(name))[1]
);
-- Política: cualquiera puede ver avatares (bucket público)
CREATE POLICY "Anyone can view avatars"
ON storage.objects
FOR SELECT
USING (bucket_id = 'avatars');Subir y servir archivos
// components/AvatarUpload.tsx
"use client";
import { useState } from "react";
import { createClient } from "@/lib/supabase/client";
export function AvatarUpload({ userId }: { userId: string }) {
const [uploading, setUploading] = useState(false);
const supabase = createClient();
async function handleUpload(event: React.ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0];
if (!file) return;
setUploading(true);
// Generar un nombre de archivo único
const fileExt = file.name.split(".").pop();
const filePath = `${userId}/avatar.${fileExt}`;
const { error } = await supabase.storage
.from("avatars")
.upload(filePath, file, {
cacheControl: "3600",
upsert: true, // Reemplazar si ya existe
});
if (error) {
console.error("Error al subir:", error.message);
} else {
// Obtener la URL pública
const { data } = supabase.storage
.from("avatars")
.getPublicUrl(filePath);
console.log("URL del avatar:", data.publicUrl);
}
setUploading(false);
}
return (
<label className="cursor-pointer">
<input
type="file"
accept="image/*"
onChange={handleUpload}
disabled={uploading}
className="hidden"
aria-label="Subir avatar"
/>
<span className="inline-flex items-center rounded-lg bg-primary px-4 py-2 text-white">
{uploading ? "Subiendo..." : "Cambiar avatar"}
</span>
</label>
);
}Suscripciones en tiempo real
Supabase Realtime permite escuchar cambios en la base de datos a través de WebSockets. Esto es perfecto para funcionalidades como chats, notificaciones en vivo, actualizaciones colaborativas y dashboards en tiempo real.
// hooks/useRealtimeProjects.ts
"use client";
import { useEffect, useState } from "react";
import { createClient } from "@/lib/supabase/client";
import type { RealtimePostgresChangesPayload } from "@supabase/supabase-js";
interface Project {
id: string;
title: string;
status: string;
created_at: string;
}
export function useRealtimeProjects(initialProjects: Project[]) {
const [projects, setProjects] = useState<Project[]>(initialProjects);
const supabase = createClient();
useEffect(() => {
const channel = supabase
.channel("projects-changes")
.on(
"postgres_changes",
{
event: "*", // INSERT, UPDATE, DELETE
schema: "public",
table: "projects",
},
(payload: RealtimePostgresChangesPayload<Project>) => {
switch (payload.eventType) {
case "INSERT":
setProjects((prev) => [payload.new, ...prev]);
break;
case "UPDATE":
setProjects((prev) =>
prev.map((p) =>
p.id === payload.new.id ? payload.new : p
)
);
break;
case "DELETE":
setProjects((prev) =>
prev.filter((p) => p.id !== payload.old.id)
);
break;
}
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [supabase]);
return projects;
}Para usar este hook en una página con Server Components, pasa los datos iniciales desde el servidor:
// app/dashboard/projects/page.tsx
import { createClient } from "@/lib/supabase/server";
import { ProjectList } from "./ProjectList";
export default async function ProjectsPage() {
const supabase = await createClient();
const { data: projects } = await supabase
.from("projects")
.select("*")
.order("created_at", { ascending: false });
return <ProjectList initialProjects={projects ?? []} />;
}
// app/dashboard/projects/ProjectList.tsx
"use client";
import { useRealtimeProjects } from "@/hooks/useRealtimeProjects";
export function ProjectList({
initialProjects,
}: {
initialProjects: Project[];
}) {
const projects = useRealtimeProjects(initialProjects);
return (
<ul>
{projects.map((project) => (
<li key={project.id}>
<h3>{project.title}</h3>
<span>{project.status}</span>
</li>
))}
</ul>
);
}Nota: Para que Realtime funcione, necesitas habilitar la replicación en la tabla. Ve a Database > Publications en el panel de Supabase y asegúrate de que tu tabla esté incluida en la publicación
supabase_realtime.
Generación de tipos TypeScript
Una de las mayores ventajas de Supabase es la generación automática de tipos TypeScript a partir de tu esquema de base de datos. Esto proporciona tipado end-to-end completo, desde la base de datos hasta tus componentes React.
# Instalar la CLI de Supabase
npm install -D supabase
# Iniciar sesión
npx supabase login
# Generar tipos a partir de tu proyecto remoto
npx supabase gen types typescript --project-id abcdefghij > lib/database.types.ts
# O desde una base de datos local (si usas supabase start)
npx supabase gen types typescript --local > lib/database.types.tsEl archivo generado contiene todas las definiciones de tipos para tus tablas, vistas, funciones y enums:
// lib/database.types.ts (generado automáticamente)
export type Database = {
public: {
Tables: {
projects: {
Row: {
id: string;
created_at: string;
updated_at: string;
user_id: string;
title: string;
description: string | null;
status: "draft" | "active" | "completed" | "archived";
is_public: boolean;
metadata: Record<string, unknown>;
};
Insert: {
id?: string;
created_at?: string;
updated_at?: string;
user_id: string;
title: string;
description?: string | null;
status?: "draft" | "active" | "completed" | "archived";
is_public?: boolean;
metadata?: Record<string, unknown>;
};
Update: {
id?: string;
created_at?: string;
updated_at?: string;
user_id?: string;
title?: string;
description?: string | null;
status?: "draft" | "active" | "completed" | "archived";
is_public?: boolean;
metadata?: Record<string, unknown>;
};
};
};
};
};Luego, tipas tu cliente Supabase con estos tipos:
// lib/supabase/server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import type { Database } from "@/lib/database.types";
export async function createClient() {
const cookieStore = await cookies();
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch {
// Esperado en Server Components
}
},
},
}
);
}Ahora todas las consultas tienen autocompletado y verificación de tipos:
const supabase = await createClient();
// TypeScript sabe que 'data' es de tipo Project[] | null
const { data } = await supabase
.from("projects") // Autocompletado de nombres de tabla
.select("title, status") // Autocompletado de columnas
.eq("status", "active"); // Verificación de tipos en valoresDespliegue a producción
Cuando despliegas tu aplicación Next.js con Supabase a producción, hay varias consideraciones importantes para garantizar seguridad, rendimiento y fiabilidad.
Variables de entorno
Configura las variables de entorno en tu plataforma de despliegue (Vercel, Railway, etc.):
NEXT_PUBLIC_SUPABASE_URL: URL de tu proyecto Supabase de producción.NEXT_PUBLIC_SUPABASE_ANON_KEY: Clave anónima del proyecto de producción.SUPABASE_SERVICE_ROLE_KEY: Clave de servicio (solo servidor). Nunca exponerla al cliente.NEXT_PUBLIC_SITE_URL: URL de tu sitio web para callbacks de autenticación.
Connection pooling
Para aplicaciones con alto tráfico, usa el connection pooler de Supabase (Supavisor) en lugar de conexiones directas. Esto evita agotar el límite de conexiones de PostgreSQL:
// Para conexiones desde serverless functions (Vercel, etc.)
// Usa el modo "Transaction" del pooler
// La URL del pooler está disponible en Settings > Database > Connection Pooling
// .env.local (producción)
DATABASE_URL=postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres?pgbouncer=true
DIRECT_URL=postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:5432/postgresLista de verificación para producción
- RLS habilitado: Verifica que todas las tablas con datos de usuario tengan RLS activado y políticas configuradas.
- Confirmación de email: Habilita la confirmación de email en Authentication > Settings para evitar cuentas falsas.
- Rate limiting: Configura límites de tasa en las funciones de autenticación para prevenir ataques de fuerza bruta.
- Backups: Supabase incluye backups diarios automáticos en planes de pago. Verifica que estén configurados.
- Monitorización: Usa el dashboard de Supabase para monitorear consultas lentas, uso de conexiones y errores de autenticación.
- SSL: Todas las conexiones a Supabase usan SSL por defecto. Asegúrate de no deshabilitarlo.
- Tipos actualizados: Añade
npx supabase gen typesa tu pipeline de CI para mantener los tipos sincronizados con tu esquema.
Consejo final: Supabase combina la potencia de PostgreSQL con la facilidad de uso de un BaaS moderno. Con Next.js 15, esta combinación te permite construir aplicaciones fullstack production-ready con autenticación, base de datos, almacenamiento y tiempo real, todo con tipado end-to-end y la seguridad que proporciona Row Level Security.