IntegrateIntegrate
Database

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.

On this page