rivetkit-client-swift
RivetKit Swift Client
Use this skill when building Swift clients that connect to Rivet Actors with RivetKitClient.
Version
RivetKit version: 2.0.42
Install
Add the Swift package dependency and import RivetKitClient:
// Package.swift
dependencies: [
.package(url: "https://github.com/rivet-dev/rivetkit-swift", from: "2.0.0")
]
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "RivetKitClient", package: "rivetkit-swift")
]
)
]
Minimal Client
Endpoint URL
import RivetKitClient
let config = try ClientConfig(
endpoint: "https://my-namespace:pk_...@api.rivet.dev"
)
let client = RivetKitClient(config: config)
let handle = client.getOrCreate("counter", ["my-counter"])
let count: Int = try await handle.action("increment", 1, as: Int.self)
Explicit Fields
import RivetKitClient
let config = try ClientConfig(
endpoint: "https://api.rivet.dev",
namespace: "my-namespace",
token: "pk_..."
)
let client = RivetKitClient(config: config)
let handle = client.getOrCreate("counter", ["my-counter"])
let count: Int = try await handle.action("increment", 1, as: Int.self)
Stateless vs Stateful
import RivetKitClient
let config = try ClientConfig(endpoint: "http://localhost:3000/api/rivet")
let client = RivetKitClient(config: config)
let handle = client.getOrCreate("counter", ["my-counter"])
// Stateless: each call is independent
let current: Int = try await handle.action("getCount", as: Int.self)
print("Current count: \(current)")
// Stateful: keep a connection open for realtime events
let conn = handle.connect()
// Subscribe to events using AsyncStream
let eventTask = Task {
for await count in await conn.events("count", as: Int.self) {
print("Event: \(count)")
}
}
_ = try await conn.action("increment", 1, as: Int.self)
eventTask.cancel()
await conn.dispose()
await client.dispose()
Getting Actors
import RivetKitClient
struct GameInput: Encodable {
let mode: String
}
let config = try ClientConfig(endpoint: "http://localhost:3000/api/rivet")
let client = RivetKitClient(config: config)
// Get or create an actor
let room = client.getOrCreate("chatRoom", ["room-42"])
// Get an existing actor (fails if not found)
let existing = client.get("chatRoom", ["room-42"])
// Create a new actor with input
let created = try await client.create(
"game",
["game-1"],
options: CreateOptions(input: GameInput(mode: "ranked"))
)
// Get actor by ID
let byId = client.getForId("chatRoom", "actor-id")
// Resolve actor ID
let resolvedId = try await room.resolve()
print("Resolved ID: \(resolvedId)")
await client.dispose()
Actions support positional overloads for 0–5 args:
import RivetKitClient
let config = try ClientConfig(endpoint: "http://localhost:3000/api/rivet")
let client = RivetKitClient(config: config)
let handle = client.getOrCreate("counter", ["my-counter"])
let count: Int = try await handle.action("getCount")
let updated: String = try await handle.action("rename", "new-name")
let ok: Bool = try await handle.action("setScore", "user-1", 42)
print("Count: \(count), Updated: \(updated), OK: \(ok)")
await client.dispose()
If you need more than 5 arguments, use the raw JSON fallback:
import RivetKitClient
let config = try ClientConfig(endpoint: "http://localhost:3000/api/rivet")
let client = RivetKitClient(config: config)
let handle = client.getOrCreate("counter", ["my-counter"])
let args: [JSONValue] = [
.string("user-1"),
.number(.int(42)),
.string("extra"),
.string("more"),
.string("args"),
.string("here")
]
let ok: Bool = try await handle.action("setScore", args: args, as: Bool.self)
print("OK: \(ok)")
await client.dispose()
Connection Parameters
import RivetKitClient
struct ConnParams: Encodable {
let authToken: String
}
let config = try ClientConfig(endpoint: "http://localhost:3000/api/rivet")
let client = RivetKitClient(config: config)
let chat = client.getOrCreate(
"chatRoom",
["general"],
options: GetOrCreateOptions(params: ConnParams(authToken: "jwt-token-here"))
)
let conn = chat.connect()
// Use the connection...
for await status in await conn.statusChanges() {
print("Status: \(status.rawValue)")
if status == .connected {
break
}
}
await conn.dispose()
await client.dispose()
Subscribing to Events
import RivetKitClient
let config = try ClientConfig(endpoint: "http://localhost:3000/api/rivet")
let client = RivetKitClient(config: config)
let conn = client.getOrCreate("chatRoom", ["general"]).connect()
// Subscribe to events using AsyncStream
let messageTask = Task {
for await (from, body) in await conn.events("message", as: (String, String).self) {
print("\(from): \(body)")
}
}
// For one-time events, break after receiving
let gameOverTask = Task {
for await _ in await conn.events("gameOver", as: Void.self) {
print("done")
break
}
}
// Let it run for a bit
try await Task.sleep(for: .seconds(5))
// Cancel when done
messageTask.cancel()
gameOverTask.cancel()
await conn.dispose()
await client.dispose()
Event streams support 0–5 typed arguments. If you need raw values or more than 5 arguments, use JSONValue:
import RivetKitClient
let config = try ClientConfig(endpoint: "http://localhost:3000/api/rivet")
let client = RivetKitClient(config: config)
let conn = client.getOrCreate("chatRoom", ["general"]).connect()
let rawTask = Task {
for await args in await conn.events("message") {
print(args)
}
}
try await Task.sleep(for: .seconds(5))
rawTask.cancel()
await conn.dispose()
await client.dispose()
Connection Lifecycle
import RivetKitClient
let config = try ClientConfig(endpoint: "http://localhost:3000/api/rivet")
let client = RivetKitClient(config: config)
let conn = client.getOrCreate("chatRoom", ["general"]).connect()
// Monitor status changes (immediately yields current status)
let statusTask = Task {
for await status in await conn.statusChanges() {
print("status: \(status.rawValue)")
}
}
// Monitor errors
let errorTask = Task {
for await error in await conn.errors() {
print("error: \(error.group).\(error.code)")
}
}
// Monitor open/close events
let openTask = Task {
for await _ in await conn.opens() {
print("connected")
}
}
let closeTask = Task {
for await _ in await conn.closes() {
print("disconnected")
}
}
// Check current status
let current = await conn.currentStatus
print("Current status: \(current.rawValue)")
// Let it run for a bit
try await Task.sleep(for: .seconds(5))
// Cleanup
statusTask.cancel()
errorTask.cancel()
openTask.cancel()
closeTask.cancel()
await conn.dispose()
await client.dispose()
Low-Level HTTP & WebSocket
For actors that implement onRequest or onWebSocket, you can call them directly:
import RivetKitClient
let config = try ClientConfig(endpoint: "http://localhost:3000/api/rivet")
let client = RivetKitClient(config: config)
let handle = client.getOrCreate("chatRoom", ["general"])
// Raw HTTP request
let response = try await handle.fetch("history")
let history: [String] = try response.json([String].self)
print("History: \(history)")
// Raw WebSocket connection
let websocket = try await handle.websocket(path: "stream")
try await websocket.send(text: "hello")
let message = try await websocket.receive()
print("Received: \(message)")
await client.dispose()
Calling from Backend
Use the same client in server-side Swift (Vapor, Hummingbird, etc.):
import RivetKitClient
let config = try ClientConfig(endpoint: "http://localhost:3000/api/rivet")
let client = RivetKitClient(config: config)
let handle = client.getOrCreate("counter", ["server-counter"])
let count: Int = try await handle.action("increment", 1, as: Int.self)
print("Count: \(count)")
await client.dispose()
Error Handling
import RivetKitClient
let config = try ClientConfig(endpoint: "http://localhost:3000/api/rivet")
let client = RivetKitClient(config: config)
do {
_ = try await client.getOrCreate("user", ["user-123"])
.action("updateUsername", "ab", as: String.self)
} catch let error as ActorError {
print("Error code: \(error.code)")
print("Metadata: \(String(describing: error.metadata))")
}
await client.dispose()
If you need an untyped response, you can decode to JSONValue:
import RivetKitClient
let config = try ClientConfig(endpoint: "http://localhost:3000/api/rivet")
let client = RivetKitClient(config: config)
let handle = client.getOrCreate("data", ["raw"])
let value: JSONValue = try await handle.action("getRawPayload")
print("Raw value: \(value)")
await client.dispose()
Concepts
Keys
Keys uniquely identify actor instances. Use compound keys (arrays) for hierarchical addressing:
import RivetKitClient
let config = try ClientConfig(endpoint: "http://localhost:3000/api/rivet")
let client = RivetKitClient(config: config)
// Use compound keys for hierarchical addressing
let room = client.getOrCreate("chatRoom", ["org-acme", "general"])
let actorId = try await room.resolve()
print("Actor ID: \(actorId)")
await client.dispose()
Don't build keys with string interpolation like "org:\(userId)" when userId contains user data. Use arrays instead to prevent key injection attacks.
Environment Variables
ClientConfig reads optional values from environment variables:
RIVET_NAMESPACE- Namespace (can also be in endpoint URL)RIVET_TOKEN- Authentication token (can also be in endpoint URL)RIVET_RUNNER- Runner name (defaults to"default")
The endpoint parameter is always required. There is no default endpoint.
Endpoint Format
Endpoints support URL auth syntax:
https://namespace:token@api.rivet.dev
You can also pass the endpoint without auth and provide RIVET_NAMESPACE and RIVET_TOKEN separately. For serverless deployments, set the endpoint to your app's /api/rivet URL. See Endpoints for details.
API Reference
Client
RivetKitClient(config:)- Create a client with a configClientConfig- Configure endpoint, namespace, and tokenclient.get()/getOrCreate()/getForId()/create()- Get actor handlesclient.dispose()- Dispose the client and all connections
ActorHandle
handle.action(name, args..., as:)- Stateless action callhandle.connect()- Create a stateful connectionhandle.resolve()- Get the actor IDhandle.getGatewayUrl()- Get the raw gateway URLhandle.fetch(path, request:)- Raw HTTP requesthandle.websocket(path:)- Raw WebSocket connection
ActorConnection
conn.action(name, args..., as:)- Action call over WebSocketconn.events(name, as:)- AsyncStream of typed eventsconn.statusChanges()- AsyncStream of status changesconn.errors()- AsyncStream of connection errorsconn.opens()- AsyncStream that yields on connection openconn.closes()- AsyncStream that yields on connection closeconn.currentStatus- Current connection statusconn.dispose()- Close the connection
Types
ActorConnStatus- Connection status enum (.idle,.connecting,.connected,.disconnected,.disposed)ActorError- Typed actor errors withgroup,code,message,metadataJSONValue- Raw JSON value for untyped responses
Need More Than the Client?
If you need more about Rivet Actors, registries, or server-side RivetKit, add the main skill:
npx skills add rivet-dev/skills
Then use the rivetkit skill for backend guidance.