Skip to main content

Auth Hooks

Auth hooks let you customize and extend the authentication flow with server-side logic. They run inside App Functions with a special auth trigger.

Overview

HookTimingBlockingCan RejectTypical Use
beforeSignUpBefore user creationYes (5s)YesDomain validation, role assignment
afterSignUpAfter user creationNoNoWelcome email, profile creation
beforeSignInBefore session creationYes (5s)YesAccount suspension checks
afterSignInAfter successful loginNoNoActivity logging
onTokenRefreshOn JWT refreshYes (5s, fail-safe)NoDynamic custom claims
beforePasswordResetBefore password changeYes (5s)YesPassword policy enforcement

Defining Hooks

Define auth hooks in edgebase.config.ts:

import { defineConfig } from 'edgebase';

export default defineConfig({
functions: [
{
name: 'restrict-domain',
trigger: { type: 'auth', event: 'beforeSignUp' },
handler: async (ctx) => {
const email = ctx.data.after.email;
if (!email.endsWith('@company.com')) {
throw new Error('Only company emails are allowed.');
}
},
},
],
});

Hook Context API

ctx.data.after    // User data object { id, email, displayName, ... }
ctx.auth // Always null in auth hooks
ctx.admin.auth // Admin API (getUser, updateUser, setCustomClaims, revokeAllSessions, listUsers)
ctx.admin.db() // Cross-DO database access
Important Limitations
  • ctx.admin.auth.createUser() is not available inside hooks (requires D1)
  • ctx.admin.auth.deleteUser() is not available inside hooks (requires D1)
  • ctx.auth is always null — use ctx.data.after for user info

Blocking vs Non-Blocking

Blocking Hooks

beforeSignUp, beforeSignIn, onTokenRefresh, beforePasswordReset:

  • Awaited — blocks the operation until complete or timeout
  • Throw an error to reject the operation (returns 403 to client)
  • Return an object to modify data (for beforeSignUp and onTokenRefresh)
  • 5-second timeout (hardcoded)

Non-Blocking Hooks

afterSignUp, afterSignIn:

  • Fire-and-forget via ctx.waitUntil()
  • API response sent immediately
  • Errors logged but don't affect the client
  • Return values ignored

Common Patterns

1. Domain Restriction (beforeSignUp)

{
name: 'restrict-domain',
trigger: { type: 'auth', event: 'beforeSignUp' },
handler: async (ctx) => {
const email = ctx.data.after.email;
if (!email?.endsWith('@company.com')) {
throw new Error('Registration restricted to company emails.');
}
},
}

2. Auto Role Assignment (beforeSignUp)

{
name: 'assign-role',
trigger: { type: 'auth', event: 'beforeSignUp' },
handler: async (ctx) => {
const email = ctx.data.after.email;
if (email?.endsWith('@admin.company.com')) {
return { role: 'admin' };
}
return { role: 'member' };
},
}

3. Account Suspension Check (beforeSignIn)

{
name: 'check-suspension',
trigger: { type: 'auth', event: 'beforeSignIn' },
handler: async (ctx) => {
const user = await ctx.admin.auth.getUser(ctx.data.after.id);
if (user.customClaims?.suspended) {
throw new Error('Your account has been suspended.');
}
},
}

4. Welcome Email (afterSignUp)

{
name: 'welcome-email',
trigger: { type: 'auth', event: 'afterSignUp' },
handler: async (ctx) => {
const user = ctx.data.after;
await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: { Authorization: 'Bearer re_xxx', 'Content-Type': 'application/json' },
body: JSON.stringify({
from: 'welcome@myapp.com',
to: user.email,
subject: 'Welcome!',
html: `<p>Hi ${user.displayName || 'there'}!</p>`,
}),
});
},
}

5. Dynamic Custom Claims (onTokenRefresh)

{
name: 'inject-claims',
trigger: { type: 'auth', event: 'onTokenRefresh' },
handler: async (ctx) => {
const userId = ctx.data.after.id;
// Fetch subscription from your database
const sub = await ctx.admin.db('main').table('subscriptions')
.get({ filter: `userId = "${userId}"` });
return {
plan: sub?.plan || 'free',
features: sub?.features || [],
};
},
}
note

onTokenRefresh is fail-safe — if it errors or times out, token refresh proceeds without hook claims.

6. Password Policy (beforePasswordReset)

{
name: 'password-policy',
trigger: { type: 'auth', event: 'beforePasswordReset' },
handler: async (ctx) => {
const user = await ctx.admin.auth.getUser(ctx.data.after.id);
if (user.customClaims?.passwordLocked) {
throw new Error('Password changes are disabled for this account.');
}
},
}

Timeout

The hook timeout is 5 seconds (fixed) and cannot be configured.

Custom Claims Priority

When both setCustomClaims() and onTokenRefresh return claims, the hook return value takes priority on conflicts:

Stored customClaims = { plan: 'free', region: 'us' }
onTokenRefresh return = { plan: 'pro' }
Final JWT custom = { plan: 'pro', region: 'us' } // hook wins on conflict

Protected claims that cannot be overridden: sub, iss, exp, iat, isAnonymous.

Next Steps

  • App Functions — Complete App Functions documentation including database triggers
  • Password Policy — Built-in password validation rules (complementary to beforePasswordReset hook)
  • Session Management — Session limits, eviction, and token rotation details