Migration: Legacy Widget to Headless SDK
Date: 2026-05-11
This guide covers migrating one workflow at a time from the legacy Google Customer Engagement AI web widget to the framework-neutral headless SDK. Existing widget deployments stay supported; nothing here forces a full cutover.
1. chatSdk.registerContext to createChatClient
Legacy:
<script src="https://cdn.example.com/chat-widget.js"></script>
<script>
window.chatSdk.registerContext({
userId: 'u-123',
locale: 'en-US',
deployment: 'support-prod',
});
</script>
Headless:
import { createChatClient, tokenEndpointAuth } from 'gecx-chat';
const client = createChatClient({
auth: tokenEndpointAuth({ endpoint: '/api/gecx-chat-token' }),
deployment: 'support-prod',
});
const session = await client.createSession({
metadata: { userId: 'u-123', locale: 'en-US' },
});
Context that used to live in registerContext becomes the metadata argument on createSession or per-message send calls. Move secrets server-side: the browser only sees the customer-hosted token endpoint.
2. Widget DOM events to typed session handlers
Legacy widgets dispatch DOM events like chatSdk:message, chatSdk:tool-result, and chatSdk:reset. Two paths:
2a. Use the compatibility bridge (incremental)
import { createWidgetEventBridge } from 'gecx-chat/compat';
const bridge = createWidgetEventBridge({ session });
// On unmount:
bridge.dispose();
The bridge listens on window (or any EventTarget you pass) for legacy custom events and forwards them to the new ChatSession methods. Analytics listeners that already subscribe to chatSdk:* events keep working. This is the recommended first step when migrating one page at a time.
2b. Replace listeners with useChatSession (target state)
import { useChatSession } from 'gecx-chat/react';
const chat = useChatSession({ config });
chat.session?.on('message_added', ({ message }) => analytics.track('chat_message', message));
chat.session?.on('status_changed', ({ status }) => analytics.track('chat_status', { status }));
chat.session?.on('handoff_status_changed', ({ status }) => analytics.track('chat_handoff', { status }));
The typed session events replace string-based DOM events. The bridge from 2a is temporary scaffolding while listeners are ported.
3. Custom widget templates to renderer registry
Legacy templates are HTML strings registered with chatSdk.registerTemplate('order-card', html). Headless customers register typed renderers per ChatMessagePart type:
import { ChatProvider, MessagePart, createRendererRegistry } from 'gecx-chat/react';
import { createRichContentRegistry } from 'gecx-chat/rich-content';
const richContent = createRichContentRegistry();
richContent.register('order-card', { schema: orderCardSchema });
const renderers = {
'order-summary': (part) => <OrderCard order={part} />,
'product-carousel': (part) => <ProductGrid products={part.products} />,
'custom': (part) => part.payloadType === 'order-card' ? <OrderCard order={part.data} /> : null,
};
<ChatProvider client={client} renderers={renderers}>
{messages.map((m) => m.parts.map((p) => <MessagePart key={p.id} part={p} />))}
</ChatProvider>
Custom HTML is replaced by typed React components keyed on the discriminated part type. The SDK never emits Shadow-DOM markup -- your component library owns the chrome.
4. Client functions to defineClientTool
Legacy:
chatSdk.registerFunction('lookup_invoice', async (input) => {
return fetchInvoice(input.invoiceId);
});
Headless:
import { defineClientTool } from 'gecx-chat';
export const lookupInvoiceTool = defineClientTool({
name: 'lookup_invoice',
description: 'Look up an invoice for the signed-in user',
inputSchema: {
type: 'object',
required: ['invoiceId'],
properties: { invoiceId: { type: 'string' } },
},
timeoutMs: 10_000,
permissions: { requiresUserApproval: false },
execute: async ({ invoiceId }, ctx) => {
ctx.signal.throwIfAborted();
return fetchInvoice(invoiceId);
},
});
The differences from legacy registration:
- Inputs are validated against JSON Schema before your handler runs.
ctx.signallets you cancel in-flight requests when the user callssession.stop().- Timeouts and approval hooks are first-class config, not implementation details.
5. Backend agent: no changes required
The agent definition, deployment, tools registered server-side, and routing logic are unchanged. Migration is strictly a UI- and event-consumption refactor.
6. Verification checklist
After migrating a workflow, verify:
pnpm typecheckpasses with no remaining references to the legacychatSdkglobal.- Browser DevTools network panel shows requests only to the customer-hosted token endpoint and (if applicable) the proxy endpoint -- no direct calls to legacy widget CDN URLs.
- Existing analytics listeners on
chatSdk:*events still fire (verified via the compatibility bridge). - Playwright/E2E tests for the migrated route pass against the new mock transport.
pnpm gecx doctorreports green for token endpoint, deployment ID, and CSP.
7. Out of scope (post-GA)
- Automated codemods.
- Migration of voice or video workflows.
- Mobile SDK migration.
What's next
- React Integration -- the full React adapter API for the headless SDK.
- Client Tools -- defining tools with schemas, timeouts, and approval hooks.
- Custom Renderers -- building typed renderers to replace widget templates.
- Identity & Continuity -- how guest-to-authenticated upgrade works.
- Testing -- mock transport for testing migrated flows.
docs/guides/migration-from-widget.md