little-durable-objects
A small, provider-neutral durable-object runtime. Modal is the first sandbox provider; immutable actor-state snapshots live in regional GCS buckets, while placement and each actor's current state head live in Postgres.
Install
Install the TypeScript API in actor and workflow projects:
Install the Rust runtime from crates.io:
Quickstart
-
Create a Postgres database and one GCS
STANDARDbucket. Give the service account inGOOGLE_APPLICATION_CREDENTIALSobject access to the bucket. -
Build the Rust runtime and TypeScript package:
-
Start the control plane. Its HTTP origin serves the public REST API and the internal host gRPC API, so it must be reachable from Modal with HTTP/2 enabled.
-
Export actors from
src/durable-objects.tsin your project:import { Actor } from "little-durable-objects" export class Counter extends Actor { count = 0 async increment(): Promise<number> { return ++this.count } } -
From your trusted backend, call the JSON API using
Authorization: Bearer $DURABLE_OBJECT_ADMIN_TOKEN:PUT /v1/namespaces/{namespaceId}/deployment POST /v1/namespaces/{namespaceId}/workflow-tokens -
Give the issued project token to the workflow and call the actor. The package resolves a short-lived actor target through the control plane, caches it, and invokes the regional host directly over gRPC:
import { configureDurableObjects } from "little-durable-objects" import { Counter } from "./durable-objects.js" configureDurableObjects({ token: process.env.DURABLE_OBJECT_TOKEN!, namespaceId: "my-project", controlPlaneUrl: "https://objects.example.com" }) await Counter.get("account-1").increment()
WebSockets
Actors can own WebSockets without a context object or an explicit accept step. Define any lifecycle hooks you need and attach JSON-serializable, typed metadata to each connection:
import { Actor } from "little-durable-objects"
import type { ActorSocket } from "little-durable-objects"
interface Session {
userId: string
connectedAt: number
}
export class ChatRoom extends Actor {
async onMessage(socket: ActorSocket<Session>, message: string | Uint8Array): Promise<void> {
this.broadcast(message)
}
async onDisconnect(socket: ActorSocket<Session>, code: number, reason: string, wasClean: boolean): Promise<void> {
console.log(socket.metadata.userId, code, reason, wasClean)
}
}
Connect from a trusted Node.js workflow with the same configured client:
const socket = await ChatRoom.get("lobby").connect({
userId: "user-1",
connectedAt: Date.now()
})
socket.addEventListener("message", event => console.log(event.data))
socket.send("hello")
License
MIT © 2026 Terse