The StorageDriver interface
StorageDriver is a flat bundle of async methods organized into eight areas:
One additional method,
runExclusive(key, fn), serializes graph writes per key to prevent concurrent insertions from jointly forming a group or reporting cycle. Implement it with a SQL advisory lock in multi-node deployments (see Multi-node note).
Optional methods
The following methods are optional. Custom drivers can omit them and keep compiling, and the Application either falls back to a slower path or fails loudly at construction if the feature depending on them is turned on.memoryDriver and mongoDriver implement the event-log methods out of the box (mongoDriver omits catalog history and metrics; see its section). prismaDriver includes each group only when the corresponding delegate is present on the client you hand it, so if you merged only the ten core models and generated your client, events: { persist: true } and metrics: {} will still throw at construction. Add the optional models, migrate, and regenerate first.
memoryDriver()
The in-memory driver is the reference implementation of the storage seam. It is complete and correct. Every Application feature works with it, which makes it the natural choice for unit tests, local development, and ephemeral deployments.
memoryDriver() returns an independent, empty store. Two calls produce two isolated stores, which helps when testing multi-tenant scenarios or provider federation in-process.
Testing with the memory driver
prismaDriver
The Prisma driver is the production driver shipped in @alfiz/prisma. It maps the StorageDriver interface onto a generated Prisma client without taking @prisma/client as a hard dependency. It operates against a structural delegate interface instead.
Installation
@prisma/client must be in your own dependencies and generated from a schema that includes the Alfiz models. @alfiz/prisma does not depend on it directly.
Creating a Prisma driver
PrismaClient instance directly. Any Prisma client generated from a schema containing the Alfiz models satisfies the AlfizPrismaDelegates interface structurally, with no cast required.
PrismaDriverOptions
prismaDriver accepts an optional second argument:
<T>(key: string, fn: () => Promise<T>) => Promise<T>
A cross-node serialization function for graph writes. In a single-process deployment the default in-process promise-chain mutex is sufficient. In a multi-node deployment you must provide a database advisory lock here so concurrent graph writes on different nodes cannot jointly form a cycle. Example for Postgres:The keys the driver hands to
lock already carry the partition ("docs:groups" under partition: "docs"), so applications sharing one database never contend on each other’s locks. Supplying lock also marks the driver cross-process capable (crossProcess), a prerequisite for accepting a mesh write edge.string
Pins the driver to one application’s slice of shared tables. Rows are written and read with
app = partition; the default "" is the unpartitioned dataset. The recommended value is the application’s primary catalog namespace. The delegate types make the discriminator required in every query shape, so a query inside the driver that forgot the partition fails to compile rather than scanning every tenant.The delegate interface
prismaDriver requires only the subset of the generated Prisma client it actually calls, expressed as the AlfizPrismaDelegates interface. Ten delegate properties are required and map directly to the core Alfiz schema models; four more are optional, and a client generated without them still satisfies the interface:
PrismaClient for a schema containing the Alfiz models satisfies this interface automatically. You can also pass a hand-rolled mock for testing:
Schema fragment (v2)
Add the following models to yourschema.prisma. These are a fragment, so they have no datasource or generator blocks and inherit yours. All models are prefixed Alfiz to avoid colliding with your own models.
Since 0.8.0 the fragment is v2: every model carries an app partition discriminator (@default("")) and every primary key and index is a composite led by it, so several Applications can share one set of tables. A single-application deployment changes nothing, because an unpartitioned driver reads and writes partition "", which is where a migrated v1 dataset lands. Upgrading from v1 is a column-add plus PK/index rebuilds; see the 0.8.0 changelog entry.
Timestamps (
createdAt, expiresAt, at, etc.) are stored as BigInt (epoch milliseconds) for lossless round-tripping with the core types’ number fields. IDs are opaque strings assigned by the Application, with no autoincrement or cuid() used. No Prisma relations or cascades are declared; referential integrity between Alfiz rows is enforced by the Application layer above the seam.Optional models
Four more models are additive. Leave them out and everything above still works; add one when you turn on the feature that needs it. Each corresponds to an optional delegate, and the Application refuses loudly at construction when you enable a feature whose model is missing.mongoDriver
The MongoDB driver ships in @alfiz/mongo and implements the seam directly over the official mongodb package, so there is no schema to merge and no code generation step. Collections and their hot-path indexes are created lazily on first use; every row is stored with its Alfiz id as Mongo’s _id for free uniqueness and point reads.
Installation
mongodb (v6 or v7) is a peer dependency.
Creating a Mongo driver
mongoDriver also accepts a Promise<Db>, for constructing the Application before connect() resolves:
MongoDriverOptions
number
default:"10000"
Lease duration for the cross-process graph lock, in milliseconds.
runExclusive is an in-process mutex plus a best-effort lease in the locks collection, so two nodes writing group parentage at the same moment cannot jointly form a cycle. There is no lock option to configure, unlike the Prisma driver. An expired lease is stolen, so a crashed holder cannot wedge writes forever.string
Pins the driver to one application’s slice of a shared
Db; how the partition lands in the database is chosen by layout. A separate Db per application (client.db("alfiz_docs")) needs no option at all and is strictly preferred where databases are not rationed. Must match [A-Za-z0-9_-]+."collections" | "rows"
default:"collections"
How
partition lands in the shared Db. "collections" prefixes every collection (docs_grants, docs_epoch, docs_locks, …), which is physically unforgettable but a parallel collection set per application. "rows" (since 0.8.1) keeps one shared collection set: every document carries an app discriminator, _id becomes the compound { app, id }, and every index leads with app, the same shape as the Prisma v2 tables, so the collection list stays flat however many applications share the Db. "rows" requires an explicit partition (Mongo has no migrated-in-place default dataset the way SQL does), and the rule is one layout per shared Db, because a "rows" driver cannot see documents written without the discriminator. Both layouts pass the same isolation and mesh conformance suites.What this driver supports
The event-log methods work out of the box, soevents: { persist: true } and client revalidation need no extra setup, the right posture for serverless. Because the lease already serializes across processes, the driver qualifies for mesh write edges as shipped. Two features are not implemented: catalog history (only the head is kept, so the wildcard-drift report answers unsupported rather than wrongly) and the rolling metrics methods (metrics: {} refuses at construction). Every value the driver assigns into a query document is checked to be the expected primitive first, so an object-shaped value off the wire ({"subject": {"$ne": null}}) can never be read as a query operator.
Partitioned storage: several Applications, one backend
Both database drivers take apartition option pinning the driver, at construction and irrevocably, to one application’s slice of a shared backend. Isolation is strict and pinned by a published conformance suite; the applications interact in no way unless you opt into the mesh. The rule in shared tables is all partitioned or none: an application that omits the option lands in partition "" (Prisma) or the unprefixed collections (Mongo) beside any legacy data.
Multi-node deployments
The default in-process mutex inside the memory driver and the Prisma driver only serializes within one Node.js process. When you run multiple application instances against the same database, two concurrent group-parentage or reporting-edge writes could each pass their individual cycle checks while jointly forming a cycle. Supply the Prisma driver’slock option with a database advisory lock to prevent this; the Mongo driver’s lease covers it as shipped.
Implementing a custom driver
If you use a database without a bundled driver, such as DynamoDB or a raw SQL driver, implementStorageDriver directly. The interface is in @alfiz/application:
runExclusive serialization, which is what keeps graph-cycle detection sound under concurrency. A driver that fails it is unsound multi-node, not merely non-conforming.
Since 0.8.0 the suite also exports isolationContractCases(makePartitionedDriver) and meshContractCases(makePartitionedDriver). A custom driver claiming multi-application support over one backend is graded against the same isolation bar the bundled drivers pass, including the failure directions that leak silently: the unfiltered lists, the singletons, event-log sequencing, and the audit-chain seed. See Shared-store topologies.
The conventions the suite pins:
- Absent rows return
null, neverundefinedand never a thrown error. Both bundled drivers follow this. - Audit order is (
at, thenid), andlistAudit({ limit })returns the NEWESTlimitevents in that order rather than the oldest, whilelistAudit({ cursor })returns the firstlimitstrictly after the cursor, ascending, for export paging. The memory driver slices from the end; the Prisma driver orders ascending and takes-limit. - Catalog history is optional but honest: implement
getCatalogVersion/listCatalogVersions(and retain a row perputCatalog) to enable the wildcard-drift report; omit them and the report answersunsupportedrather than wrongly.