npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

solid-objects

v1.0.5

Published

Fix race conditions across Node.js processes: ordered operations, expiring holds, and state that survives a restart, in your SQL database. A SQL-backed virtual actor library for TypeScript and Node.js.

Readme

Solid Objects for Node and Your Browser

CI npm

Open Source Durable Objects in your Node app.

Solid Objects is a SQL-backed virtual actor library for TypeScript and Node.js, with durable state, ordered operations, and automatic activation. Each actor has a stable identity, and its state lives in the SQL database that your app already uses. Virtual actors in TypeScript and Node.js explains the model and when to use it.

Status. Version 1.0: the public API follows semantic versioning and does not break before 2.0. Versioning gives the limits of the promise.

Production use. One first-party application with more than 100,000 users runs it in production. That is a count of users, not a measure of load. The benchmarks are development measurements.

It is not Solid Queue, SolidJS, or the service_actor gem, and it is not affiliated with Rails or Cloudflare.

In a shopping cart, paying twice at the same time is a big problem. The payment provider might time out, and your Node site could be restarting before recovery finishes.

To deal with this safely, you often need logic scattered between 7-10 files like database row locks, Redis locks, delayed jobs, retries, and cleanup code to keep that process straight. They are not all large, but they must agree about the same payment state and failure rules. That coordination is the difficult part.

With Solid Objects, one actor in one file owns each shopping cart's full state and recovery work. Method calls on that object run one at a time, state lives in your existing SQL database, and scheduled recovery resume after restarts.

Solid Object JavaScript Actors elegantly fit anything where one identifiable thing must remember state, handle competing requests in order, or wake up later:

  • Ticket holds and reservations
  • Multiplayer games and shared rooms
  • Shopping carts and checkout recovery
  • Low-rate quotas and account limits
  • Session expiration
  • Job leases and workflows
  • Connected devices
  • Collaborative documents

And so much more.

Deploying on Cloudflare? The Durable Objects backend runs the same actor API on Workers, with durable mailboxes, alarms, and browser subscriptions. Start with the Cloudflare example.

Contents

Installation

Solid Objects is ESM-only and requires Node.js 24.4 or newer. TypeScript users need TypeScript 5.9 or newer. Coding agents should follow the agent guide, which gives each step in order.

npm install solid-objects
npx solid-objects quickstart --yes

The quickstart uses Node's built-in SQLite driver and proves that concurrent calls to one identity do not overwrite each other. It is an unusually formal introduction to addition.

An expiring ticket hold

Save this as ticket-sale.mjs:

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

const HOLD_MILLISECONDS = 10 * 60 * 1000

class TicketSale extends Actor {
  static actorType = "TicketSale"
  available = 1
  holds = {}

  hold({ buyer }) {
    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 }) {
    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: "tickets.sqlite3" }),
  authorizeMessage: () => true,
  authorizeQuery: () => true,
})

await runtime.install()

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

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

Run the background roles in one terminal and place two concurrent holds in another:

node ticket-sale.mjs work
node ticket-sale.mjs hold

Only one buyer gets the ticket. The successful call stores the hold and its ten-minute expiry together. Stop the worker process before the deadline and restart it afterwards; the reminder is still in tickets.sqlite3 and runs when the process returns. We have now given available += 1 a recovery plan.

The authorization callbacks above are for this local example only. Production policies must bind actor IDs and operations to the authenticated user or tenant.

Why this exists

The handwritten version usually starts with a row lock. Then it gains an expiresAt column, a sweeper, retries, per-room ordering, and a broadcast path that must agree with the write. The original transaction now depends on four other subsystems.

Solid Objects makes the application-defined identity the coordination boundary. Its state, mailbox, retries, reminders, and staged consequences live in SQLite, PostgreSQL, or MySQL. No separate actor service, broker, Cloudflare account, or new datastore is required. A process of your application calls runtime.run(signal) to run reminders, effects, and queued messages. Redis is optional wake-up plumbing, not durable state.

