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 installimport { 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) } } }
// Generated by `terse actor generate`: typed end to end. import { actors } from "./generated" // Server: grant a browser a socket for one chat. const grant = await actors.ChatHistory.prepareWebsocket({ actorId: "launch-week", metadata: null, }) // Browser: onConnect sends the full history on join. const socket = new WebSocket(grant.websocketUrl) socket.onmessage = (event) => { const messages: actors.ChatHistory.Outgoing = JSON.parse(event.data) render(messages) } // onMessage: every open tab sees the reply stream in. socket.send(JSON.stringify("What's blocking the launch?"))
includedonConnect(socket)
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.
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.
$ 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.
$ terse actor generate Read 1 actor Wrote generated/index.ts $ bun run start count: 42
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.

Here is a sick counter demo.
This is the most durable, multiplayer safe counter you'll ever see.