npm: solid-objects

Durable Objects for Node, backed by your existing SQL database

Write an ordinary TypeScript class. Solid Objects gives each instance a durable identity, serialized calls, and durable state on SQLite, PostgreSQL, or MySQL — without deploying to Cloudflare, and with no Redis or message broker to run.

Early pre-1.0 — expect breaking changes version 0.13.1 license MIT requires Node.js 24.15+ databases SQLite / PostgreSQL 14+ / MySQL 8+
Quickstart

Try it in one command

No files, no setup. This runs the packaged demo against the published solid-objects@0.13.1 package. Needs Node.js 24.15 or newer.

terminalshell
npm exec --yes --package=solid-objects@0.13.1 -- solid-objects quickstart
observed outputstdout
{
  "sameIdentityCalls": 25,
  "sameIdentityFinalState": 25,
  "independentIdentitiesOverlapped": true,
  "temporaryStateRemoved": true
}

An actor is just a class

counter.tstypescript
import { Actor } from "solid-objects"

class Counter extends Actor {
  static override readonly actorType = "Counter"
  count = 0
  increment(): number {
    this.count += 1
    return this.count
  }
}

// Five concurrent calls to one identity, serialized:
const counter = Counter.ref("global")
const results = await Promise.all([1, 2, 3, 4, 5].map(() => counter.increment()))
console.log(results.sort((a, b) => a - b)) // [ 1, 2, 3, 4, 5 ]

The full runnable file, with runtime setup, is /examples/counter.ts. SQLite needs no driver; it uses Node's built-in node:sqlite.

What it is for

Stateful features that keep re-assembling the same machinery

Reach for Solid Objects when many independently-addressed entities each need ordered state changes, durable work, recovery, and realtime updates — and you would otherwise wire that together from a database, Redis, a queue, and locks.

Good fit
  • Game tables, chat rooms, and live collaborative documents.
  • Carts, orders, accounts, and ledgers with concurrent updates.
  • Devices, sessions, and agent runs that need per-entity ordering.
  • Anything where one identity must serialize its own writes and survive a restart.
Reach for something else
  • One short SQL transaction already enforces the invariant.
  • Bulk or data-parallel pipelines with no per-identity state.
  • A single hot identity that must outrun one sequential mailbox.
  • Atomic transactions spanning many identities, or edge placement near users.
How it works

How a turn commits

One mailbox, one fenced lease, one commit. Your handler runs outside the database transaction; its result and everything it stages commit together, and a stale lease holder is rejected at commit time.

Actor commit flow: a call becomes a durable message in the mailbox; a worker claims the actor under a fenced lease; the handler runs outside the transaction; it stages the result and consequences; one fenced transaction commits state and staged work together; a reject or throw rolls back; a stale lease holder is rejected by fencing; external effects run at least once outside the transaction and deduplicate by a stable effect id; then a post-commit wake-up takes the next turn. Actor commit flow, top to bottom: a call becomes a durable message in the mailbox; a worker claims the actor under a fenced lease; the handler runs outside the transaction; it stages the result and consequences; one fenced transaction commits state and staged work together; a reject or throw rolls back; a stale lease holder is rejected by fencing; external effects run at least once outside the transaction and deduplicate by a stable effect id; then a post-commit wake-up takes the next turn.
The commit path for one actor turn. State and staged work commit atomically; external effects are the deliberate exception.

Delivery is ordered per identity and at least once. External effects run outside the transaction and can repeat, so make them idempotent and deduplicate by their stable effect id. The full contract is in correctness.md and architecture.md.

Deployed reference application

Shuffle Up and Play: one deployed reference application

Shuffle Up and Play is a deployed two-player Magic: The Gathering table built on Solid Objects. Each table is one durable GameRoom actor: it serializes mutations, persists state, runs background work, and sends a different authorized projection to each player. The stack is small on purpose: Node 24, TypeScript, SQLite, node:http, and ws, with no web framework and no bundler. This is one deployed first-party reference application. There is no measured scale and no third-party production use yet.

What the deployed application exercises. Every row is verified in its source and tests.
Production concern How it is handled Source
Concurrent writes One GameRoom actor per table serializes mutations; 25 concurrent life changes converge to one deterministic result. game-room.ts
Private realtime state Each seat gets its own payload projection; the opponent's hand and library render as “Hidden card,” tested at the actor, WebSocket, and browser-client layers. room-snapshot.ts
External work Deck imports run as durable effects with success and failure callbacks; superseded results are ignored. runtime.ts
Recovery Committed state, an accepted message, an unfinished effect, and a scheduled reminder all survive a runtime restart. restart.test.ts
Operations Doctor checks, retention, reconciliation, dead-letter inspection, and an optional loopback dashboard. doctor.ts

Scope of this evidence. The public deployment is a single Node process backed by one on-disk SQLite database; it demonstrates a real deployed workload, not every topology. “Recovery” means state survives a graceful restart, verified by tests, not a specific traffic level. The player experience is a standalone application, not an interactive Solid Objects tutorial.

Documentation

Read the contract

The claims above are backed by docs, a test suite, and a runnable multi-process crash-and-fencing demonstration.

Correctness

correctness.md — delivery and commit semantics.

Architecture

architecture.md — mailbox, leases, fencing, adapters.

Parity

parity.md — Native, Partial, and Planned ledger.

API

api.md — public exports and runtime managers.

Lifecycle

state-and-lifecycle.md — state, migrations, snapshots.

Errors

errors-and-recovery.md — the error contract and retries.

Crash / fencing demo

demo.ts — two processes, SIGKILL, lease takeover, correct final state.

Package

npm: solid-objects — the published artifact.

Why this exists

Author and project history

Author
Lucas Carlson has authored over 20 open source projects with over 10 million downloads and thousands of stars across GitHub projects.
Ruby and TypeScript
The TypeScript package ports the programming model of the Ruby solid_objects gem. The two runtimes share no database schema or wire protocol; the relationship is spiritual parity, not shared code. The package is versioned to track that parity, which is why it begins at 0.12.
Production
The Ruby implementation is used in a mature Rails codebase with over 100,000 users.
FAQ

Common questions

Is Solid Objects affiliated with Cloudflare?

No. It is an independent open-source project that ports the Durable Objects programming model to Node.js. It does not reproduce Cloudflare's edge placement, global routing, WebSocket hibernation, or storage APIs.

Does it guarantee exactly-once execution?

No. Delivery is ordered per identity and at least once. Operations and external effects can repeat, so make external effects idempotent and deduplicate by their stable effect id. Fencing ensures at most one valid lease holder can commit.

Do I need Redis or a message broker?

No. State, mailbox, leases, fencing, retries, timers, and outboxes live in SQLite, PostgreSQL, or MySQL. Redis is an optional wake-up latency layer; losing it increases polling latency without losing committed work.

Is it production ready?

It is an early pre-1.0 release (v0.13.1); expect breaking changes. There is one deployed first-party reference application. There is no measured scale and no third-party production use yet. Evaluate it with the runnable quickstart, the reference application, and the test suite.

Which Node.js and database versions are required?

Node.js 24.15 or newer and TypeScript 5.9 or newer. SQLite uses the built-in node:sqlite. PostgreSQL 14 or newer uses the optional pg driver; MySQL 8.0 or newer with InnoDB uses the optional mysql2 driver.