Hooks
Extend database adapters with app-specific behavior
Database adapters accept optional hooks to customize token persistence and trigger authorization without forking the SDK.
drizzleAdapter(db, {
provider: "pg",
schema: { providerToken, trigger },
hooks: {
onTokenChange: async ({ userId, provider, action }) => { /* ... */ },
resolveAccountIdentity: async (provider, tokenData, emailHint, context) => { /* ... */ },
authorizeTrigger: async (row, context) => { /* ... */ },
},
});onTokenChange
Called after a token is saved or removed.
onTokenChange: ({ userId, provider, action }) => {
// action: "set" | "remove"
revalidateTag(`library-integrations:${userId}`);
},Use for cache invalidation, analytics, or webhooks.
resolveAccountIdentity
Resolve account_email and account_id before upserting a token. The default derives accountId from provider:email when email is known.
For GitHub, fetch the user's profile when email is missing:
resolveAccountIdentity: async (provider, tokenData, emailHint) => {
let accountEmail = normalizeAccountEmail(emailHint);
let accountId = tokenData.accountId ?? null;
if (provider === "github" && (!accountEmail || !accountId)) {
const identity = await resolveGitHubIdentity(tokenData.accessToken);
accountEmail = accountEmail ?? identity.accountEmail;
accountId = accountId ?? identity.accountId;
}
if (!accountId && accountEmail) {
accountId = `${provider}:${accountEmail}`;
}
return { accountEmail, accountId };
},Import helpers from integrate-sdk/server:
import { normalizeAccountEmail } from "integrate-sdk/server";authorizeTrigger
Called when loading or mutating a trigger. Return a (possibly updated) row, or null to deny access.
Use for ownership repair when user_id on the trigger row is stale:
authorizeTrigger: async (row) => {
const ownerId = await resolveOwnerFromAutomationTables(row.id);
if (ownerId && ownerId !== row.userId) {
await db.update(trigger).set({ userId: ownerId }).where(eq(trigger.id, row.id));
return { ...row, userId: ownerId };
}
return row;
},After the hook, the adapter checks context.userId against the returned row's userId.
getSessionContext (not a hook)
Session resolution stays on createMCPServer, not the adapter. Wire your auth library or internal headers:
getSessionContext: async (req) => {
const internal = resolveCronActor(req);
if (internal) return internal;
const session = await auth.api.getSession({ headers: req.headers });
return session?.user?.id ? { userId: session.user.id } : undefined;
},Raw callbacks override hooks
If you pass explicit getProviderToken / setProviderToken / removeProviderToken or individual triggers.* callbacks on createMCPServer, those take precedence over the adapter. Hooks still run inside the adapter for operations the adapter handles.