Syncing Users with Webhooks
When you delete or ban a user in Authon, your app's database doesn't automatically know about it. Webhooks solve this by delivering real-time events to your backend.
The Problem
Without webhooks, your app has no way of knowing when Authon's user state changes. This leads to stale data — deleted users still appearing, banned users still accessing features.
The Solution
Subscribe to user.* events. When they fire, update your database to match Authon's state.
Express + Prisma Example
A complete webhook handler that keeps a Prisma-managed users table in sync with Authon.
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 });
}
);Next.js App Router Example
A Next.js API route that handles user sync events using the App Router.
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 });
}Edge Cases
Reverse Sync (App → Authon)
Webhooks handle the Authon → App direction. For the reverse — creating or updating users in Authon from your app — use the Backend API with your secret key.
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,
});
}
}All user management operations are available via the REST API endpoint POST /v1/backend/users.