Autenticación Completa con Clerk
Implementación lista para producción: login social, multi-tenant, RBAC personalizado, webhooks y JWT para APIs internas.
Next.js 15 App Router
TypeScript
@clerk/nextjs
Multi-tenant (Organizaciones)
RBAC 4 roles
Prisma sync via webhooks
JWT Templates
4
Roles personalizados
6
Eventos webhook mapeados
~0
UI de auth a construir
<2h
Integración completa
Flujo de autenticación
1
ClerkProvider en RootLayout
Envuelve toda la app, gestiona sesiones automáticamente
2
Middleware en el Edge
Intercepta cada request, redirige no-autenticados
3
SignIn / SignUp Components
Google OAuth + magic links, zero UI custom
4
Webhook → Prisma sync
user.created persiste en BD local al instante
Roles RBAC — NutriTrack Pro
org:owner
Director de clínica
org:admin
Jefe de dietistas
org:dietista
Dietista — acceso propio
org:viewer
Recepcionista
middleware.ts — Protección de rutas en el Edge
Intercepta cada request antes de llegar al servidor. Rutas públicas explícitas; todo lo demás requiere sesión activa.
middleware.ts
middleware.ts
TypeScript
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'; // Rutas accesibles sin autenticación const isPublicRoute = createRouteMatcher([ '/', '/pricing', '/sign-in(.*)', '/sign-up(.*)', '/api/webhooks(.*)', // Clerk webhooks deben ser públicos ]); export default clerkMiddleware(async (auth, req) => { if (!isPublicRoute(req)) { await auth.protect(); // Redirige a /sign-in si no hay sesión } }); export const config = { matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'], };
RootLayout + Header — ClerkProvider y componentes prebuilt
ClerkProvider como wrapper global. Header con OrganizationSwitcher (multi-tenant) y UserButton (avatar, logout, perfil).
app/layout.tsx
app/layout.tsx
TSX
import { ClerkProvider } from '@clerk/nextjs'; import { OrganizationSwitcher, UserButton } from '@clerk/nextjs'; function Header() { return ( <nav className="flex items-center justify-between px-6 py-3 border-b"> <span className="font-bold text-lg">NutriTrack Pro</span> <div className="flex items-center gap-4"> // hidePersonal: solo workspaces de clínica, sin espacio personal <OrganizationSwitcher hidePersonal={true} /> <UserButton afterSignOutUrl="/" /> </div> </nav> ); } export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <ClerkProvider> <html lang="es"> <body> <Header /> <main>{children}</main> </body> </html> </ClerkProvider> ); }
Server Action con RBAC — Solo dietistas crean pacientes
La autorización ocurre en el servidor. El cliente nunca puede saltarse este control. Usa
has({ permission }) desacoplado de los nombres de roles.app/actions/patients.ts
app/actions/patients.ts
TypeScript
'use server'; import { auth } from '@clerk/nextjs/server'; import { db } from '@/lib/db'; interface CreatePatientInput { name: string; email: string; birthDate: Date; goal: 'weight_loss' | 'muscle_gain' | 'maintenance'; } export async function createPatient(input: CreatePatientInput) { const { userId, orgId, has } = await auth(); // Verificación 1: usuario autenticado con organización if (!userId || !orgId) throw new Error('Unauthorized'); // Verificación 2: permiso específico (independiente del nombre del rol) if (!has({ permission: 'org:patients:create' })) { throw new Error('Forbidden: solo dietistas y admins pueden crear pacientes'); } return db.patient.create({ data: { ...input, orgId, // Aislado por clínica (organización) assignedTo: userId, // Auto-asignado al dietista creador }, }); }
Webhook → Prisma — Sincronización automática de usuarios y clínicas
Clerk notifica eventos a tu endpoint. Verificación con svix obligatoria. Mantiene una copia local para queries eficientes sin llamar a la API de Clerk por cada operación.
app/api/webhooks/clerk/route.ts
app/api/webhooks/clerk/route.ts
TypeScript
import { Webhook } from 'svix'; import { type WebhookEvent } from '@clerk/nextjs/server'; import { db } from '@/lib/db'; export async function POST(req: Request) { const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!); const body = await req.text(); // Verificación OBLIGATORIA — rechaza cualquier payload no firmado por Clerk const event = wh.verify(body, { 'svix-id': req.headers.get('svix-id')!, 'svix-timestamp': req.headers.get('svix-timestamp')!, 'svix-signature': req.headers.get('svix-signature')!, }) as WebhookEvent; switch (event.type) { case 'user.created': await db.user.create({ data: { clerkId: event.data.id, email: event.data.email_addresses[0]?.email_address, name: `${event.data.first_name} ${event.data.last_name}`.trim(), }}); break; case 'user.deleted': await db.user.delete({ where: { clerkId: event.data.id } }); break; case 'organization.created': await db.clinic.create({ data: { clerkOrgId: event.data.id, name: event.data.name, slug: event.data.slug, plan: 'free', // Plan inicial, actualizable via billing }}); break; } return new Response('OK'); }
Eventos Webhook mapeados
| Evento Clerk | Acción Prisma | Modelo |
|---|---|---|
| user.created | db.user.create() | User |
| user.updated | db.user.update() — nombre/email | User |
| user.deleted | db.user.delete() | User |
| organization.created | db.clinic.create() — plan: free | Clinic |
| organizationMembership.created | db.membership.create() | Membership |
| organizationMembership.deleted | db.membership.delete() | Membership |
JWT Template — API interna de análisis nutricional
Microservicio de análisis nutricional separado de la app Next.js. Verifica tokens Clerk sin compartir cookies ni sesiones.
services/nutrition-api/auth.ts
services/nutrition-api/auth.ts
TypeScript
// Configurar en dashboard.clerk.com → JWT Templates → "nutrition-api" // Claims: { "userId": "{{user.id}}", "orgId": "{{org.id}}", "role": "{{org.role}}" } // ── Cliente Next.js: obtener token antes de llamar al microservicio ── import { useAuth } from '@clerk/nextjs'; export function useNutritionApi() { const { getToken } = useAuth(); return async function analyzeNutrition(patientId: string) { const token = await getToken({ template: 'nutrition-api' }); return fetch(`https://api.nutritrack.pro/v1/analyze/${patientId}`, { headers: { Authorization: `Bearer ${token}` }, }).then(r => r.json()); }; } // ── Microservicio Express: verificar token entrante ── import { createClerkClient } from '@clerk/backend'; const clerk = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY }); export async function verifyToken(req: Request) { const token = req.headers.get('authorization')?.replace('Bearer ', ''); if (!token) throw new Error('No token'); const payload = await clerk.verifyToken(token); // payload.userId, payload.orgId, payload.role disponibles return payload; }