Identity & Continuity

What identity is

Every chat session is tied to an identity. The SDK supports two kinds:

  • Guest -- created automatically when no authentication is present. The SDK generates a stable identityId and persists it in local storage so returning visitors keep their conversation history.
  • Authenticated -- created when your app provides an externalId (your user ID). Authenticated identities can carry displayName, email, and arbitrary claims.

A user always starts as a guest. When they sign in, you upgrade the identity in place. The guest's conversation history transfers to the authenticated identity.

IdentityManager

IdentityManager owns the current identity. The SDK creates it internally when you call createChatClient, but you interact with it through the client instance.

Key methods

getIdentity(): ChatIdentity -- returns the current identity synchronously. The returned object includes kind, identityId, externalId (if authenticated), displayName, email, claims, createdAt, and updatedAt.

upgradeToAuthenticated(input: UpgradeInput): Promise<ChatIdentity> -- promotes a guest to an authenticated identity. Requires externalId. Optionally accepts displayName, email, and claims. Claims are merged with any existing claims.

signOut(options?: SignOutOptions): Promise<ChatIdentity> -- signs the user out and creates a fresh guest identity. Pass { clearConversations: true } to also remove the conversation history for the previous identity.

setClaims(claims: Record<string, IdentityClaim>): Promise<ChatIdentity> -- merges new claims into the current identity without changing the identity kind.

subscribe(listener): Unsubscribe -- listens for identity changes. The listener receives an IdentityChangeEvent with identity, previous, and reason (one of bootstrap, upgrade, sign_out, claims_updated, cross_tab_sync, external_set).

ConversationRegistry

ConversationRegistry tracks the conversations belonging to the current identity. Each conversation is a ConversationDescriptor with conversationId, identityId, title, lastActiveAt, surface, and metadata.

Key methods

create(input?: CreateConversationInput): Promise<ConversationDescriptor> -- creates a new conversation. You can supply your own conversationId, a title, a surface label, and arbitrary metadata. If you omit conversationId, the SDK generates one.

list(): ConversationDescriptor[] -- returns all conversations for the current identity, sorted by lastActiveAt descending.

get(conversationId): ConversationDescriptor | undefined -- returns a single conversation by ID.

remove(conversationId): Promise<void> -- deletes a conversation from the registry.

importRemote(descriptors): Promise<void> -- merges an array of ConversationDescriptor objects into the local registry. Newer entries (by lastActiveAt) win when there is a conflict.

Cross-tab sync

When the user has your app open in multiple tabs, identity and conversation changes sync automatically via BroadcastChannel. If one tab calls upgradeToAuthenticated, every other tab receives an identity_changed event with reason cross_tab_sync.

No configuration is needed. The sync channel is created when the client starts. Environments without BroadcastChannel (SSR, older browsers) silently fall back to a no-op channel.

Cross-device continuity

For cross-device sync (user signs in on a new device), fetch the conversation list from your backend and call importRemote:

const remoteConversations = await fetch('/api/conversations', {
  headers: { Authorization: `Bearer ${userToken}` },
}).then((res) => res.json());

await client.conversations.importRemote(remoteConversations);

The registry merges remote entries with local ones. If a conversation exists in both, the entry with the later lastActiveAt wins.

Guest to authenticated upgrade

import { createChatClient, tokenEndpointAuth } from 'gecx-chat';

const client = createChatClient({
  auth: tokenEndpointAuth({ endpoint: '/api/chat-token' }),
});

// User is a guest at this point.
const guest = client.identity.getIdentity();
console.log(guest.kind); // 'guest'

// After the user signs in to your app:
const authenticated = await client.identity.upgradeToAuthenticated({
  externalId: 'user-abc-123',
  displayName: 'Jane Doe',
  email: 'jane@example.com',
  claims: { plan: 'pro', orgId: 'org-456' },
});
console.log(authenticated.kind); // 'authenticated'

Sign-out

// Listen for sign-out before it happens:
client.identity.onSignedOut(({ previousIdentityId }) => {
  console.log('Signed out from', previousIdentityId);
});

// Sign out and reset to a fresh guest:
await client.identity.signOut({ clearConversations: true });

Voice and signal subsystems inherit identity

Voice and signal subsystems do not own their own identity primitives. VoiceSession borrows chatSession.identity verbatim — the same sessionId, the same identityId, the same X-Ceai-Identity-* headers on the wire. SignalRunner and SignalEscalator operate on the parts stream owned by the chat session and never reach beyond it. Memory, similarly, scopes facts to the current identity and migrates them when a guest upgrades to authenticated. The single identity surface is the authoritative one.

What's next

Source: docs/guides/identity-and-continuity.md