Skip to main content

Under the hood

Tech stack

LibraryRoleWhy
PouchDB (pouchdb-browser, pouchdb-find, pouchdb-selector-core)Storage and replicationA mature implementation of the CouchDB replication protocol with revision trees, conflict detection and a changes feed: the substrate an offline-first store needs, and a remote ecosystem to replicate against.
Zod 4Validation and type inferenceA class's hydrated schema is a Zod object, so one definition validates at runtime and types at compile time.
jsondiffpatchSchema evolutionA precise delta between the old and new class schema is what propagation applies to existing documents.
semverPatch orderingConsumer and system patches sort and compare by semantic version.
Web Crypto (crypto.subtle)EncryptionNative PBKDF2 and AES-GCM: faster than a JavaScript implementation and a smaller surface for error.
rabe (Fraunhofer AISEC), compiled to WASMAttribute-based encryptionThe AC17 CP-ABE scheme, vendored into @docstack/abe, seals scope keys under attribute policies. Loaded lazily, only when a stack declares scopes.
PlaywrightTestsThe client suite runs the compiled library in a real Chromium against real IndexedDB.
Rollup, tsc, WebpackBuildsRollup emits the client as ESM plus a UMD bundle; the React package is plain tsc; the workbench is Webpack.

The engines

EngineSourceResponsibility
Stackcore/stack.ts, core/index.tsClientStack: the database handle, lifecycle, patches, reads and writes, and DocStack, which owns several stacks.
Schemacore/class.ts, core/attribute.ts, core/domain.ts, utils/index.tsClass hydration, Zod fields, domains, applySchemaDelta.
Pluginplugins/pouchdb.tsThe authoring pipeline: validation, triggers, relation checks, encryption, class-model propagation, on bulkDocs, get and bulkGet.
Triggercore/trigger/Function hydration and execution.
Jobcore/job-engine/JobEngine, JobScheduler, the schedule grammar.
Querycore/query-engine/Parser, planner, executor, accumulators.
Cryptocore/crypto-engine/The keyring: the document key, retired keys and admitted scope keys dispatched by kid; canaries; AES-GCM payloads with scope binding.
Access scopescore/stack.ts (scope registry, unlockScopes, buildAccessScope), packages/abe~AccessScope documents, attribute-key admission, and the CP-ABE primitive (rabe's AC17 compiled to WASM) with the policy normaliser and authority helpers.
Transactioncore/transaction-engine/Stage, overlay, sweep, commit.
Synccore/sync/Lifecycle, filters and their identity, internal documents, the schema gate, tenant scoping.
Content transfercore/content-transfer.tsexportContent and importContent.
Data modelcore/datamodel/The system patches and the ids they seed.

The rule-based policy engine that once lived in core/policy-engine/ is deleted: the scope model is the one access-control language, ~Policy documents are inert legacy data, and system patch 0.0.18 deactivates the seeded ones.

Patterns

  • ES classes with explicit engines. ClientStack composes jobEngine, jobScheduler, cryptoEngine and transactionEngine as members rather than hiding them, so advanced use reaches them directly.
  • Function hydration. Triggers and jobs are strings turned into functions with new Function, with document, classObj, stack or stack, params, job injected as arguments. It is stated honestly everywhere: not a sandbox, but a way to store behaviour as data.
  • Pristine capture. The plugin receives the database's original bulkDocs, get and bulkGet as an argument at construction, because PouchDB installs them per instance and capturing them any other way yields nothing or recurses. The sync layer's replication handle restores them for its own reads.
  • One feed per stack. A single changes feed is demultiplexed to every subscriber; building a class does not subscribe it.
  • The guarded handle. stack.db is a proxy that closes the three PouchDB doors that would skip the pipeline.
  • Random ids. Type-<24 hex>, minted at write time; a conflict on create throws instead of silently merging.
  • Honest reporting over assumed guarantees. Transaction commits report the adapter's atomicity; sync reports lastConvergedAt rather than "last activity"; the scheduler reports why a job was skipped.

Every one of these is pinned by the Playwright suite in packages/client/src-test/: transactions and their overlay, crypto-aware queries, subqueries, replication filters, late-joining stacks, locked sync, patch chains and patch jobs. The reasons are recorded as decision records.