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.