Supabase en Server Components de NextJS
Los Server Components son el contexto ideal para hacer queries a Supabase. Se ejecutan en el servidor, no envian JavaScript al browser, y pueden acceder a las cookies directamente para leer la sesión del usuario. En esta guía vas a aprender a fetchear datos, verificar autenticación y pasar información a Client Components.
Setup previo
Antes de continuar, necesitas tener configurados los tres clientes de Supabase como se explica en la sección anterior. En particular, el cliente del servidor en lib/supabase/server.ts:
// 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 {
// Esperado en Server Components (read-only)
}
},
},
}
)
}Fetch de datos en Server Components
Un Server Component que lee datos de Supabase se ve así:
// app/productos/page.tsx
import { createClient } from '@/lib/supabase/server'
export default async function ProductosPage() {
const supabase = await createClient()
const { data: productos, error } = await supabase
.from('productos')
.select('id, nombre, precio, imagen_url')
.eq('activo', true)
.order('created_at', { ascending: false })
if (error) {
return <p>Error al cargar productos: {error.message}</p>
}
if (!productos || productos.length === 0) {
return <p>No hay productos disponibles.</p>
}
return (
<div>
<h1>Productos</h1>
<ul>
{productos.map((producto) => (
<li key={producto.id}>
<h2>{producto.nombre}</h2>
<p>${producto.precio}</p>
</li>
))}
</ul>
</div>
)
}Esto se ejecuta completamente en el servidor. El browser recibe HTML puro -- sin fetch requests, sin loading states, sin JavaScript adicional.
Autenticación con getUser()
Para saber quién es el usuario actual en un Server Component, usa supabase.auth.getUser():
// app/perfil/page.tsx
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export default async function PerfilPage() {
const supabase = await createClient()
const { data: { user }, error } = await supabase.auth.getUser()
if (error || !user) {
redirect('/login')
}
return (
<div>
<h1>Tu perfil</h1>
<p>Email: {user.email}</p>
<p>ID: {user.id}</p>
<p>Creado: {new Date(user.created_at).toLocaleDateString('es-MX')}</p>
</div>
)
}getUser() vs getSession()
Siempre usa getUser() en vez de getSession() en el servidor. getUser() valida el JWT contra los servidores de Supabase, mientras que getSession() solo lee el token local sin validar. En un Server Component, necesitas la validación real.
Queries con RLS y el usuario autenticado
Cuando creas el cliente del servidor con las cookies del request, el SDK automáticamente incluye el JWT del usuario en las queries. Esto significa que las políticas de RLS se aplican correctamente:
// app/mis-pedidos/page.tsx
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export default async function MisPedidosPage() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
redirect('/login')
}
// Esta query respeta RLS
// Si tienes una política "usuarios ven sus propios pedidos",
// solo devuelve los pedidos del usuario actual
const { data: pedidos, error } = await supabase
.from('pedidos')
.select(`
id,
total,
estado,
created_at,
items:pedido_items(
cantidad,
producto:productos(nombre, precio)
)
`)
.order('created_at', { ascending: false })
if (error) {
return <p>Error al cargar pedidos.</p>
}
return (
<div>
<h1>Mis pedidos</h1>
{pedidos.length === 0 ? (
<p>Todavía no tienes pedidos.</p>
) : (
<ul>
{pedidos.map((pedido) => (
<li key={pedido.id}>
<p>Pedido #{pedido.id}</p>
<p>Total: ${pedido.total}</p>
<p>Estado: {pedido.estado}</p>
<p>{pedido.items.length} productos</p>
</li>
))}
</ul>
)}
</div>
)
}No necesitas filtrar manualmente por usuario_id. RLS se encarga de que cada usuario solo vea sus propios datos.
Pasar datos a Client Components
Los Server Components no pueden tener interactividad (useState, onClick, etc.). Cuando necesitas interactividad con datos de Supabase, fetchea en el Server Component y pasa los datos como props:
// app/dashboard/page.tsx (Server Component)
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import { DashboardClient } from './dashboard-client'
export default async function DashboardPage() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
redirect('/login')
}
// Fetch en el servidor
const { data: estadisticas } = await supabase
.from('estadisticas_usuario')
.select('*')
.eq('usuario_id', user.id)
.single()
const { data: notificaciones } = await supabase
.from('notificaciones')
.select('*')
.eq('usuario_id', user.id)
.eq('leida', false)
.order('created_at', { ascending: false })
.limit(5)
// Pasar datos al Client Component
return (
<DashboardClient
usuario={user}
estadisticas={estadisticas}
notificaciones={notificaciones ?? []}
/>
)
}// app/dashboard/dashboard-client.tsx (Client Component)
'use client'
import { useState } from 'react'
import type { User } from '@supabase/supabase-js'
interface DashboardClientProps {
usuario: User
estadisticas: {
total_pedidos: number
total_gastado: number
productos_favoritos: number
} | null
notificaciones: {
id: string
mensaje: string
created_at: string
}[]
}
export function DashboardClient({
usuario,
estadisticas,
notificaciones,
}: DashboardClientProps) {
const [mostrarNotificaciones, setMostrarNotificaciones] = useState(false)
return (
<div>
<h1>Hola, {usuario.email}</h1>
{estadisticas && (
<div>
<p>Total de pedidos: {estadisticas.total_pedidos}</p>
<p>Total gastado: ${estadisticas.total_gastado}</p>
</div>
)}
<button onClick={() => setMostrarNotificaciones(!mostrarNotificaciones)}>
Notificaciones ({notificaciones.length})
</button>
{mostrarNotificaciones && (
<ul>
{notificaciones.map((n) => (
<li key={n.id}>{n.mensaje}</li>
))}
</ul>
)}
</div>
)
}Patrón recomendado
Fetchea datos en Server Components, pasa como props a Client Components. Así separas las responsabilidades: el servidor se encarga de los datos y la seguridad, el cliente se encarga de la interactividad.
Consideraciones de caching
NextJS cachea los Server Components por defecto. Esto puede causar que los datos de Supabase no se actualicen al navegar entre páginas.
Forzar datos frescos
// Opcion 1: Configuracion de la ruta
export const dynamic = 'force-dynamic'
// Opcion 2: revalidar cada N segundos
export const revalidate = 60 // Revalidar cada 60 segundosCuándo usar cada estrategia
| Escenario | Estrategia |
|---|---|
| Datos del usuario (perfil, pedidos) | force-dynamic -- siempre frescos |
| Catalogo de productos | revalidate = 300 -- cada 5 minutos |
| Página pública sin auth | Dejar el cache por defecto |
| Dashboard con métricas | force-dynamic |
// app/mis-pedidos/page.tsx
import { createClient } from '@/lib/supabase/server'
// Los pedidos del usuario deben ser siempre frescos
export const dynamic = 'force-dynamic'
export default async function MisPedidosPage() {
const supabase = await createClient()
// ...
}Cuidado con el cache y auth
Si una página muestra datos específicos del usuario, siempre usa dynamic = 'force-dynamic'. Sin esto, NextJS podría cachear la página del usuario A y mostrársela al usuario B.
Ejemplo completo: página de perfil con datos del usuario
// app/perfil/page.tsx
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import { PerfilForm } from './perfil-form'
export const dynamic = 'force-dynamic'
export default async function PerfilPage() {
const supabase = await createClient()
// Verificar autenticacion
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) {
redirect('/login?redirect=/perfil')
}
// Traer el perfil extendido del usuario
const { data: perfil, error: perfilError } = await supabase
.from('perfiles')
.select('nombre, bio, avatar_url, sitio_web')
.eq('id', user.id)
.single()
if (perfilError) {
return <p>Error al cargar tu perfil.</p>
}
// Traer estadisticas
const { count: totalPosts } = await supabase
.from('posts')
.select('*', { count: 'exact', head: true })
.eq('autor_id', user.id)
const { count: totalComentarios } = await supabase
.from('comentarios')
.select('*', { count: 'exact', head: true })
.eq('autor_id', user.id)
return (
<div>
<h1>Tu perfil</h1>
<div>
<p>Email: {user.email}</p>
<p>Miembro desde: {new Date(user.created_at).toLocaleDateString('es-MX')}</p>
<p>{totalPosts ?? 0} posts publicados</p>
<p>{totalComentarios ?? 0} comentarios</p>
</div>
{/* Client Component para editar el perfil */}
<PerfilForm
perfilInicial={{
nombre: perfil.nombre ?? '',
bio: perfil.bio ?? '',
sitioWeb: perfil.sitio_web ?? '',
}}
/>
</div>
)
}El Server Component hace 3 queries a Supabase (perfil, posts, comentarios), todo en el servidor. El usuario recibe la página ya renderizada. El formulario de edición es un Client Component que usa Server Actions para guardar los cambios (eso lo cubrimos en la siguiente sección).
Resumen
| Concepto | Detalle |
|---|---|
| Cliente | createClient() de lib/supabase/server.ts |
| Auth | supabase.auth.getUser() (valida JWT en el servidor) |
| RLS | Se aplica automáticamente con el JWT del usuario |
| Interactividad | Fetchear en Server Component, pasar props a Client Component |
| Cache | force-dynamic para datos del usuario, revalidate para datos públicos |
| Patrón | Server Component = datos + seguridad, Client Component = interactividad |