Auth Hooks
EdgeBase auth hooks let you run server-side logic during authentication flows such as sign-up, sign-in, token refresh, password reset, sign-out, account deletion, and email verification.
Auth hooks are defined as App Functions with trigger.type = 'auth'.
import { defineFunction } from '@edgebase/shared';
export default defineFunction({
trigger: { type: 'auth', event: 'afterSignUp' },
handler: async (ctx) => {
await ctx.admin.db('shared').table('profiles').insert({
userId: String(ctx.data?.after?.id),
displayName: String(ctx.data?.after?.displayName ?? 'New User'),
});
},
});
Context Shape
Auth hooks receive a single context object.
interface AuthHookContext {
request: Request;
auth: null;
admin: {
db(namespace: string, id?: string): { table(name: string): TableProxy };
table(name: string): TableProxy;
auth: {
getUser(userId: string): Promise<Record<string, unknown>>;
listUsers(options?: { limit?: number; offset?: number }): Promise<unknown>;
updateUser(userId: string, data: Record<string, unknown>): Promise<Record<string, unknown> | null>;
setCustomClaims(userId: string, claims: Record<string, unknown>): Promise<void>;
revokeAllSessions(userId: string): Promise<void>;
};
sql(namespace: string, id: string | undefined, query: string, params?: unknown[]): Promise<unknown[]>;
broadcast(channel: string, event: string, payload?: Record<string, unknown>): Promise<void>;
functions: { call(name: string, data?: unknown): Promise<unknown> };
kv(namespace: string): unknown;
d1(database: string): unknown;
vector(index: string): unknown;
push: unknown;
};
data?: {
after?: Record<string, unknown>;
};
}
Inside auth hooks, ctx.admin.auth.createUser() and ctx.admin.auth.deleteUser() are intentionally unavailable. Use the Admin API or an App Function outside the auth hook pipeline for those operations.
Supported Events
| Event | Blocking | ctx.data?.after payload |
|---|---|---|
beforeSignUp | Yes | Draft signup payload such as id, email, displayName, avatarUrl |
afterSignUp | No | Sanitized created user |
beforeSignIn | Yes | Sanitized user about to sign in |
afterSignIn | No | Sanitized signed-in user |
onTokenRefresh | Yes | Sanitized user whose token is being refreshed |
beforePasswordReset | Yes | Usually { userId } |
afterPasswordReset | No | Sanitized user after password change/reset |
beforeSignOut | Yes | { userId } |
afterSignOut | No | { userId } |
onDeleteAccount | No | Sanitized user snapshot before deletion |
onEmailVerified | No | Sanitized verified user |
Blocking hooks can reject the operation by throwing. Non-blocking hooks run via waitUntil() semantics and do not affect the client response once the main auth action succeeds.
Common Patterns
Reject a Signup
import { defineFunction, FunctionError } from '@edgebase/shared';
export default defineFunction({
trigger: { type: 'auth', event: 'beforeSignUp' },
handler: async (ctx) => {
const email = String(ctx.data?.after?.email ?? '');
if (email.endsWith('@blocked.example')) {
throw new FunctionError('forbidden', 'This domain is not allowed.');
}
},
});
Create a Profile After Signup
import { defineFunction } from '@edgebase/shared';
export default defineFunction({
trigger: { type: 'auth', event: 'afterSignUp' },
handler: async (ctx) => {
const user = ctx.data?.after;
if (!user?.id) return;
await ctx.admin.db('shared').table('profiles').insert({
userId: String(user.id),
displayName: String(user.displayName ?? 'New User'),
});
},
});
Add Claims on Token Refresh
onTokenRefresh is the only auth hook whose return value is fed back into token generation.
import { defineFunction } from '@edgebase/shared';
export default defineFunction({
trigger: { type: 'auth', event: 'onTokenRefresh' },
handler: async (ctx) => {
const userId = String(ctx.data?.after?.id ?? '');
if (!userId) return;
const { items } = await ctx.admin.db('shared').table('subscriptions').list({
limit: 1,
filter: [['userId', '==', userId]],
});
const active = items[0];
return {
plan: active?.plan ?? 'free',
subscriptionStatus: active?.status ?? 'inactive',
};
},
});
The returned object overrides stored customClaims keys with the same name for the new access token.
Revoke Sessions After Password Reset
import { defineFunction } from '@edgebase/shared';
export default defineFunction({
trigger: { type: 'auth', event: 'afterPasswordReset' },
handler: async (ctx) => {
const userId = String(ctx.data?.after?.id ?? '');
if (!userId) return;
await ctx.admin.auth.revokeAllSessions(userId);
await ctx.admin.db('shared').table('activity_log').insert({
type: 'password_reset',
userId,
timestamp: new Date().toISOString(),
});
},
});
Audit Sign-Out
import { defineFunction } from '@edgebase/shared';
export default defineFunction({
trigger: { type: 'auth', event: 'afterSignOut' },
handler: async (ctx) => {
const userId = String(ctx.data?.after?.userId ?? '');
if (!userId) return;
await ctx.admin.db('shared').table('activity_log').insert({
type: 'sign_out',
userId,
timestamp: new Date().toISOString(),
});
},
});
Clean Up External Systems on Account Deletion
import { defineFunction } from '@edgebase/shared';
export default defineFunction({
trigger: { type: 'auth', event: 'onDeleteAccount' },
handler: async (ctx) => {
const user = ctx.data?.after;
if (!user?.id) return;
await fetch('https://analytics.example.com/api/delete-user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: user.id,
email: user.email,
}),
});
},
});
Notes
- Use
ctx.admin.db(...).table(...).list({ filter, limit })for table lookups inside auth hooks. The auth-hook admin proxy exposesget,list,insert,update, anddelete. - Use
ctx.admin.sql(...)when you need joins, aggregation, or bulk cleanup. - Throwing from a blocking hook returns a rejection to the caller. Non-blocking hooks only log failures.
- The auth hook pipeline always passes data through
ctx.data?.after; there is no legacy multi-argument handler signature in the current runtime.