Skip to content
Docs

Chat SDK with Edgenivo Passport

Use Edgenivo Passport to identify signed-in users of a browser chatbot and keep their conversations under one Chat SDK History API user key. A person can start a new conversation and continue using context from their earlier conversations.

Passport verifies the browser session. The Web Adapter uses that identity as the message author, and history.user.identity returns the author's ID as the history key. Chat SDK stores the messages in your state adapter.

The examples use lib and app at the project root. If your project uses src, put both directories under src.

Install Chat SDK, its Web and Redis adapters, Passport, and AI SDK:

Terminal
pnpm add chat @chat-adapter/web @chat-adapter/state-redis @vercel/passport ai

Create lib/passport.ts to authenticate each browser request. Disable Passport's development identity and reject missing or unverified identities:

lib/passport.ts
import { Buffer } from 'node:buffer';
import { getIdentity } from '@vercel/passport';
 
export async function getPassportUser(request: Request) {
  try {
    const identity = await getIdentity(request, { development: false });
    if (!identity?.verified || !identity.payload.iss) return null;
 
    const id = Buffer.from(
      JSON.stringify([identity.payload.iss, identity.subject]),
    ).toString('base64url');
 
    return { id, name: identity.name };
  } catch {
    return null;
  }
}

The key encodes the verified issuer and subject together. Base64url encoding removes the colons that the Web Adapter rejects in user IDs; it does not encrypt the identity. Keep the subject opaque and use the same encoding wherever you resolve this person's history key. Email addresses and session IDs are unsuitable as stable identifiers.

On Edgenivo, getIdentity(request) verifies the Passport token and checks the project and environment against the deployment's system environment variables. Keep VERCEL_PROJECT_ID and VERCEL_ENV available to the function. See Read Passport identity for the request header and verification behavior.

Create lib/bot.ts. The Web Adapter assigns the authenticated ID to author.userId, and Chat SDK resolves message.userKey before calling the handler:

lib/bot.ts
import { Chat, toPromptEntries } from 'chat';
import { createWebAdapter } from '@chat-adapter/web';
import { createRedisState } from '@chat-adapter/state-redis';
import { generateText } from 'ai';
import { getPassportUser } from './passport';
 
export const bot = new Chat({
  userName: 'my-bot',
  adapters: {
    web: createWebAdapter({
      userName: 'my-bot',
      protocol: 'ai-sdk',
      getUser: getPassportUser,
    }),
  },
  state: createRedisState(),
  history: {
    user: {
      identity: ({ adapter, author }) =>
        adapter === 'web' ? author.userId : null,
      retention: '30d',
      maxPerUser: 200,
    },
  },
});
 
bot.onDirectMessage(async (thread, message) => {
  const userKey = message.userKey;
  if (!userKey) return;
 
  const model = process.env.AI_GATEWAY_MODEL;
  if (!model) throw new Error('Set AI_GATEWAY_MODEL to an AI Gateway model ID.');
 
  await bot.history.user.append(thread, message);
  const recent = await bot.history.user.list({ userKey, limit: 20 });
 
  const { text } = await generateText({
    model,
    system: "Answer using the signed-in user's previous messages for context.",
    messages: toPromptEntries(recent),
    abortSignal: AbortSignal.timeout(60_000),
  });
 
  await thread.post(text);
  await bot.history.user.append(
    thread,
    { role: 'assistant', text },
    { userKey },
  );
});

The handler explicitly appends both the incoming message and the completed reply. history.user.list returns the latest 20 entries in chronological order, and toPromptEntries converts them into model messages. Posting a reply does not automatically add it to user history.

This example keeps up to 200 entries per user. The 30-day retention is an inactivity timeout for the entire user's history list, refreshed on each append. It is not a maximum age for each message. The Web Adapter's thread history is a separate store with its own retention settings.

Create app/api/chat/route.ts. Return the Web Adapter response directly and pass background work to Next.js after:

app/api/chat/route.ts
import { after } from 'next/server';
import { bot } from '../../../lib/bot';
 
export const maxDuration = 120;
 
export async function POST(request: Request) {
  return bot.webhooks.web(request, {
    waitUntil: (task) => after(() => task),
  });
}

Use the Node.js runtime, which is the App Router default. The 120-second function duration leaves time for identity verification, history retrieval, and delivery around the 60-second model timeout.

Connect the browser client from the Next.js guide to /api/chat. Keep the browser UI and endpoint on the same Passport-protected deployment so requests include the signed-in session. The server derives the user ID from Passport; the browser supplies only a conversation ID and message content.

Deploy to an environment with Passport enabled, then check the flow:

  1. Sign in and tell the bot a preference, such as your preferred programming language.
  2. Start another conversation by changing the client's threadId. Ask about the preference. The bot receives the same user's saved messages even though the conversation ID changed.
  3. Sign in as a different person. Their model context must contain only their own history, even if they use the same conversation ID.

A local request without a real Passport token returns HTTP 401 from the Web Adapter. This example deliberately rejects Passport's unverified development identity. On a protected deployment, Passport redirects unauthenticated browser requests to sign in before they reach the function.

Passport does not automatically connect a browser identity to a Slack, Teams, or Discord account. Before returning the same history key for another adapter, establish a verified account link and store the platform, workspace or tenant, and platform user ID alongside the Passport issuer and subject. Resolve that mapping on the server; do not match accounts by display name or an unverified email.

The example returns null for adapters other than web. A missing history key skips user-history persistence; it does not reject an event. Authenticate platform webhooks separately, and keep personal history out of shared-channel replies where other members could see it.


Was this helpful?

supported.