rivetkit-client-swift

RivetKit Swift client guidance. Use for Swift clients that connect to Rivet Actors with RivetKitClient, create actor handles, call actions, or manage connections.

Skill file

Preview skill file
---
name: "rivetkit-client-swift"
description: "RivetKit Swift client guidance. Use for Swift clients that connect to Rivet Actors with RivetKitClient, create actor handles, call actions, or manage connections."
---

# RivetKit Swift Client

Use this skill when building Swift clients that connect to Rivet Actors with `RivetKitClient`.

## Version

RivetKit version: 2.3.0-rc.5

## Error Handling Policy

- Prefer fail-fast behavior by default.
- Avoid broad `do/catch` unless absolutely needed.
- If a catch block is used, handle the error explicitly, at minimum by logging it.

## Install

Add the Swift package dependency and import `RivetKitClient`:

```swift
// 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

```swift
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

```swift
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

```swift
import RivetKitClient

let config = try ClientConfig(endpoint: "http://localhost:6420")
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

```swift
import RivetKitClient

struct GameInput: Encodable {
    let mode: String
}

let config = try ClientConfig(endpoint: "http://localhost:6420")
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:

```swift
import RivetKitClient

let config = try ClientConfig(endpoint: "http://localhost:6420")
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:

```swift
import RivetKitClient

let config = try ClientConfig(endpoint: "http://localhost:6420")
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

```swift
import RivetKitClient

struct ConnParams: Encodable {
    let authToken: String
}

let config = try ClientConfig(endpoint: "http://localhost:6420")
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

```swift
import RivetKitClient

let config = try ClientConfig(endpoint: "http://localhost:6420")
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`:

```swift
import RivetKitClient

let config = try ClientConfig(endpoint: "http://localhost:6420")
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

```swift
import RivetKitClient

let config = try ClientConfig(endpoint: "http://localhost:6420")
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:

```swift
import RivetKitClient

let config = try ClientConfig(endpoint: "http://localhost:6420")
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.):

```swift
import RivetKitClient

let config = try ClientConfig(endpoint: "http://localhost:6420")
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

```swift
import RivetKitClient

let config = try ClientConfig(endpoint: "http://localhost:6420")
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`:

```swift
import RivetKitClient

let config = try ClientConfig(endpoint: "http://localhost:6420")
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:

```swift
import RivetKitClient

let config = try ClientConfig(endpoint: "http://localhost:6420")
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](/docs/general/endpoints#url-auth-syntax) for details.

## API Reference

### Client
- `RivetKitClient(config:)` - Create a client with a config
- `ClientConfig` - Configure endpoint, namespace, and token
- `client.get()` / `getOrCreate()` / `getForId()` / `create()` - Get actor handles
- `client.dispose()` - Dispose the client and all connections

### ActorHandle
- `handle.action(name, args..., as:)` - Stateless action call
- `handle.connect()` - Create a stateful connection
- `handle.resolve()` - Get the actor ID
- `handle.getGatewayUrl()` - Get the raw gateway URL
- `handle.fetch(path, request:)` - Raw HTTP request
- `handle.websocket(path:)` - Raw WebSocket connection

### ActorConnection
- `conn.action(name, args..., as:)` - Action call over WebSocket
- `conn.events(name, as:)` - AsyncStream of typed events
- `conn.statusChanges()` - AsyncStream of status changes
- `conn.errors()` - AsyncStream of connection errors
- `conn.opens()` - AsyncStream that yields on connection open
- `conn.closes()` - AsyncStream that yields on connection close
- `conn.currentStatus` - Current connection status
- `conn.dispose()` - Close the connection

### Types
- `ActorConnStatus` - Connection status enum (`.idle`, `.connecting`, `.connected`, `.disconnected`, `.disposed`)
- `ActorError` - Typed actor errors with `group`, `code`, `message`, `metadata`
- `JSONValue` - 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:

```bash
npx skills add rivet-dev/skills
```

Then use the `rivetkit` skill for backend guidance.

Source

Creator's repository · rivet-dev/skills

View on GitHub

Security

Security checks in progress
Results will appear here once audits complete
What this skill can do
Reads your filesConnects to the internetRuns code on your machine
Checked by 3 independent security firms
Does it try to trick the AI?Not yet checkedPending · Gen Agent Trust Hub
Does it sneak in hidden code?Not yet checkedPending · Socket
Does it have known bugs?Not yet checkedPending · Snyk