Guide · TypeScript and Node.js

Virtual actors in TypeScript and Node.js

Solid Objects is a SQL-backed virtual actor library for TypeScript and Node.js: stable identities, durable state, ordered operations, and automatic activation on SQLite, PostgreSQL, or MySQL.

Short answer

Yes. Solid Objects is a SQL-backed virtual actor library for TypeScript and Node.js. The npm package is solid-objects. It gives each actor a stable identity, durable state, ordered operations, and automatic activation.

The actor runtime runs in your Node.js processes. State and mailboxes live in SQLite, PostgreSQL, or MySQL. It needs no broker, no daemon, no Cloudflare account, and no new datastore. Redis is optional and only shortens wake-up latency.

Solid Objects is a pre-1.0 release. Read Compatibility and maturity before you choose it.

What a virtual actor is

A virtual actor is a logical object that always exists by name. The caller does not create it, start it, or stop it. The runtime loads it when a message arrives and releases it when it is idle. Microsoft Orleans made this model known as "virtual actors".

Solid Objects implements four properties of that model:

Property What it means How Solid Objects does it
Stable identity An actor is addressed by type and ID, for example one room for each game. Room.ref("room-7") returns a reference. The reference does not load the actor.
Automatic activation The first message activates the actor. An idle actor is released. A process claims a fenced activation lease when work arrives. A worker releases it after idleDeactivationTimeoutMilliseconds.
Durable state State survives process exits and deploys. Enumerable public fields are a JSON document in SQL.
Ordered turns One identity runs one operation at a time, in a fixed order. Each call is a durable mailbox message with a per-actor sequence number.

Different identities run concurrently across processes. One identity is a serialization point on purpose.

A small example

This program holds one ticket for a buyer and releases the hold after ten minutes. It uses the SQLite driver that Node.js includes. Save it as ticket-sale.mts, so that Node.js loads it as an ES module:

import { Actor, configure } from "solid-objects"
import { sqlite } from "solid-objects/database/sqlite"

const HOLD_MILLISECONDS = 10 * 60 * 1000

export class TicketSale extends Actor {
  static override readonly actorType = "TicketSale"

  available = 1
  holds: Record<string, number> = {}

  hold({ buyer }: { buyer: string }): { held: boolean; available: number } {
    if (this.available === 0 || Object.hasOwn(this.holds, buyer)) {
      return { held: false, available: this.available }
    }

    this.available -= 1
    this.holds = { ...this.holds, [buyer]: Date.now() }
    this.schedule({ at: new Date(Date.now() + HOLD_MILLISECONDS), key: buyer }).expire({ buyer })
    return { held: true, available: this.available }
  }

  expire({ buyer }: { buyer: string }): number {
    if (!Object.hasOwn(this.holds, buyer)) return this.available

    const remainingHolds = { ...this.holds }
    delete remainingHolds[buyer]
    this.holds = remainingHolds
    this.available += 1
    return this.available
  }
}

const runtime = configure({
  database: sqlite({ path: process.env.TICKET_DATABASE ?? "tickets.sqlite3" }),
  authorizeMessage: () => true,
  authorizeQuery: () => true,
})

await runtime.install()

try {
  const sale = TicketSale.ref("event-42")

  if (process.argv[2] === "work") {
    const controller = new AbortController()
    process.once("SIGINT", () => controller.abort())
    process.once("SIGTERM", () => controller.abort())
    await runtime.run(controller.signal)
  } else {
    const buyers = process.argv.length > 3 ? process.argv.slice(3) : ["ada", "grace"]
    const results = await Promise.all(buyers.map((buyer) => sale.hold({ buyer })))
    console.log(JSON.stringify(results))
  }
} finally {
  await runtime.close()
}

Run the background roles in one terminal. Place two concurrent holds in a second terminal:

npm install solid-objects
node ticket-sale.mts work
node ticket-sale.mts hold

The two holds enter the same mailbox and commit one at a time, so only one buyer gets the ticket. The hold and its reminder commit in one transaction. If the worker stops, the reminder stays in tickets.sqlite3. It runs when the worker starts again.

