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
identityIdand 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 carrydisplayName,email, and arbitraryclaims.
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
- Data Governance -- consent, retention, and the right to delete.
- Permissions -- device permissions sit alongside identity, syncing into governance.
- Analytics -- tracking user journey events.
- React Integration -- using identity with the React adapter.
docs/guides/identity-and-continuity.md