little-durable-objects 0.1.19

Standalone regional durable-object control plane, host, and durability runtime
Documentation

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:

pnpm add little-durable-objects

Install the Rust runtime from crates.io:

cargo install little-durable-objects --locked

Quickstart

  1. Create a Postgres database and one GCS STANDARD bucket. Give the service account in GOOGLE_APPLICATION_CREDENTIALS object access to the bucket.

  2. Build the Rust runtime and TypeScript package:

    pnpm install
    pnpm build
    chmod +x npm/dist/providers/modalCli.js
    
  3. 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 DURABLE_OBJECT_PROCESS_ROLE=control_plane
    export DURABLE_OBJECT_POSTGRES_URL='postgresql://localhost/durable_objects?sslmode=disable'
    export DURABLE_OBJECT_STANDARD_BUCKETS='{"north-america-east":"my-actor-state-bucket"}'
    export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
    export DURABLE_OBJECT_CONTROL_PLANE_BIND=0.0.0.0:7100
    export DURABLE_OBJECT_CONTROL_PLANE_URL=https://objects.example.com
    export DURABLE_OBJECT_JWT_SIGNING_KEY="$(openssl genpkey -algorithm Ed25519 -outform DER | base64 | tr -d '\n')"
    export DURABLE_OBJECT_ADMIN_TOKEN="$(openssl rand -hex 32)"
    export DURABLE_OBJECT_SANDBOX_PROVIDER=modal
    export DURABLE_OBJECT_SANDBOX_COMMAND="$PWD/npm/dist/providers/modalCli.js"
    export MODAL_TOKEN_ID=...
    export MODAL_TOKEN_SECRET=...
    
    ./target/release/little-durable-objects
    
  4. Export actors from src/durable-objects.ts in your project:

    import { Actor } from "little-durable-objects"
    
    export class Counter extends Actor {
        count = 0
        async increment(): Promise<number> {
            return ++this.count
        }
    }
    
  5. 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
    
  6. 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")

await ChatRoom.get("lobby").broadcast("streamed output")

License

MIT © 2026 Terse