Skip to main content
The storage seam is the single interface a database must satisfy to host an Alfiz Application. All application semantics (graph integrity, request workflows, org-root gating, the audit log) live above the seam in the Application layer and are identical regardless of which driver you use. Drivers store and retrieve; they never interpret. All IDs are assigned by the Application.

The StorageDriver interface

A 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.
Each call to memoryDriver() returns an independent, empty store. Two calls produce two isolated stores, which helps when testing multi-tenant scenarios or provider federation in-process.
The memory driver is not persistent. Everything is lost when the process exits. Use it for tests and local dev only; use prismaDriver (or a custom driver) for anything you need to survive a restart.

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

Pass the 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:
A generated 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 your schema.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, so events: { 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 a partition 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.
See Shared-store topologies for the full picture: what collides without partitioning, the mesh’s access edges, and the org partition.

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’s lock 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, implement StorageDriver directly. The interface is in @alfiz/application:
Conventions a custom driver has to get right, and since 0.7.0 a published conformance suite that checks them for you:
The suite covers the row round-trips, the audit ordering and filters below, catalog history, and the case that matters most for correctness, 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, never undefined and never a thrown error. Both bundled drivers follow this.
  • Audit order is (at, then id), and listAudit({ limit }) returns the NEWEST limit events in that order rather than the oldest, while listAudit({ cursor }) returns the first limit strictly 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 per putCatalog) to enable the wildcard-drift report; omit them and the report answers unsupported rather than wrongly.