AUTH4D-158 Flows event model and trigger points

Flows let a project react to what happens in its sign-in flows. This fragment defines the version 1 event and trigger shapes and where the API raises them. Nothing is delivered to your servers yet: webhook delivery and configurable hooks come in later Flows tickets. No endpoint changes.

The schemas are in packages/contracts (FlowEventSchema, FlowTriggerInputSchema, FlowHookDecisionSchema, FlowHooks). The publish point and hook call sites are in apps/api/src/modules/flows.

What a payload carries

Every event and trigger carries the project (tenant_id), the application (application_id, the client id), the user (user_id), how they signed in (method) and coarse context. Context is an optional two-letter country code from Cloudflare’s edge and an optional identity provider.

Payloads never carry tokens, authorization or device codes, identity proofs, email addresses, IP addresses or secrets. The schemas are strict, so any extra field is rejected.

method is one of:

Value Meaning
email, discord, steam The user proved this identity in this flow.
guest An anonymous guest account.
session An existing hosted-login browser session was reused with no new proof.
device A native device received tokens after a browser approved its code.

Async events

Events are published after the action has been committed, one queue message per event:

{
  "event_id": "evt_7wp-mPdHTZ_ug4hvHKYjjaV31ZK8b79pVRLZ0EjCBNU",
  "version": 1,
  "type": "user.signed_in",
  "tenant_id": "ten_…",
  "application_id": "cli_…",
  "user_id": "usr_…",
  "occurred_at": "2026-10-06T18:27:19.079Z",
  "context": { "country": "ZA", "provider": "discord" },
  "data": { "method": "discord" }
}
Type Published when data
user.created A guest is created, or an email, Discord or Steam sign-in stores a new user. method: email, discord, steam or guest
user.signed_in A guest is created, hosted login issues an authorization code, or a device collects its tokens. method
guest.upgraded A browser or native guest upgrade attaches a credential. method: email, discord or steam
session.revoked A session ends through logout, token revocation, an administrator or a password reset or change. reason: logout, token_revocation, admin or password
member.changed A project member joins through an invitation, changes role or is removed. change: added, role_changed or removed; role or null

Notes:

  • application_id is null for member.changed and for an administrator revoking all of a user’s sessions, which is one event rather than one per session. Management events have empty context.
  • event_id is derived from the project, the event type and the action (for example the session or the authorization code). A retried publish of the same action has the same event_id, so consumers must deduplicate on it. Ids are unique per project.
  • Each message belongs to exactly one project and names it in tenant_id.
  • Publishing is best effort and never fails the user’s request. A queue error is logged with the event type and project only.
  • When a guest upgrades with email, Discord or Steam, the hosted login first creates a short-lived account for the new credential. That account gets its own user.created, then the credential moves to the guest and guest.upgraded names the guest.

The queue binding is FLOW_EVENTS (auth4-flow-events-{local,staging,production}). AUTH4D-157 consumes it and delivers the events to project webhook endpoints; see auth4d-157.md.

Synchronous triggers

Triggers run inside the action. Protocol checks always run first: PKCE, redirect_uri, origin and CSRF checks, flow cookies and rate limits are unchanged and are never handed to a hook. A hook answers { "action": "continue" } or { "action": "deny" }. The default implementation always continues. A hook that throws or answers anything else denies the action.

Trigger Runs On deny
pre_registration Before a guest, email, Discord or Steam user is stored. There is no user_id yet. No user is stored. Guests get 403 access_denied; other methods fail as a failed sign-in.
post_login After the user is authenticated, before a guest’s tokens or an authorization code are issued. No tokens or code. Hosted login redirects with error=access_denied; guests get 403.
pre_guest_upgrade After the upgrade proof is checked, before the credential is attached. method is its provider. 403 access_denied; nothing is attached and the guest session stays.
device_approval When a signed-in browser approves a user code. method is session. The request is recorded as denied, so the device’s next poll gets access_denied.

Trigger input:

{
  "version": 1,
  "trigger": "post_login",
  "tenant_id": "ten_…",
  "application_id": "cli_…",
  "user_id": "usr_…",
  "method": "email",
  "context": { "provider": "email" }
}

Tests

tests/integration/auth4d-158.flows.test.ts checks the schemas and that each action publishes its events once, with no tokens, codes or email addresses, and only for its own project. It also checks that a denying hook stops guest creation, token issuance and guest upgrades without publishing anything.

Source: docs/api-spec/auth4d-158.md