BluAuth
Docs
Sign in
User FAQ
  • Reset my password
  • I can't sign in
  • Didn't get reset email
  • Account linking
  • Session expiry
  • Two-factor auth
Admin Guides
Theme Studio
  • Overview
  • Layouts
  • Styling tokens
  • Concept copy
  • Assets & backgrounds
  • Advanced CSS
Admin Shell
  • Users
  • Providers
  • Clients
  • Invitations
Integrations
  • OIDC flow
  • Legacy OAuth flow
  • Provider token brokering
  • Email triggers
  • Webhook events
  • Session contract
Reference
  • API
  • Error codes
  • Event shapes
  • Design tokens
Runbooks
  • Deployment
  • Local operations

Event Shapes

Every event BluAuth emits conforms to the canonical envelope defined in shared/types/events.ts. Events flow out of the Nuxt server → SNS topic (BluAuthEvents) → SQS queue (WebhookDeliveryQueue) → per-client HTTPS webhook deliveries. Dead letters land in WebhookDLQ after 3 failed attempts.

Envelope

interface EventPayload {
    eventType: string;
    aggregateId: string; // UUID — the primary entity the event describes
    timestamp: string; // ISO-8601 UTC with Z suffix
    data: Record<string, unknown>;
}

eventType values are the string constants exported by EventType. What varies between events is the data object. This page documents the shape of data for each event.

Event type enumeration

The canonical list from EventType (see shared/types/events.ts):

ConstantString valueAggregate
USER_CREATEDuser.createduser
USER_UPDATEDuser.updateduser
USER_DELETEDuser.deleteduser
USER_DEACTIVATEDuser.deactivateduser
USER_REACTIVATEDuser.reactivateduser
ACCOUNT_LINKEDaccount.linkeduser (acting on account)
ACCOUNT_UNLINKEDaccount.unlinkeduser (acting on account)
SESSION_CREATEDsession.createduser (legacy) / session (new)
SESSION_REVOKEDsession.revokedsession
INVITATION_CREATEDinvitation.createdinvitation
INVITATION_ACCEPTEDinvitation.acceptedinvitation
INVITATION_REVOKEDinvitation.revokedinvitation

Delivery contract

  • Transport: HTTPS POST to the endpoint stored on webhook_endpoints.url.
  • Signature: X-BluAuth-Signature header — HMAC-SHA256 of the raw body, keyed by the webhook's KMS-decrypted secret. Hex-encoded. Constant-time compare on your side.
  • Headers: Content-Type: application/json, X-BluAuth-Event (event type), X-BluAuth-Delivery (delivery UUID), X-BluAuth-Timestamp (Unix seconds).
  • Retries: SQS redrives up to 3 times with exponential backoff before DLQing. Your endpoint must be idempotent on aggregateId + eventType + timestamp.
  • Order: not guaranteed. Rely on timestamp and your own sequencing when ordering matters (e.g. user.created before account.linked).
  • Idempotency key: webhook_deliveries.id (UUID) is unique per delivery attempt. Dedupe on (eventType, aggregateId, timestamp) if you need finer-grained replay protection.
  • At-least-once. Every subscriber must tolerate duplicate deliveries.

User events

user.created

Fired when a new user row is inserted. Sources: admin create, OAuth first-login, invitation accept, credential sign-up via Better Auth.

{
    "eventType": "user.created",
    "aggregateId": "<user-uuid>",
    "timestamp": "2026-04-16T17:23:45.000Z",
    "data": {
        "email": "user@example.com",
        "name": "User Name",
        "emailVerified": true,
        "createdVia": "invitation | signup | admin | oauth",
        "clientId": "<uuid-if-initiated-by-a-client>",
        "invitationId": "<uuid-if-from-invitation>",
        "provider": "<provider-slug-if-from-oauth>"
    }
}

createdVia distinguishes the entry path. clientId is present when the flow was initiated via a specific OAuth client (e.g. the hosted login was accessed with ?client_slug=).

user.updated

Fired on any write to users that changes email, name, profile fields, or role.

{
    "eventType": "user.updated",
    "aggregateId": "<user-uuid>",
    "timestamp": "2026-04-16T17:23:45.000Z",
    "data": {
        "email": "new@example.com",
        "name": "Updated Name",
        "changedFields": ["email", "name"]
    }
}

changedFields lists only the fields that actually changed (diff vs. prior row). Purely-metadata updates (e.g. updatedAt only) do not fire this event.

user.deleted

Fired before the row is soft-deleted. Consumers may still look up the user during processing.

{
    "eventType": "user.deleted",
    "aggregateId": "<user-uuid>",
    "timestamp": "2026-04-16T17:23:45.000Z",
    "data": {
        "email": "user@example.com",
        "actorId": "<admin-user-uuid-or-null>"
    }
}

user.deactivated / user.reactivated

{
    "eventType": "user.deactivated",
    "aggregateId": "<user-uuid>",
    "timestamp": "2026-04-16T17:23:45.000Z",
    "data": {
        "actorId": "<admin-user-uuid>",
        "reason": "string or null"
    }
}

Deactivation revokes all active sessions as a side effect — consumers will observe session.revoked events for each revoked session in close succession.


Account events

Provider-account linkage changes.

account.linked

Fired when a credentials-type or upstream-provider account is attached to a user. Fires in the OAuth callback when resolution.kind !== 'existing-account' and on admin-driven link operations.

{
    "eventType": "account.linked",
    "aggregateId": "<user-uuid>",
    "timestamp": "2026-04-16T17:23:45.000Z",
    "data": {
        "providerSlug": "google",
        "providerAccountId": "112233445566778899",
        "providerAccountEmail": "user@example.com",
        "linkedBy": "oauth-callback | admin | invitation"
    }
}

