A vendor-neutral iOS support channel that can use Jake, Intercom Fin, or a company-owned agent without putting agent logic or secrets in the app.
The visible channel and the answering agent are deliberately different choices:
| Product | Responsibility |
|---|---|
SupportKitCore |
Identity, channel contract, capability model, and pinned-channel coordinator |
SupportKitUI |
Native SwiftUI conversation interface |
CustomAgentAdapter |
Router-backed native channel for Jake, Fin, or internal agents |
JakeSupportAdapter |
Optional opaque Jake hosted Messenger channel |
IntercomAdapter |
Optional opaque Intercom Messenger channel |
JakeSDK, SupportAdapterKit, and IntercomSupportAdapter remain available as compatibility
products during migration.
The universal path is SupportKitUI → RouterChannelAdapter → canonical router → selected agent.
That path supports agent hot-swapping through a normalized server-side handoff. Vendor Messenger
SDKs remain useful channels, but their iOS APIs do not make them arbitrary-agent transports; their
adapters therefore advertise externalAgentRouting == false.
Add this Swift package in Xcode. For the portable native experience, link:
SupportKitCoreSupportKitUICustomAgentAdapter
Link JakeSupportAdapter or IntercomAdapter only when you intentionally want that vendor's
hosted Messenger UI.
The hosted Jake Messenger SDK is also exposed through:
packages/jake_flutterfor Flutter on iOS and Androidpackages/react-native-jakefor React Native on iOS and Androidandroid/jake-sdkfor native Kotlin/Android applications
All three integrations expose the same lifecycle: configure, authenticate with a short-lived customer token, present/dismiss, logout, analytics, user attributes, push tokens, unread count, authentication-expiry events, and errors. The Flutter and React Native packages bridge to this Swift SDK on iOS and the native Kotlin SDK on Android.
The cross-platform packages currently wrap the hosted Messenger experience. The portable
SupportKitUI/router channel remains a native Swift implementation; cross-platform native
conversation widgets would be a separate UI layer over the documented router contract.
import CustomAgentAdapter
import SupportKitCore
import SupportKitUI
let channel = RouterChannelAdapter(
configuration: SupportRouterConfiguration(
baseURL: URL(string: "https://support-api.example.com")!,
agentProviderID: "jake", // or "intercom-fin" / "internal"
sessionToken: { try await applicationBackend.shortLivedSupportToken() }
)
)
let coordinator = SupportChannelCoordinator()
try coordinator.register(channel)
try coordinator.select(.native)
try await coordinator.startSession(
SupportChannelSession(
customer: SupportCustomer(
id: currentUser.databaseID,
externalID: currentUser.id,
name: currentUser.name,
email: currentUser.email
)
)
)
let model = SupportConversationModel(channel: channel)
let view = SupportConversationView(model: model)For remote routing, call SupportSelectionClient.select first and pass its agentProvider into
SupportRouterConfiguration. The selection endpoint derives rollout attributes server-side and
returns both the visible channel and agent. Cache it only for ttlSeconds, and never apply a new
selection to an active conversation.
The session token proves the customer identity. The router ignores client-supplied identity when authorizing existing conversations.
try await channel.requestAgentHandoff(
to: "intercom-fin",
conversationID: conversationID,
currentIntent: "refund invoice INV-42",
reason: "Fin rollout cohort"
)The server prepares the target first and commits the assignment atomically. If import fails, the source agent remains pinned. Configuration changes never silently move an active conversation.
import JakeSupportAdapter
try coordinator.register(
JakeChannelAdapter(
configuration: JakeConfiguration(
workspaceId: "workspace_123",
publicKey: "jake_pk_123"
)
)
)Intercom works the same way with IntercomChannelAdapter. Supply only short-lived user tokens or
JWTs from your backend. Never ship a Jake secret, Intercom API secret, Fin access token, MCP token,
or provider signing key in the application.
Use JakeSDK when the iOS experience must be the production web widget rather than a SwiftUI
recreation:
import JakeSDK
Jake.configure(
workspaceId: session.workspaceId,
publicKey: session.publicKey
)
try await Jake.authenticate(userId: currentUser.id, token: session.token)
Jake.present()The default Messenger URL is https://widget.tryjake.ai/messenger. Jake.present() mounts that
hosted widget directly in a native sheet, so its branding, conversation UI, composer, and behavior
stay aligned with the web widget.
For local simulator development, the example server includes a session-token bridge. It keeps the application secret on the Mac and returns only a short-lived customer session to the app:
cd Examples/SelectionServer
cp .env.example .env.local
# Add the application's public key and secret to .env.local.
./run-local.shThe launcher also accepts .env when that is your existing local convention. Both local
environment filenames are ignored by git.
The bridge listens on http://127.0.0.1:8787/mobile/support-token. A production application must
implement the same server-side responsibility in its own authenticated backend; it must never put
the Jake application secret in the iOS bundle.
Templates/customer-config is the optional per-customer repository. Schema v2 selects
defaultChannel independently from defaultAgentProvider, includes reviewed routing/evaluation
files, and references secret names rather than values. Git is the review/history layer; production
uses a validated immutable imported version, not a live checkout.
See architecture, backend contract, migration, and security.
swift test
node --test Examples/SelectionServer/selection.test.mjs
node scripts/validate-cross-platform.mjs
(cd packages/react-native-jake && npm test)Requires iOS 15+, Swift 6, and Xcode 16+. MIT licensed; Jake's proprietary runtime is not included.