Concurrent Node servers
Configure MongoClient with the worker-backed server engine when multiple
requests should query SQLite while other requests write. The engine opens one
writer worker and a bounded pool of read-only workers against the same WAL file.
Application code uses the MongoDB-compatible JavaScript API.
The caller owns the supplied engine: closing the client releases its cursors and
sessions, and closing database releases the workers. Set closeDatabase: true
on the client only when it should also own engine shutdown. Drain application
requests before closing either handle.
One SQLite file is one database. dbName labels the client's database handle
(default test); it does not partition the file. Other database names reject.
Use separate files for separate databases.
Install the optional better-sqlite3 peer for this example. Omit adapter to use
Node's built-in SQLite. Both choices execute SQL in workers with this entry point.
The ordinary /node and /better-sqlite3 entries retain their lighter, single
connection behavior and execute SQL on the calling thread.
Ownership and routing
The server opens its own file-backed connections, requests WAL and
synchronous=FULL, and verifies the actual journal mode. :memory:, native
connection injection, and wal:false are rejected here. Existing single-connection
drivers remain usable through { driver }; supplying one does not automatically
create a reader pool. Closing a supplied driver is opt-in with closeDriver:true.
Document queries, aggregations, counts, and normal ShareDB reads use the read pool. Schema creation, document mutations, and commits use the writer. Planning index metadata and executing a query share a reader snapshot, so an index change cannot make the compiled query refer to a different catalog version.
For application transactions, pass a Mongo session to every participating operation:
Session operations use the writer's transaction, including reads, and see their own writes. Other reads can see the last committed state; unrelated writes wait. Do not await an operation without its session inside the transaction or run parallel operations with one session. Transaction cursors must be consumed inside the transaction. See sessions and transactions for failure handling and supported semantics.
SQLite still permits only one writer at a time. Workers improve reader concurrency and keep SQL execution off Node's event loop; they do not distribute writes across hosts. Multiple ShareDB backend processes also need a shared PubSub adapter.
Share the coordinator with SQL and file storage
database.sql.get/all/run/exec use the writer queue. database.sql.read.get/all/iterate accepts
a single SELECT query (including SELECT CTEs and VALUES) and can use the reader
pool. Every facade checks the engine's lifetime. sql.transaction() supplies a
scoped SQL facade; manual transaction-control SQL is rejected. Use parameter binding
for values. Raw driver and native sqlite handles are low-level interfaces and
must not introduce a competing transaction manager.
The core engine's database.transaction(async tx => ...) also supplies a scoped
handle: all tx operations, including tx.sql.read, stay on its writer connection.
Use that supplied handle for every operation in a core transaction. This low-level
API is described in the engine reference.
Mongo and core APIs share ordinary collection tables and indexes. ShareDB and
custom SQL/BLOB tables are separate; they survive Mongo dropDatabase(), which
removes ordinary document collections and their indexes. These integrations can
share the same owner and file.
Keep ShareDB document changes in its operation protocol. Coordinating a BLOB write
does not by itself make a later ShareDB metadata update part of that transaction.
The framework patches add an explicit provider using this API.
Cursor resources
Ordinary toArray() reads finish their statements before returning the requested
array. A server iterator leases one reader and keeps its query snapshot while rows
are delivered in bounded batches. Writing through the database from its loop body
works because the writer is a different connection. A new iteration of a core Silt
cursor executes a new query; Mongo cursors maintain one
position until rewound.
Breaking iteration, closing the cursor/client/database, and errors release the
reader. A queued cursor can be cancelled without waiting for another cursor's
lease. An abandoned pooled cursor expires after cursorIdleTimeoutMs; subsequent
consumption rejects explicitly. Active long-lived snapshots can retain WAL pages,
so close unused cursors and inspect workload-specific storage growth. An explicit
read transaction's lifetime follows its callback and has no automatic deadline.
Expo and single-connection iterators instead materialize the result once into SQLite TEMP storage, then deliver bounded pages without keeping a source statement open between application awaits. This costs temporary storage and initial work. Actual Expo web tests verify disk-backed OPFS spooling; native runtime evidence is listed separately in Expo.
Settings and measurements
database.driver.stats() reports writer/reader thread IDs and active, queued, and cursor
counts. These are diagnostics, not application data. Cache settings are targets,
not a hard process memory limit; every worker has its own runtime and SQLite cache.
The September 2026 measured fixture improved grouping/lookup throughput about 1.6–2× with two readers and reduced event-loop stalls. Tiny primary-key SQL and document point reads were slower; workers added startup and RSS costs. See the performance results for the reproducible workloads, exact figures, and limits. Choose settings using your own request mix.