Skill: autenticacion-clerk-web

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
Acceso total
org:admin
Jefe de dietistas
Gestión equipo
org:dietista
Dietista — acceso propio
CRUD pacientes
org:viewer
Recepcionista
Solo lectura
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;
}