Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Durable Actors
Open source alternative to Durable Objects
If you've been burned by memory limits in Durable Objects, the lack of observability or want to avoid the lock in, Durable Actors is a self-hostable alternative that could work for you!
Use Durable Actors to build persistence and coordination for Software Factories, Multiplayer AI and agent fleets.
Get it working end-2-end in minutes. Define your actor class, generate your type-safe client and easily integrate into your existing tech stack.
Start with a coding agent
Paste this prompt into your coding agent:
Go to https://github.com/TerseAI/durable-actors, follow the README and build a sample project. Get the development server running and ask me where I would like to invoke my actors from.
Give each conversation, document, or agent a TypeScript actor: its saved state survives restarts, and its methods run one at a time by default so concurrent callers can update it safely.
The SDK provides actor classes, type-safe clients, and WebSocket support. The runtime loads actors on demand and persists fields marked @Persisted. Develop locally with one command, then self-host the runtime for production.
For example, a chat actor can keep a conversation across server restarts, or a document actor can coordinate edits from several people and agents without each caller managing database locks.
Languages
Local development
Install Node.js 22.19+, pnpm, and Bun 1.3.9+.
Create your actor project in your directory of choice
# Or with npm:
Define an Actor
Define and export actors in your actor project’s src/actors.ts, the default entrypoint loaded by durable-actors dev. For example, a chat history actor:
import { openai } from "@ai-sdk/openai"
import { streamText } from "ai"
import { Actor, Persisted, Reentrant, type ActorSocket } from "durable-actors"
type Member = { name: string }
type Message = { role: "user" | "assistant"; content: string }
type Chat = { messages: Message[]; busy: boolean }
export class ChatHistory extends Actor<Member, string, Chat> {
@Persisted messages: Message[] = []
async onConnect(socket: ActorSocket<Member, Chat>) {
socket.send({ messages: this.messages, busy: false })
}
@Reentrant
async onMessage(socket: ActorSocket<Member, Chat>, text: string) {
const messages: Message[] = [...this.messages, { role: "user", content: `${socket.metadata.name}: ${text}` }]
this.broadcast({ messages, busy: true })
const reply: Message = { role: "assistant", content: "" }
const result = streamText({ model: openai("gpt-5-mini"), messages })
for await (const chunk of result.textStream) {
reply.content += chunk
this.broadcast({ messages: [...messages, reply], busy: true })
}
this.messages = [...messages, reply]
this.broadcast({ messages: this.messages, busy: false })
}
}
Connect your Backend
We make it super easy to integrate the actors into your existing tech stack. Just generate the client and you get a fully type safe contract to interact with.
Now you may call your actor and access the state.
import express from "express"
import { actors } from "../generated/index.js"
export const app = express()
app.post("/api/chat/:room/socket", async (req, res) => {
const grant = await actors.ChatHistory.prepareWebsocket({
actorId: req.params.room,
metadata: { name: String(req.query.name ?? "Guest") }
})
res.set("Cache-Control", "no-store").json(grant)
})
Connect the frontend (React)
import { useEffect, useRef, useState } from "react"
import { createRoot } from "react-dom/client"
import type { actors } from "../generated/index.js"
const params = new URLSearchParams(location.search)
const room = params.get("chat") ?? "lobby"
const name = params.get("name") ?? "Guest"
function Chat() {
const socket = useRef<WebSocket>(null)
const [chat, setChat] = useState<actors.ChatHistory.Outgoing>({ messages: [], busy: true })
useEffect(() => {
let active = true
async function connect() {
const response = await fetch(`/api/chat/${encodeURIComponent(room)}/socket?name=${encodeURIComponent(name)}`, { method: "POST" })
const { websocketUrl } = await response.json()
if (!active) return
socket.current = new WebSocket(websocketUrl)
socket.current.onmessage = event => setChat(JSON.parse(event.data))
}
void connect()
return () => {
active = false
socket.current?.close()
}
}, [])
function send(form: FormData) {
const text = String(form.get("message")).trim()
if (!text || socket.current?.readyState !== WebSocket.OPEN) return
socket.current.send(JSON.stringify(text))
setChat(chat => ({ ...chat, busy: true }))
}
return (
<main>
<h1>AI chat · {room}</h1>
<div role="log" aria-label="Messages">
{chat.messages.map((message, index) => (
<article key={index}>
<strong>{message.role}</strong>
<p>{message.content}</p>
</article>
))}
</div>
<form action={send}>
<input name="message" aria-label="Message" required disabled={chat.busy} />
<button disabled={chat.busy}>Send</button>
</form>
</main>
)
}
createRoot(document.getElementById("root")!).render(<Chat />)
For complete sample applications, see AI Chat, Collaborative documents, and Chatroom.
Here's what it looks like in action:
Community
Bug reports, feature requests, documentation fixes, and code contributions are welcome. See the contributing guide for repository setup and checks, and follow our code of conduct.
Use GitHub Issues for bugs, ideas, and questions. Report vulnerabilities privately using our security policy.
Follow development and release notes on GitHub Releases.
License
MIT © 2026 Terse