Transactions
DocStack writes were always individually validated, but a multi-document logical change had no all-or-nothing story: an order and its lines, a rename touching two classes, an import. The transaction engine gives it one without touching the pipeline every write already goes through.
Stage above, commit through
A transaction handle keeps a stage, an in-memory journal of validated documents. Nothing it does touches the database until commit, and commit is exactly one stack.db.bulkDocs through the unchanged authoring pipeline: triggers, relation checks and encryption run exactly as they would for a direct write, on the batch as a whole.
This placement is the whole design. Staging below the plugin would have meant re-implementing validation and encryption for the staged path, and a second pipeline drifts. Staging above it means the pipeline stays single and a transaction is a batch with a memory in front of it.
Stage late
A write is validated when it is staged, against the committed world plus what the stage already holds, so a bad document never enters the journal. The validation sweep runs again at commit against the world as it is then. That second sweep is the atomicity boundary: a refusal there persists nothing and leaves the transaction open.
Overlay reads
Reads through the handle (t.db.get, t.db.find, t.findDocuments, t.query) merge the stage over the committed state. When the stage covers a selector entirely the read costs nothing at the storage layer; otherwise the committed query runs unwindowed and the stage is unioned in memory, which is why an overlay read with a hundred staged documents costs more than a plain one. Joins in t.query see the overlay on every side. Nothing outside the handle sees the stage: plain reads, other handles, live subscriptions and replication all read committed state.
Optimistic concurrency
Each staged document records the revision it was staged against. Commit pre-flights every one of them against the stored winner and refuses with TransactionConflictError if any moved, whether by a direct write, another transaction or replication. Commits on a stack are serialized so one commit's pre-flight cannot be invalidated by another's write. A refused commit persists nothing; the handle stays open to re-stage or discard.
Atomicity is reported
A bulkDocs batch is atomic only if the storage adapter makes it so. IndexedDB, the default, reports per-document results; the revision pre-flight shrinks the window in which a partial write can happen but cannot eliminate it. So every commit report carries adapter.atomicBatch: true when the adapter landed the batch as one storage transaction, false when results are per document. A partial commit leaves the handle in status: 'partial' with only the failed entries retained, so a raced document conflicts on retry instead of being silently overwritten. Reporting the guarantee is the honest alternative to assuming one.
What cannot be staged
Class models, because their write propagates to other documents mid-pipeline and cannot be staged or rolled back; patches, which have their own transactional path; _design/ and _local/ documents. DocStack's own writers, the scheduler, jobs and sync, always write directly.
Patches ride on it
The same engine is what makes a patch chain all-or-nothing: the chain stages through an internal transaction, propagation is dry-run against the staged models, and one commit lands every class document as a single batch. See Patches and migrations.
What is next
A transaction-scoped pipeline facade, so triggers at commit read the transaction's view rather than the committed world; public staging of class models with propagation recomputed at commit; overlay support for findDocumentsIterator and allDocs; and class-level sugar over the handle. These are recorded as the engine's second version in the roadmap.
The decision is ADR-0039.