User Creation Flow and Hooks
This document describes the complete user creation flow in AuthHero and all hooks that are triggered during the process.
Pipeline architecture
For the design rationale behind the three-phase (prepare / commit / publish) model, transaction boundaries, and how post-hooks are delivered via the outbox with retries and dead-letter, see the Hooks & Outbox Pipeline architecture doc. The guide below focuses on which hooks fire and in what order.
Overview
When a new user signs up or is created in AuthHero, whether through email/password, passwordless, social login, or the Management API, a series of validation checks and hooks are executed to ensure proper authorization and data integrity.
Hook execution follows a strict three-phase model:
- Prepare — validation + blocking hooks (pre-registration, pre-update, pre-deletion). Outside any DB transaction so webhook calls and user-authored action code can take unbounded time.
- Commit — one short DB transaction that writes the user + password + outbox event atomically. No external I/O.
- Publish — post-hook dispatch via the outbox (webhooks, finalizers). Retried with exponential backoff; dead-lettered after exhaustion. Self-heals on the user's next login via
registration_completed_at.
User Creation Methods
Users can be created through several methods:
- Email/Password Signup - Via
/dbconnections/signupendpoint - Passwordless (Email/SMS) - Via
/passwordless/startendpoint followed by verification - Social Login - Via OAuth connections (Google, Facebook, etc.)
- Management API - Direct user creation via
/api/v2/usersendpoint
Signup Validation Flow
1. Early Validation (Optional)
Before a user even attempts to sign up, you can validate their eligibility using the validateRegistrationUsername function. This is useful for:
- Identifier pages that check if signup is allowed before showing the signup form
- Providing early feedback to users
- Avoiding unnecessary user interactions when signup is disabled
Function: validateRegistrationUsername(ctx, client, data, email)
Returns: { allowed: boolean, reason?: string }
Checks:
- Whether
disable_sign_upsis set to"true"in client metadata - If
screen_hint=signupis present in authorization URL (overrides the disable setting) - If another user with the same email exists (allows linking)
2. Pre-Registration Hook (Right Before User Creation)
When user creation is attempted through any signup flow (email/password, social, passwordless email), the preUserRegistrationHook is executed.
Function: preUserRegistrationHook(ctx, client, data, email)
Executed for:
- ✅ Email/password signups
- ✅ Passwordless email signups
- ✅ Social login signups (Google, Facebook, etc.)
- ❌ Passwordless SMS signups (phone-based, no email validation needed)
- ❌ Management API user creation (no client_id context)
Note: SMS/phone-based signups are not subject to email-based signup validation since they don't have an email address. The disable_sign_ups client metadata only applies to email-based authentication methods.
Actions:
- Re-validates signup eligibility using
validateRegistrationUsername - Logs failed signup attempts (type:
fs) - Invokes pre-registration webhooks if configured
- Throws
HTTPException 400if signup should be blocked
Bypass Conditions:
screen_hint=signupis present in the authorization URL- A user with the same verified email already exists (account linking)
3. User Creation Hook
Once the pre-registration hook passes, the actual user creation begins through data.users.create. This triggers additional hooks through createUserHooks.
Actions:
- Validates client_id exists in context (auth flows only)
- Fetches client configuration
- Executes
preUserRegistrationHook(for auth flows with client_id) - Invokes
onExecutePreUserRegistration(programmatic hook) - Resolves the effective
userLinkingMode(per-client → service-level) - Commits the user via
commitUserHook— when the resolved mode allows it, the email→primary lookup runs inside the same transaction; otherwise the lookup is skipped and only the row is committed - Invokes
onExecutePostUserRegistration(programmatic hook) - Dispatches enabled
post-user-registrationtemplate hooks (e.g.account-linking) - Invokes post-user-registration webhooks
Complete Hook Execution Order
When a new user signs up through an authentication flow:
PHASE 1 — Prepare (no DB transaction held)
1. validateRegistrationUsername (optional, early check)
↓
2. preUserRegistrationHook
├── validateRegistrationUsername (re-validation)
├── Log failed signup (if blocked)
└── preUserRegistrationWebhook ◀── HTTP, can take seconds
↓
3. onExecutePreUserRegistration (programmatic hook)
↓
4. pre-user-registration code hooks (Cloudflare Dispatch)
PHASE 2 — Commit (single short DB transaction)
5. commitUserHook
├── (optional) getPrimaryUserByEmail — only if userLinkingMode resolves
│ to "builtin" for the current client/tenant
├── users.rawCreate (bypasses the decorator; no hook re-entry)
└── linked_to resolution if a primary was found
PHASE 3 — Publish (runs after the commit, never blocks it)
6. onExecutePostUserRegistration (programmatic, inline for now)
7. post-user-registration code hooks (inline for now — see roadmap)
8. post-user-registration template hooks (e.g. account-linking)
└── pre-defined function dispatched by template_id; not user code
9. enqueuePostHookEvent("post-user-registration")
└── outbox relay → WebhookDestination + RegistrationFinalizerDestination
├── POSTs to enabled webhooks with Idempotency-Key = event.id
├── retries with exponential backoff on failure (max 5)
├── moves to dead-letter after retry exhaustion
└── on success → sets user.registration_completed_atSelf-healing: if registration_completed_at is still null on the user's next login, postUserLoginHook re-enqueues the post-user-registration event so a dead-lettered or lost delivery recovers automatically. Webhook consumers must be idempotent (enforced by the Idempotency-Key header).
Signup Blocking with disable_sign_ups
Configuration
Set the disable_sign_ups client metadata to "true" to block public signups:
{
"client_metadata": {
"disable_sign_ups": "true"
}
}Behavior
When disable_sign_ups is enabled:
❌ Blocked:
- New signups via email/password
- New signups via passwordless email
- New signups via social logins (Google, Facebook, etc.)
✅ Allowed:
- Signups via SMS/phone (not email-based, so not restricted)
- Signups when
screen_hint=signupis in the authorization URL (e.g., for invited users) - Signups when a user with the same verified email already exists (account linking)
- User creation via Management API (admin operations bypass signup restrictions)
- Login attempts for existing users
Important: The disable_sign_ups setting only applies to email-based authentication methods. SMS/phone-based signups are not restricted because they don't use email addresses and follow a different validation flow.
Use Cases
Invite-Only Applications
- Block public signups but allow invited users via
screen_hint=signup
- Block public signups but allow invited users via
Account Linking
- User signs up with email/password
- Later tries to sign in with Google using the same email
- Google account is automatically linked to existing account even with
disable_sign_ups: true
Gradual Rollout
- Start with closed beta (signups disabled)
- Send invitation links with
screen_hint=signup - Open to public later by removing the flag
Hook Types
URL/Form Hooks (Management API)
Configured via the Management API and support the following triggers:
pre-user-registration- Before user creationpost-user-registration- After successful creationpost-user-login- After successful loginvalidate-registration-username- Validate registration eligibilitypre-user-deletion- Before user deletionpost-user-deletion- After user deletion
Programmatic Hooks (Application Config)
Defined in your application code when initializing AuthHero:
const hooks = {
onExecutePreUserRegistration: async (event, api) => {
// Modify user metadata before creation
api.user.setUserMetadata("source", "web");
},
onExecutePostUserRegistration: async (event, api) => {
// Perform actions after user is created
await sendWelcomeEmail(event.user.email);
},
};Management API User Creation
When creating users through the Management API (POST /api/v2/users):
Differences:
- No
client_idin context - Pre-signup hooks are skipped (no client-specific validation)
- Account linking hooks still execute
- Post-registration hooks still execute
- Intended for administrative operations
Rationale: Management API operations are admin-initiated and bypass client-specific signup restrictions.
Best Practices
- Use
validateRegistrationUsernameearly - Check eligibility before showing signup forms - Log blocked signups - Monitor failed signup attempts for security
- Test with different methods - Verify that all signup methods respect
disable_sign_ups - Use
screen_hint=signup- For invitation flows and onboarding - Monitor account linking - Ensure users with same email are properly linked
## Error Messages
When signup is blocked, users see:
- HTTP 400 status
- Message: "Public signup is disabled for this client" (or custom reason)
- Log entry with type `fs` (failed signup)
## Examples
### Example 1: Blocking Social Login Signups
```typescript
// 1. Configure client
await clients.update("tenant-id", "client-id", {
client_metadata: {
disable_sign_ups: "true",
},
});
// 2. User tries to sign up with Google
// Result: HTTP 400 - "Public signup is disabled for this client"
// 3. Existing user tries to log in with Google
// Result: Success - login proceeds normallyExample 2: Invite-Only Application
// 1. Disable public signups
await clients.update("tenant-id", "client-id", {
client_metadata: {
disable_sign_ups: "true",
},
});
// 2. Send invitation with screen_hint
const inviteUrl =
`https://your-domain/authorize?` +
`screen_hint=signup&` +
`client_id=client-id&` +
`redirect_uri=https://your-app/callback`;
// 3. User clicks invite link and signs up
// Result: Success - allowed via screen_hint=signupExample 3: Account Linking
// 1. User signs up with email/password
await fetch("/dbconnections/signup", {
method: "POST",
body: JSON.stringify({
email: "user@example.com",
password: "SecurePass123!",
connection: "Username-Password-Authentication",
client_id: "client-id",
}),
});
// 2. Later, disable public signups
await clients.update("tenant-id", "client-id", {
client_metadata: {
disable_sign_ups: "true",
},
});
// 3. User tries to log in with Google (same email)
// Result: Success - Google account is linked to existing account
// Reason: Account linking is allowed even when signups are disabled