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.
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.
npm exec --yes --package=solid-objects@0.13.1 -- solid-objects quickstart
{
"sameIdentityCalls": 25,
"sameIdentityFinalState": 25,
"independentIdentitiesOverlapped": true,
"temporaryStateRemoved": true
}
An actor is just a class
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.
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.
- 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.
- 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 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.
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.
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.
Live application · Source · Tests · CI
| 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.
Read the contract
The claims above are backed by docs, a test suite, and a runnable multi-process crash-and-fencing demonstration.
correctness.md — delivery and commit semantics.
architecture.md — mailbox, leases, fencing, adapters.
parity.md — Native, Partial, and Planned ledger.
api.md — public exports and runtime managers.
state-and-lifecycle.md — state, migrations, snapshots.
errors-and-recovery.md — the error contract and retries.
demo.ts — two processes, SIGKILL, lease takeover, correct final state.
npm: solid-objects — the published artifact.
Author and project history
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.