@alfiz/mongo
v0.8.1
Published
The MongoDB storage driver for the Alfiz Application: implements the storage seam directly over the official `mongodb` driver, including the persisted invalidation log and a cross-process lease for graph-write serialization.
Readme
@alfiz/mongo
The MongoDB storage driver for the Alfiz Application. It implements the
storage seam (StorageDriver from @alfiz/application) directly over the
official mongodb driver — there is
no schema to merge and no code generation step; point it at a database and
go.
Construct the driver
import { MongoClient } from "mongodb";
import { createApplication } from "@alfiz/application";
import { mongoDriver } from "@alfiz/mongo";
const client = new MongoClient(process.env.MONGO_URL!);
await client.connect();
const storage = mongoDriver(client.db("alfiz"));
const app = createApplication({ storage /* ... */ });mongoDriver also accepts a Promise<Db>, for callers that want to
construct the Application before client.connect() resolves:
const storage = mongoDriver(client.connect().then((c) => c.db("alfiz")));Collections (grants, revokes, roles, groups, users, requests,
catalog, audit, epoch, events, locks) and their hot-path indexes are
created lazily on first use — no migration step. Every row is stored with
its Alfiz id as Mongo's _id, for free uniqueness and point reads.
Partitioned storage: several Applications, one deployment
Three mechanisms. A separate Db per application —
client.db("alfiz_docs") — works as shipped with zero configuration and
is strictly preferred where databases are not rationed: isolation holds by
construction. Where one Db must be shared, choose a layout:
// Collection prefix (the default layout): docs_grants, docs_epoch, docs_locks, …
const docs = mongoDriver(db, { partition: "docs" });
// Row partitioning: ONE shared collection set — grants, epoch, locks — with
// an `app` discriminator per document and compound { app, id } _ids,
// matching the Prisma v2 layout exactly. Since 0.8.1.
const docs = mongoDriver(db, { partition: "docs", layout: "rows" });The prefix is the physically stronger mechanism — no query can forget
the predicate, and indexes are unchanged — at the cost of a parallel
collection set per application, which gets noisy in Compass with many
co-tenants. layout: "rows" keeps the collection list flat however many
applications share the Db: every query the driver issues is
partition-scoped through one shared helper, every index leads with app,
and lease keys carry the partition, so co-tenants never falsely contend.
Both layouts are graded by the same isolation and mesh conformance
suites; above the seam nothing can tell them apart.
Either way, a driver is pinned to its partition at construction and
cannot address any other. The recommended partition key is the
application's primary catalog namespace, and the rules in a shared Db
are all partitioned or none and one layout per Db — an
application that omits the option lands in the unprefixed collections
beside any legacy data, and a "rows" driver cannot see documents
written without the discriminator (which is also why "rows" requires
an explicit partition: there is no migrated-in-place default dataset
on Mongo the way there is on SQL). Because the lease already serializes
across processes, this driver qualifies for mesh WRITE edges
(StorageDriver.crossProcess) as shipped, under either layout.
The invalidation log and cross-process locking
The epoch/events collections back the Application's
events: { persist: true } option out of the box — no extra setup, unlike
the Prisma driver's additive schema models. runExclusive (graph-write
serialization) is a promise-chain mutex in-process, plus a best-effort lease
in the locks collection cross-process: two nodes writing group parentage at
the same moment cannot jointly form a cycle. An expired lease is stolen, so a
crashed holder can't wedge writes forever — configurable via
leaseMs (default 10s):
const storage = mongoDriver(db, { leaseMs: 5_000 });Catalog history and permission metrics
This driver keeps only the catalog head (no AlfizCatalogVersion
equivalent), so the wildcard-drift report answers unsupported rather than
wrongly. It also does not implement the optional rolling-metrics methods.
Both are OPTIONAL on StorageDriver — the Application detects their absence
and refuses the corresponding feature at construction rather than accepting
calls that go nowhere.
Query safety
Nothing upstream of a storage driver validates the runtime type of an id
or filter value — @alfiz/application's Provider API checks field
presence, not shape, so a JSON body carrying {"subject": {"$ne": null}}
reaches the driver unmolested. Every value this driver assigns into a Mongo
query document — an id, or a filter field such as subject, scope,
roleId, actor — is checked to be the expected primitive first and
rejected with a TypeError otherwise, so an object-shaped value can never be
read as a query operator instead of data.
What lives where
The driver stores and retrieves; it never interprets. All ids are opaque strings assigned by the Application layer, which also owns graph integrity, request workflows, catalog versioning, and the audit log.
