Tiny servers for multiplayer AI.

Each actor handles one request at a time and has its own SQLite database. The open-source alternative to Cloudflare Durable Objects, with no lock-in and observability built in.

bunx terse-cli@latest install
Read the quickstart →
my-actors/src
import { Actor, Interleave, Persisted } from "terse-sdk/actor"

export class ChatHistory extends Actor<…> {
  @Persisted messages: Message[] = []

  async onConnect(socket) {
    socket.send(this.messages)
  }

  @Interleave
  async onMessage(_socket, text: string) {
    this.messages.push({ role: "user", content: text })
    const result = streamText({ model, messages: this.messages })
    const reply = { role: "assistant", content: "" }
    this.messages.push(reply)
    for await (const chunk of result.textStream) {
      reply.content += chunk
      this.broadcast(this.messages)
    }
  }
}
its own SQLite@Persisted
WebSocket server
includedonConnect(socket)
define contract
via methodsonMessage(…)

Every resource and Agent gets its own little home.

Give each resource its own co-located micro-VM and SQLite database, coordinate edits from agents and humans without worrying about race conditions.

Build your actor, deploy it, add it to your product. That's it.

Step 1: Define your actor

Use our declarative API to build your actor. The methods become the RPC client, the generics become the API types, and @Persisted or this.db.exec() persist state.

counter/actor/src/actors.ts
import { Actor, Emittable, Persisted } from "terse-sdk/actor"

export class Counter extends Actor {
  @Persisted @Emittable value = 0

  async read() {
    return this.value
  }

  async increment() {
    return ++this.value
  }
}

Step 2: Deploy it

Run it locally while you build, then ship it to Terse Cloud with one command. Once it's deployed, your actors are reachable from anywhere in the world.

~/counter/actor
$ terse actor dev 
 actor server running

$ terse deploy 
 https://api.useterse.ai/YOUR_PROJECT_ID/actors

Step 3: Generate the client

Run terse actor generate and get a fully typed client for your backend, CLI or serverless function. Methods, WebSocket messages and state are all type-safe.

Read the TypeScript guide →
counter/client
$ terse actor generate
 Read 1 actor
 Wrote generated/index.ts

$ bun run start
count: 42
server.ts
import { actors } from "./generated"

// typed end to end
const count = await actors.Counter
  .get("counter").increment()

// hand a browser a socket for one chat
const grant = await actors.ChatHistory
  .prepareWebsocket({ actorId: "lobby" })

Anatomy of a Durable Actor

Every tiny server bundles its own compute, SQLite database and WebSocket server, and streams every write to GCS Rapid storage so nothing is lost.

Observability built in.

Every actor gets a live dashboard: request rate, queue wait and run time, each RPC call, and every WebSocket connection over its lifetime.

Terse dashboard for the ChatRoom actor room-launch-week: 3.6 requests per second, 15 ms average queue wait and 3.6 ms average request time, a timeline of RPC and WebSocket calls by method, and a lifetime chart of nine WebSocket connections.

Here is a sick counter demo.

This is the most durable, multiplayer safe counter you'll ever see.

Try Durable Actors now.