Skip to main content

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>;
};
}
note

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

EventBlockingctx.data?.after payload
beforeSignUpYesDraft signup payload such as id, email, displayName, avatarUrl
afterSignUpNoSanitized created user
beforeSignInYesSanitized user about to sign in
afterSignInNoSanitized signed-in user
onTokenRefreshYesSanitized user whose token is being refreshed
beforePasswordResetYesUsually { userId }
afterPasswordResetNoSanitized user after password change/reset
beforeSignOutYes{ userId }
afterSignOutNo{ userId }
onDeleteAccountNoSanitized user snapshot before deletion
onEmailVerifiedNoSanitized 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 exposes get, list, insert, update, and delete.
  • 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.