iOS / CHAT SDK
Add a MultiVibe assistant to your app.
Embed a native chat with sign-in, model selection, streaming responses and actions supplied by your application. Users bring their MultiVibe account and credits. This native preview keeps each application’s history separate.
This guide covers MultiVibeSDK and MultiVibeChatUI. The Swift API client in the Cloud repository is a separate, lower-level package with an externally supplied token provider.
1. Install the Swift package
Requires Swift 6 and iOS 18 or later. In Xcode, add the local multivibe Core checkout, or its native/sdk/MultiVibeSDK directory, as a package dependency. Link MultiVibeSDK and MultiVibeChatUI to your app target. Add only one of those package locations: they export the same code.
The package has no external dependencies. Once a tested commit is published and you have repository access, use the repository root with an explicit revision:
.package(
url: "https://github.com/thibautrey/multivibe.git",
revision: "PUBLISHED_COMMIT_SHA"
)
// In your app target's dependencies:
.product(name: "MultiVibeSDK", package: "multivibe"),
.product(name: "MultiVibeChatUI", package: "multivibe")
Replace the revision placeholder with a published commit containing the root Package.swift. Do not assume a version tag or similarly named registry package exists.
2. Register your iOS application
- Open My apps, create an application and register its exact HTTPS callback, for example
https://app.example.com/multivibe/callback. Copy the public client UUID. - Expand Configure iOS SDK, enter your bundle identifier and callback domain, then select Set iOS registration. All registered redirects must use that domain; wildcards, query strings and fragments are not supported.
- Publish the displayed TXT record at
_multivibe.app.example.comwith its exact generated value, then select Verify domain. Changing the registered callbacks requires verification again. - Enable Associated Domains on your signed app and add
applinks:app.example.com. Serve the following JSON over HTTPS athttps://app.example.com/.well-known/apple-app-site-association, using your own Apple Team ID and bundle identifier.
{
"applinks": {
"details": [{
"appIDs": ["YOUR_TEAM_ID.com.example.app"],
"components": [{ "/": "/multivibe/callback" }]
}]
}
}
Use application/json and serve the association file without a redirect. The portal records your bundle identifier and domain; your Apple Team ID belongs in your association file. DNS verification and Apple's signed-app association are separate requirements. Each application needs its own client UUID. No client secret or project API key belongs in the app.
3. Embed the chat
The SwiftUI view provides sign-in, the model picker, conversation selection, Markdown responses, streaming and interruption controls. Keep it inside a NavigationStack so its toolbar is visible.
import SwiftUI
import MultiVibeSDK
import MultiVibeChatUI
@MainActor
struct AssistantView: View {
@State private var client = MultiVibeClient(configuration: .init(
clientID: "YOUR_PORTAL_UUID",
redirectURI: URL(string:
"https://app.example.com/multivibe/callback")!
))
var body: some View {
NavigationStack {
MultiVibeChatView(client: client)
.navigationTitle("Assistant")
.tint(.indigo)
}
}
}
For UIKit, push the hosting controller onto your navigation controller using the same configured client:
let chat = MultiVibeChatViewController(client: client)
navigationController?.pushViewController(chat, animated: true)
Use SwiftUI's .tint and .navigationTitle to fit your app. Both interfaces also accept tools and contextProvider.
4. Connect the user's account
Sign-in uses ASWebAuthenticationSession and the existing MultiVibe web session when available. The MultiVibe page collects consent and unlocks recovery locally. The SDK sends its installation public key as history_key and receives only app-derived keys wrapped to that key. Its private key stays in Keychain; your app never requests recovery or receives global history keys.
The user authorizes model access, consumption on their paying project and synchronization of your application's conversations and shared context. The SDK requests models:read chat:write conversations:read conversations:write. It handles PKCE, callback state validation, token exchange, private Keychain storage and refresh rotation. Read the OAuth protocol if you build a custom connection screen.
MultiVibeChatView handles incoming URLs while mounted. With a custom flow, use MultiVibeAuthenticationPresenter(window:), or beginAuthorization() and handleOpenURL(_:), and deliver each OAuth callback exactly once. Keep the client alive during authorization. A callback contains a one-use code, not the official MultiVibe session.
For a custom SwiftUI screen, use MultiVibeConnectButton as the standard connection control and invoke MultiVibeAuthenticationPresenter from its action. It reflects progress without receiving tokens, recovery codes or conversation keys.
If the host application must not see messages or keys, use MultiVibeIsolatedChatButton. It opens the application-scoped Chat in Safari and passes only the public client UUID. This isolated mode has encrypted history and the familiar model picker, but no host tools.
disconnect() revokes the application's grant. Users can also revoke access from MultiVibe or Connected applications. Revocation preserves their conversation history. Inference still depends on available credits, quotas and model availability; connecting grants no credits and does not authorize purchases.
5. Supply context and actions
A context provider returns the app data needed for the current conversation. This text is sent to the selected model and saved with the conversation, so include only data the user agreed to share.
struct ScreenContext: MultiVibeContextProvider {
let selectedProject: String
func context() async throws -> String {
"Selected project: " + selectedProject
}
}
// Inside your view:
MultiVibeChatView(
client: client,
tools: tools,
contextProvider: ScreenContext(selectedProject: "Summer trip")
)
Declare each action with a JSON schema and an asynchronous executor. The following adapter takes your existing app operation as a closure; it does not implement or claim to create a note itself.
func makeNoteTool(
createNote: @escaping @Sendable (String) async throws -> String
) -> MultiVibeTool {
MultiVibeTool(
name: "create_note",
description: "Create a note in this application",
parameters: .object([
"type": .string("object"),
"properties": .object([
"title": .object(["type": .string("string")])
]),
"required": .array([.string("title")]),
"additionalProperties": .bool(false)
]),
modifiesData: true
) { arguments in
guard case .object(let fields) = arguments,
case .string(let title) = fields["title"],
!title.isEmpty, title.count <= 200 else {
throw MultiVibeError.invalidArguments
}
let noteID = try await createNote(title)
return .object(["noteID": .string(noteID)])
}
}
Mark every data-changing action modifiesData: true: the chat asks for confirmation before entering its executor. Use false for read-only tools. Your executor remains responsible for app authorization, business rules and any iOS permission prompt.
Supported schema keywords are type, properties, required, additionalProperties, items, enum, description and title. Unsupported constraints are rejected; keep additional validation in the executor.
A response can execute at most eight tool calls, with a 30-second deadline per executor. The local journal records execution before handing control to the app and prevents automatic replay of the same call after interruption. Executors should honor cancellation. If an executor ignores cancellation, the SDK stops waiting but cannot forcibly stop your operation; verify the app's actual state before starting another action.
The Swift preview loads Cloud, connected-account and Relay models through the application API. Relay remains on the exact selected relay/<machine>/<model>; it never falls back to paid Cloud inference. During one tool response, the SDK carries the server continuation receipt between rounds, stops before a host tool if that receipt is missing and starts the next user message without the old receipt. The official account-owner client does not execute third-party tools.
The picker identifies models with explicit tool support. For other models, the built-in view explains that tools are unavailable and sends text only, without silently changing the selected model. Passing localProvider: AppleFoundationLocalProvider() when creating the client optionally adds Apple Foundation Local on eligible iOS 26+ devices. That adapter is text-only; it imports none of the official app's tools, permissions or memory. Conversation synchronization still requires the connected Cloud account.
6. Understand history and privacy
Your application reads and writes only its own encrypted v2 conversations. Web, Swift and Android use the same application folder. The official account-owner client can read those folders, including older encrypted v1 envelopes. Legacy plaintext storage remains separate and is not automatically migrated.
The Open in application link returns to your registered callback with a conversationId. The built-in view resolves it against your app's own history. Custom URL handlers should call conversationFromOpenURL(_:) for a conversation link and handleOpenURL(_:) for an OAuth callback.
For your own UI, use connect(), reload(), newConversation(), save(_:operationID:), delete(_:) and respond(to:tools:contextProvider:confirm:update:). Supply a confirmation callback when using the client directly; the ready-made UI supplies its own dialog. The accountOwner mode is reserved for the official app and does not grant third-party access to other folders or global memory.
History uses revisions and idempotent encrypted writes. Conflicts require reload; no client silently overwrites another revision. Deleting removes the conversation, while revocation removes app access and preserves user history. Rotation requires reconnecting on the MultiVibe consent page.
Handle failures explicitly
- Sign-in expired or uncertain refresh: reconnect rather than replay a rotating refresh token.
- Credits, permissions or unavailable model: surface the error and let the user choose how to proceed. Never substitute a model silently.
- Interrupted stream: partial text is preserved when possible. The user can explicitly resume; this starts another inference and may incur usage.
- Version conflict: the built-in Reload action adopts server state. Retain the draft in your own UI if you need a merge workflow.
- Unknown tool outcome: inspect the real app state before requesting the action again. A timeout does not prove that the operation did not happen.
For an uncertain save, call retryHistoryWrite() to resend the persisted envelope and operation ID, or explicitly abandon and reload. Calling save again is not a retry. No inference or tool is replayed.
Run the examples
In Core, open native/sdk/Examples/MultiVibeSDKExamples.xcodeproj. The NotesExample and TasksExample targets each expose read/create actions backed by their own app sandbox. Register separate clients and set each target's SDK_CLIENT_ID, SDK_CALLBACK_HOST, bundle identifier and signing team. Unconfigured examples display a setup screen.
# From the Core repository checkout:
swift test
# Or test the nested package:
swift test --package-path native/sdk/MultiVibeSDK
Before shipping, verify on signed physical devices: app and web sign-in, both apps' isolated histories, confirmation and denial, interrupted actions, switching accounts, revocation and links back to the originating conversation. Local builds do not establish those behaviors against a deployed Cloud.
The current integration covers text chat, context and foreground tools. Attachments, realtime voice, background actions and developer-funded usage are outside this version. The Core package README and Cloud's docs/ios-sdk.md describe the underlying implementation and API contract.