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
| Hook | Timing | Blocking | Can Reject | Typical Use |
|---|---|---|---|---|
beforeSignUp | Before user creation | Yes (5s) | Yes | Domain validation, role assignment |
afterSignUp | After user creation | No | No | Welcome email, profile creation |
beforeSignIn | Before session creation | Yes (5s) | Yes | Account suspension checks |
afterSignIn | After successful login | No | No | Activity logging |
onTokenRefresh | On JWT refresh | Yes (5s, fail-safe) | No | Dynamic custom claims |
beforePasswordReset | Before password change | Yes (5s) | Yes | Password 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.authis alwaysnull— usectx.data.afterfor 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
beforeSignUpandonTokenRefresh) - 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
beforePasswordResethook) - Session Management — Session limits, eviction, and token rotation details