Webhooks

Sincronizar usuarios con Webhooks

Cuando eliminas o bloqueas a un usuario en Authon, la base de datos de tu app no lo sabe automáticamente. Los Webhooks resuelven esto entregando eventos en tiempo real a tu backend.

El problema

Sin webhooks, tu app no tiene forma de saber cuándo cambia el estado de los usuarios en Authon. Esto conduce a datos desactualizados — usuarios eliminados que aún aparecen, usuarios bloqueados que aún acceden a funciones.

1
Un admin elimina a un usuario en el panel de Authon
2
El registro del usuario en Authon es eliminado
3
La base de datos de tu app aún tiene la fila desactualizada del usuario — no hubo sincronización

La solución

Suscríbete a los eventos user.*. Cuando se disparen, actualiza tu base de datos para que coincida con el estado de Authon.

1
Registra un endpoint de webhook en el panel de Authon
2
Suscríbete a user.created, user.updated, user.deleted, user.banned, user.unbanned
3
En tu handler, inserta, actualiza o elimina la fila correspondiente en tu base de datos

Ejemplo con Express + Prisma

Un handler de webhook completo que mantiene una tabla de usuarios gestionada por Prisma sincronizada con Authon.

routes/webhooks.ts
import express from "express";
import { createHmac, timingSafeEqual } from "crypto";
import { PrismaClient } from "@prisma/client";

const app = express();
const prisma = new PrismaClient();

function verifyWebhook(
  rawBody: Buffer,
  signature: string,
  timestamp: string,
  secret: string,
): boolean {
  const payload = `${timestamp}.${rawBody.toString()}`;
  const expected = createHmac("sha256", secret).update(payload).digest("hex");
  const actual = signature.replace("v1=", "");
  return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(actual, "hex"));
}

app.post(
  "/webhooks/authon",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const sig = req.headers["x-authon-signature"] as string;
    const ts = req.headers["x-authon-timestamp"] as string; // ISO 8601

    if (!verifyWebhook(req.body, sig, ts, process.env.AUTHON_WEBHOOK_SECRET!)) {
      return res.status(401).json({ error: "Invalid signature" });
    }

    const { event, data } = JSON.parse(req.body.toString());

    switch (event) {
      case "user.created":
      case "user.updated":
        await prisma.user.upsert({
          where: { authonId: data.user.id },
          create: {
            authonId: data.user.id,
            email: data.user.email,
            name: data.user.displayName,
            avatarUrl: data.user.avatarUrl,
          },
          update: {
            email: data.user.email,
            name: data.user.displayName,
            avatarUrl: data.user.avatarUrl,
          },
        });
        break;

      case "user.deleted":
        await prisma.user.update({
          where: { authonId: data.user.id },
          data: { deletedAt: new Date() }, // soft-delete
        }).catch(() => {});
        break;

      case "user.banned":
        await prisma.user.update({
          where: { authonId: data.user.id },
          data: { suspended: true, suspendedAt: new Date() },
        });
        break;

      case "user.unbanned":
        await prisma.user.update({
          where: { authonId: data.user.id },
          data: { suspended: false, suspendedAt: null },
        });
        break;
    }

    res.json({ received: true });
  }
);

Ejemplo con Next.js App Router

Una ruta API de Next.js que gestiona los eventos de sincronización de usuarios usando el App Router.

app/api/webhooks/authon/route.ts
import { createHmac, timingSafeEqual } from "crypto";
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";

export async function POST(req: NextRequest) {
  const rawBody = await req.text();
  const signature = req.headers.get("x-authon-signature")!;
  const timestamp = req.headers.get("x-authon-timestamp")!; // ISO 8601

  const payload = `${timestamp}.${rawBody}`;
  const expected = createHmac("sha256", process.env.AUTHON_WEBHOOK_SECRET!)
    .update(payload)
    .digest("hex");
  const actual = signature.replace("v1=", "");

  if (!timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(actual, "hex"))) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
  }

  const { event, data } = JSON.parse(rawBody);

  if (event === "user.created" || event === "user.updated") {
    await db.user.upsert({
      where: { authonId: data.user.id },
      create: {
        authonId: data.user.id,
        email: data.user.email,
        name: data.user.displayName,
      },
      update: {
        email: data.user.email,
        name: data.user.displayName,
      },
    });
  } else if (event === "user.deleted") {
    await db.user.update({
      where: { authonId: data.user.id },
      data: { deletedAt: new Date() }, // soft-delete
    }).catch(() => {});
  }

  return NextResponse.json({ received: true });
}

Casos extremos

¿Qué pasa si el webhook falla?
Authon reintenta hasta 3 veces con retroceso exponencial (1s, 2s). Si todos los intentos fallan, el evento se marca como fallido en el panel donde puedes reproducirlo manualmente.
Orden de los eventos
Los eventos se entregan en orden pero pueden llegar desordenados durante los reintentos. Usa el campo timestamp para detectar e ignorar actualizaciones desactualizadas cuando el orden importa.
Migración inicial
Los webhooks solo capturan eventos futuros. Para la sincronización inicial de usuarios existentes, usa la API de Authon users.list() para paginar todos los usuarios y poblar tu base de datos.

Sincronización inversa (App → Authon)

Los webhooks gestionan la dirección Authon → App. Para la dirección inversa — crear o actualizar usuarios en Authon desde tu app — usa la API backend con tu clave secreta.

lib/authon-sync.ts
import { AuthonBackend } from "@authon/node";

const authon = new AuthonBackend(process.env.AUTHON_SECRET_KEY!);

// Create a user in Authon from your app (e.g. after admin invite)
export async function createAuthonUser(email: string, name: string) {
  return authon.users.create({
    email,
    displayName: name,
    emailVerified: true,
  });
}

// Update user metadata in Authon when your app data changes
export async function syncMetadataToAuthon(
  authonId: string,
  publicMetadata: Record<string, unknown>,
) {
  return authon.users.update(authonId, { publicMetadata });
}

// Bulk import existing users from your app into Authon
export async function bulkImportToAuthon(
  users: Array<{ email: string; name: string }>,
) {
  for (const user of users) {
    await authon.users.create({
      email: user.email,
      displayName: user.name,
      emailVerified: true,
    });
  }
}

Todas las operaciones de gestión de usuarios están disponibles mediante el endpoint de la API REST POST /v1/backend/users.

Authon — Plataforma universal de autenticación