The authorization callbacks above allow every caller. Use them only for a local example. The package release check runs this file against the packed npm tarball. The source is examples/ticket-sale.ts.

For more setup, use one of these guides:

When to use it

Solid Objects is a good candidate when most of these conditions are true:

When to use something else

Do not use an actor when a simpler tool enforces the invariant:

Choosing Solid Objects has the full list.

How it compares

This table compares coordination models for a Node.js application. It does not rank the projects. Facts about other projects were checked on October 7, 2026, against the sources in System comparisons.

Approach Unit of order Durable state Delayed work Extra service
SQL transaction or row lock Rows in one transaction Application tables None No
Job queue, such as BullMQ or pg-boss A job or a queue Owned by the application Scheduled jobs Redis for BullMQ; PostgreSQL for pg-boss
worker_threads or a pool such as Piscina None None None No
Solid Objects Actor class and ID JSON state in SQLite, PostgreSQL, or MySQL Durable per-actor reminders No
Cloudflare Durable Objects Object class and ID Per-object storage One alarm for each object Cloudflare's network. The open-source workerd runtime hosts objects on one instance only
Dapr actors (@dapr/dapr) Actor type and ID A transactional Dapr state store Durable reminders through the Dapr Scheduler service A Dapr sidecar, plus the placement and Scheduler services
Rivet Actors Actor key Actor state, plus per-actor SQLite and KV storage Scheduled actions The Rivet Engine, self-hosted or Rivet Cloud
Restate virtual objects Object key, one writer at a time Restate's state store Durable timers A Restate server
DBOS A workflow and its steps Checkpoints in PostgreSQL Durable sleeps No server; PostgreSQL is required
Temporal A workflow execution Temporal event history Durable timers A Temporal Service, self-hosted or Temporal Cloud

System comparisons has the full table, with primary references for each row.

Orleans concept map

Orleans is the reference design for virtual actors on .NET. Solid Objects uses the same programming model on a SQL database. It does not copy the Orleans cluster, placement, or feature set.

Orleans Solid Objects Difference
Grain class Actor subclass with a static actorType None in concept
Grain identity (key) Actor type and actor ID None in concept
Activation on first call Activation lease on first claimed message Solid Objects fences each activation with a database generation
Turn-based execution Ordered mailbox, one turn at a time Orleans can enable reentrancy. Solid Objects turns for one identity never interleave
Grain persistence Public fields stored as JSON in SQL An Orleans grain calls WriteStateAsync. Solid Objects persists state with the turn that changed it
Reminders schedule() Both are durable. Orleans skips a tick that falls due while the cluster is down. A due Solid Objects reminder runs when a runtime process starts. Solid Objects has no non-durable timers
Silos and cluster membership Any Node.js process that calls runtime.run(signal) Solid Objects has no placement, directory, or cluster membership. The database is the coordination point
Streams Observables and realtime sessions Solid Objects publishes committed revisions through an application-owned transport

Solid Objects delivery is at least once. Write each operation so that it can run again without harm.

Guarantees and boundaries

The correctness contract states each guarantee and its limits.

Compatibility and maturity

These runtimes have different compatibility statements:

Surface Status
Node.js SQL runtime Node.js 24.4 or newer, ESM only, TypeScript 5.9 or newer. SQLite through node:sqlite, PostgreSQL 14 or newer, MySQL 8.0 or newer with InnoDB
Browser client (solid-objects/browser) A subscription client for a server runtime. It is not the SQL actor runtime
Browser runtime (solid-objects/browser/host) The actor runtime in a browser module worker on SQLite WASM. Tested in Chromium
Cloudflare backend (solid-objects/cloudflare) Experimental. It runs the actor API on Cloudflare Durable Objects, with different capability limits

The package is pre-1.0. It has one deployed first-party reference application, no measured scale, and no known third-party production use. Supported versions lists the CI matrix. For Ruby on Rails, use the solid_objects gem.

Generated from docs/virtual-actors.md in cardmagic/solid-objects-js, v0.17.1, commit 3f409da. For Ruby on Rails, read Virtual actors in Ruby on Rails.