providerAccountId is the upstream subject (not BluAuth's pairwise sub). providerAccountEmail is the email asserted by the upstream provider and may differ from the BluAuth user's canonical email.

account.unlinked

{
    "eventType": "account.unlinked",
    "aggregateId": "<user-uuid>",
    "timestamp": "2026-04-16T17:23:45.000Z",
    "data": {
        "providerSlug": "google",
        "providerAccountId": "112233445566778899",
        "unlinkedBy": "user | admin"
    }
}

Unlinking a credential account is prohibited if it's the user's only authentication method — the operation 422s before this event would fire.


Session events

session.created

Fired on every new session — credential login, OAuth callback success, invitation accept.

{
    "eventType": "session.created",
    "aggregateId": "<user-uuid>",
    "timestamp": "2026-04-16T17:23:45.000Z",
    "data": {
        "sessionId": "<session-uuid>",
        "isNewUser": false,
        "provider": "google | github | microsoft | credential | oidc",
        "clientId": "<uuid-or-null>",
        "ipAddress": "1.2.3.4",
        "userAgent": "Mozilla/5.0 ...",
        "expiresAt": "2026-04-23T17:23:45.000Z"
    }
}

isNewUser: true means this session was issued as part of a first-login that also created the user row — consumers doing JIT provisioning should provision exactly once per user based on this flag rather than counting user.created.

session.revoked

{
    "eventType": "session.revoked",
    "aggregateId": "<session-uuid>",
    "timestamp": "2026-04-16T17:23:45.000Z",
    "data": {
        "userId": "<user-uuid>",
        "reason": "logout | admin_revocation | password_change | expiry | deactivation | merge"
    }
}

The merge reason is used when a user merge collapses sessions from a source user into a target user.


Invitation events

invitation.created

Fired when an admin creates an invitation via /api/admin/invitations. Email-send is fire-and-forget — this event fires even if the SES send fails later.

{
    "eventType": "invitation.created",
    "aggregateId": "<invitation-uuid>",
    "timestamp": "2026-04-16T17:23:45.000Z",
    "data": {
        "email": "invitee@example.com",
        "recipientName": "First Last | null",
        "clientId": "<uuid>",
        "clientSlug": "downstream-app",
        "invitedByUserId": "<admin-user-uuid>",
        "expiresAt": "2026-04-23T00:00:00.000Z",
        "context": { "role": "editor", "onboardingPath": "/welcome", "requiredProviderKeys": ["tpauth"] }
    }
}

context is the signed, admin-supplied payload attached to the invitation. Downstream apps use context to pre-configure the user on their side before accepting the invite (e.g. to assign team membership). requiredProviderKeys, when present, means the invite can only be redeemed through those providers.

invitation.accepted

Fired before the session cookie is issued, so downstream provisioning can complete before the user's first authenticated request.

{
    "eventType": "invitation.accepted",
    "aggregateId": "<invitation-uuid>",
    "timestamp": "2026-04-16T17:23:45.000Z",
    "data": {
        "userId": "<user-uuid-created-or-linked>",
        "email": "invitee@example.com",
        "clientId": "<uuid>",
        "clientSlug": "downstream-app",
        "context": { "role": "editor", "onboardingPath": "/welcome" }
    }
}

If the email matched an existing BluAuth user, userId is that user and the acceptance is treated as a client-association gesture — consumers should handle both "new user" and "existing user" cases from the same event. Correlate with session.created (fires immediately after) to distinguish.

invitation.revoked

{
    "eventType": "invitation.revoked",
    "aggregateId": "<invitation-uuid>",
    "timestamp": "2026-04-16T17:23:45.000Z",
    "data": {
        "actorId": "<admin-user-uuid>",
        "email": "invitee@example.com"
    }
}

Only pending invitations can be revoked. Attempting to revoke an already-accepted or already-revoked invitation 400s and does not emit this event.


Versioning

Event shapes are considered additive-compatible. BluAuth will:

  • Add new fields to data without a version bump.
  • Add new event types without a version bump (consumers should handle unknown eventType by ignoring).
  • Never remove or rename existing fields within the same major version.
  • Keep deprecated fields as null for at least one major version before removal.

When a breaking change is required, BluAuth will:

  1. Introduce a new event type with the new shape (e.g. user.created.v2).
  2. Dual-emit both versions for a deprecation window documented in the release notes.
  3. Stop emitting the old version only after the window closes.

Consumer checklist

For every webhook consumer:

  1. Verify X-BluAuth-Signature with constant-time compare before parsing the body.
  2. Dedupe on (eventType, aggregateId, timestamp) or X-BluAuth-Delivery — deliveries are at-least-once.
  3. Persist the event into an events table before processing — this makes replay trivial.
  4. Handle unknown eventType by ignoring (don't 4xx) — BluAuth may add events between your deploys.
  5. Respond 2xx within 10 seconds. Longer processing must be off-loaded to your own queue.
  6. Non-2xx responses are retried up to 3 times; delivery history is visible at /api/admin/webhooks/:id/deliveries.

On this page

  • Envelope
  • Event type enumeration
  • Delivery contract
  • User events
  • user.created
  • user.updated
  • user.deleted
  • user.deactivated / user.reactivated
  • Account events
  • account.linked
  • account.unlinked
  • Session events
  • session.created
  • session.revoked
  • Invitation events
  • invitation.created
  • invitation.accepted
  • invitation.revoked
  • Versioning
  • Consumer checklist
DocsPrivacyTerms
© 2026 Blu Digital Group