Good uses

  • Multiplayer rooms, collaborative sessions, and documents with ordered edits.
  • Carts, reservations, and inventory holds with durable expiry.
  • Accounts, devices, and long-lived jobs whose next action depends on committed state.
  • Realtime multi-user state where publications must follow committed revisions.

Different identities can run concurrently. One global identity serializes everything.

Solid Objects in the browser

The full runtime runs inside a browser module worker. Actors look exactly like they do in Node; the database is SQLite WASM, and persistent storage lives in the origin's private file system (OPFS), so actor state survives page reloads.

import { Actor, configure, sharedSqliteWasm } from "solid-objects/browser/host"

class Counter extends Actor {
  static actorType = "Counter"

  count = 0

  increment({ amount = 1 } = {}) {
    this.count += amount
    return this.count
  }
}

const runtime = configure({
  database: sharedSqliteWasm({ path: "app.db" }),
  authorizeMessage: () => true,
  authorizeQuery: () => true,
})
await runtime.install()

await Counter.ref("page-hits").increment()

That code runs identically in every tab. sharedSqliteWasm elects one database holder per origin through the Web Locks API, carries the other tabs' SQL to it over a BroadcastChannel, and fails over onto the same durable state when the holder's tab dies. Use sqliteWasm for a single dedicated worker.

Two more modules support local-first applications. solid-objects/browser/tab-host runs one runtime for all tabs when the application prefers request-level routing. solid-objects/transmit drains the transactional effects outbox to a server with at-least-once delivery, per-actor order, and an idempotent server ingest, so offline writes reconcile when the network returns.

The backend can be Rails, not only Node

The browser runtime does not require a Node server behind it. The transmit wire contract is shared with the Ruby gem (solid-objects-ruby): SolidObjects::Transmission.receive accepts the same envelopes as the Node ingest receiveTransmitEnvelope, dedups on the same transmit:<effectId> key, and both repositories pin the contract with one shared fixture file. A browser front end therefore replays its offline writes directly onto Ruby server actors, with no Node service in between:

class TransmitController < ApplicationController
  def create
    head :forbidden and return unless authenticated_device?

    SolidObjects::Transmission.receive(JSON.parse(request.body.read))
    head :ok
  end
end

The Ruby side of the family shipped in solid_objects 0.14.0, released the same day as this package's 0.14.0 (solid-objects-ruby#49).

The wire shapes are documented in the browser protocol, the API in the public API reference, and the platform boundaries in supported versions.

When a transaction is better

Often. If all the work happens in one request, use a transaction, constraint, or SELECT ... FOR UPDATE. It is smaller and faster.

Use Solid Objects when the critical section outlives that transaction: work must happen later, survive a restart, or remain ordered across several requests or jobs. A plain counter is not a reason to install this package.

Guarantees and boundaries

  • Calls are durably ordered per identity. Different identities may run concurrently.
  • Delivery is at least once, not exactly once. A handler can begin again after a crash.
  • One successful turn commits state and staged reminders, messages, effects, and realtime publications together.
  • Fencing prevents a stale worker from committing, though its JavaScript may keep running. It cannot undo an HTTP request, email, payment, or file write.
  • External effects can repeat and must use the stable effect ID or another durable idempotency key.
  • One hot identity is intentionally sequential. There are no transactions across identities.
  • Background work needs runtime.run(signal). If no process is running, committed work waits in SQL rather than disappearing.
  • Your application still owns authorization, database backups, failover, WebSocket transport, and capacity planning.

There is no exactly-once delivery. Read the correctness contract before using important data.

Guides

Each guide reproduces a failure, shows the simplest fix without an actor, and then shows a tested Solid Objects version with its limits:

Read more

The benchmarks, parity ledger, dashboard, browser setup, and exhaustive API notes remain in docs/, where long documentation can be long on purpose.

Status and license

Version 1.0 promises a stable public API. The correctness core has automated coverage across the supported databases, browser runtime, recovery paths, and packaged artifact. Versioning says what a release can change. The Cloudflare backend is experimental and is outside that promise.

Solid Objects is released under the MIT License. It is an independent project and is not affiliated with or endorsed by Cloudflare.