Skip to content
anchr.ing
Guide

Keep your data in step with webhooks

Signed, retried events for users, sessions and organisations, verified and deduplicated by the SDK; back-channel logout for ended sessions.

Add your endpoint (e.g. https://api.acme.example/hooks/auth) in the dashboard, Webhooks, and put its signing secret in ANCHRING_WEBHOOK_SECRET. Each environment's webhook has its own secret (promoting creates a new one for prod), so set the variable per environment.

api/hooks.ts
const auth = createAuth({ project: 'acme', env: process.env.AUTH_ENV ?? 'test', audience: 'acme-api' }); // reads ANCHRING_WEBHOOK_SECRET

app.post('/hooks/auth', auth.webhooks.handler({
  'user.created': (e) => createAccount(e.sub, e.email),
  'user.deleted': (e) => deleteAccount(e.sub),
}));

The handler checks the anchring-signature HMAC against the raw body with a 5 minute tolerance and deduplicates by event id: 401 for a bad or old signature, 200 for a replay or a type you don't handle, 500 if your handler throws (the event is retried). Deliveries are retried with backoff, so make handlers idempotent.

Events: user.created, user.updated, user.email_changed, user.deleted, user.disabled, user.enabled, session.revoked, credential.changed, invite.accepted, org.member_added, org.member_removed, org.member_updated. An unknown event name in the handler is a type error.

Express

Signed bodies need the raw bytes: mount the handler before express.json().

import { toExpress } from '@anchring/auth/express';

app.post('/hooks/auth', toExpress(auth.webhooks.handler({ /* … */ }))); // before express.json()
app.use(express.json());

Rotating the secret

Rotate secret on the webhook returns the new secret once; the old one keeps signing next to it for a grace period (a day by default). Accept both meanwhile:

createAuth({ project: 'acme', env: 'prod', webhookSecret: [process.env.WEBHOOK_SECRET_NEW!, process.env.WEBHOOK_SECRET_OLD!] });

Webhook URLs must be https; plain http is accepted only for localhost in test. With several instances of your API, pass a shared replayStore so a replay to another instance is caught too.

Back-channel logout

When a user signs out, or an admin ends their sessions, anchring posts an OIDC back-channel logout token to the app's back-channel logout URI. Configure the app's clientId (the token's audience) in createAuth:

app.post('/auth/backchannel-logout', auth.backchannel.handler((e) => destroyAppSessions(e.sid, e.sub)));

// Reject app sessions the IdP has ended (by sid, or every session of a sub before the logout)
if (await auth.backchannel.isLoggedOut(c.var.user)) return c.json({ error: 'logged_out' }, 401);

Every option, error and token type: the SDK README.