@alfiz/application
v0.8.1
Published
The Alfiz Application: a library-embedded provider implementing the Alfiz contract against your own database, with no external dependency. Standalone, it is the org root and the complete system for one organization.
Readme
@alfiz/application
The Alfiz Application: the local provider. Implements the provider
contract against your own database through the storage seam
(StorageDriver) — one database is the sole hard requirement. A complete
in-memory driver ships as the reference; @alfiz/prisma provides the
Prisma-backed one and @alfiz/mongo the MongoDB-backed one; implement
StorageDriver yourself for anything else.
Standalone (the default), the Application is the org root: it owns
groups, roles, global grants/revokes, and the reporting tree locally, with
the full feature set — including management-layer approvals against its own
hierarchy — and no external dependency. With orgRoot: false it serves the
same data as a synced read model and rejects org-domain writes.
What lives here:
createApplication— the provider: closure supply, row operations with validation + provenance + audit, graph writes with transactional DAG enforcement (cycle paths named in errors), request workflows (auto predicates run at submit; human decisions unlock consecutive autos;alfiz_internal.requests.decide_requestis the administrative override), the approver queue, catalog publishing, virtual-parent dissolution snapshots, invalidation events.memoryDriver— the reference storage driver.- The admin lifecycle surface —
createGrants(bulk: validate-all-first, one audit entry, one invalidation per subject), caller-supplied ids oncreateRole/createGroup(migration SQL and runtime agree on identity),updateGroup(rename without touching parentage or membership),setUserActive(the reversible offboarding switch: inactive principals evaluate to no access), andlistGrants/countGrantsfiltered byroleId(role-holder counts without reading every grant). - Provenance is validated on every write, before any row is touched:
a missing
actorUserIdis aProviderWriteRejectedErrornaming the field, not a driver-level error inside the audit writer. notifyScopeMoved(scope)— the move hook: the host application owns the hierarchy behindresolveAncestors, so it must report parent-pointer changes; this emits thescopeinvalidation that busts cached ancestor chains immediately. (Now async: with event persistence on, it resolves once the move event is durable to other processes.)- Event persistence (
events: { persist: true }) — every invalidation event is appended to a sequenced log in your database before the write returns, and exposed asprovider.epoch— the signal clients use (revalidateAfterMs) to revalidate their caches across processes with one single-row read. Requires a driver implementing the optional event methods (the memory and Prisma drivers do; construction fails loudly otherwise). Retention defaults to 7 days / 100 000 rows, pruned opportunistically; a client whose cursor predates retention busts everything and resumes.ingestEvents(events)re-emits foreign events into local listeners;startEventPoller(app)(fromevents.ts) tails the log on an interval for push-like invalidation on long-lived nodes — optional sugar, correctness never depends on it. - Metrics (
metrics: {}) — rolling permission-usage buckets (daily by default) keyed by grant, revoke, role, permission, and scope type, fed byreportMetricsand read back withgetGrantUsage/getRevokeUsage/getRoleUsage/getPermissionUsage/getScopeTypeUsage. Off by default and advertised throughcapabilities().metrics, so a deployment that has not opted in stores and renders nothing. Requires a driver implementing the optional metric methods (memory and Prisma do; construction fails loudly otherwise). Retention defaults to 90 days, compacted opportunistically. Writes are pre-aggregated batches delivered off the request path and never awaited by it:createProviderMetricsSink(core) is the wiring, and it drops batches under back-pressure rather than queueing — losing counts is the right failure mode for a counter, adding latency is not. Per-grantsoleMatchis whatrevocationSafeguard(core) keys on. - Closure-supply performance — the group-parent topology is cached
per Application (
groupTopologyTtlMs, default 30s,0disables), busted synchronously by local group writes and by ingested events, so a cache miss no longer re-reads every group in the organization; roles referenced by a grant set are batch-read once (StorageDriver.getRoles, optional, with a parallel per-id fallback); independent queries run concurrently. deleteSubject(subject, provenance)/deleteScope(scope, provenance)— the deletion hooks, same discipline as moves: grants key on subject and scope STRINGS, so the code path that deletes a principal or a resource must sweep its rows here, or a reused id inherits the stranded access. User deletion also removes revokes, the stored record, implicit-group grants, and cancels pending requests.- Sessions (
session.ts) — actor/subject split with view-as narrowing: every check intersects the previewed subject with the actor's REAL access, so previews can only narrow.serializeViewAs/parseViewAsfor cookie plumbing. - Service principals (
service-principal.ts) — the timing-safe env-key shim with rotation lists; pair with@alfiz/verify's client-reach guard. - Directory ingestion (
importDirectory) — groups/memberships/reporting edges from Entra/Okta/LDAP-shaped snapshots; cyclic group nesting is auto-condensed into virtual parents, cyclic reporting edges are skipped with warnings, never silently combined.
Shared-store topologies: partitioned storage and the mesh
Since 0.8.0, several Applications can share one database. Both database
drivers take a partition option pinning a driver to one application's
slice of the tables at construction — strict isolation, zero interaction,
no contract changes (memoryBackend() is the in-memory analogue for
tests). On top of that, the mesh is an opt-in topology in which
applications can be granted read or write access to each other's
partitions:
import { connectMesh, openPeerApplication, startMeshEventPoller } from "@alfiz/application";
const mesh = connectMesh({
partition: "docs",
driver: (p) => prismaDriver(prisma, { partition: p, lock }),
});
await mesh.verify({ peers: { read: ["zoom"] } }); // boot check: refuse on mismatch
// The write path: a full Application over the NEIGHBOR's partition and
// published catalog — validation, graph integrity, audit into zoom's log.
const zoom = await openPeerApplication(mesh, "zoom", { as: "admin@corp" });
await zoom.createGrants([...], provenance);
// First-party imports: imported namespaces resolve against the live
// neighboring partition; peer global-scope grants and roles join
// evaluation; peer event logs feed local invalidation.
const app = createApplication({
catalog, storage: mesh.storage(),
mesh: { connection: mesh, imports: true },
});
await app.verifyMeshImports();
await startMeshEventPoller(app, mesh);The registry (a reserved __mesh meta-partition) records members, edges,
and optionally an org partition homing organizational-domain data for
the whole mesh (promoteOrgPartition is the audited handoff; members then
boot orgRoot: false). The storage seam never learns cross-partition
addressing — a driver physically cannot reach a neighbor; the peer
Application is the single, edge-checked road in. Write edges demand the
multi-node prerequisites (cross-process runExclusive, the persisted
event log) and refuse loudly without them. Isolation is cooperative,
not adversarial: every member holds credentials to the whole table set,
so deployments needing isolation against a compromised co-tenant use
separate schemas/databases with separate credentials instead.
Relay
The Application side of the Alfiz Cloud relay: createRelayHandler serves
the provider contract over one bearer-authenticated POST endpoint, so a
linked Application is reachable by the hosted dashboard while remaining
the org root and the sole writer. Every relayed operation lands in the
same provider methods local code calls — org-root gating, validation,
graph integrity, and audit apply to relayed writes exactly as to local
ones. Mount it at an internal route:
import { createRelayHandler } from "@alfiz/application";
import { app, storage } from "@/lib/alfiz";
export const POST = createRelayHandler({
application: app,
storage,
secret: process.env.ALFIZ_RELAY_SECRET!,
applicationId: "docs",
});The secret is minted at the Alfiz Cloud link step; keep it in an
environment variable. The storage option enables the org-snapshot ops
(promotion, demotion, read-model sync); onAuthorityChanged is called
after an authority-transfer snapshot applies, so the host can reconstruct
its Application with the new orgRoot flag — a constructor commitment the
library cannot flip at runtime. Typed errors survive the wire
(ProviderWriteRejectedError codes, GraphCycleError paths), and
createRelayProvider(target) is the calling side: an AlfizProvider over
fetch that exposes the linked Application's epoch. Runtime checks never
traverse the relay — every can() runs in-process, and nothing in the
protocol is on any request path.
Every driver must pass the contract suite, published as
@alfiz/application/driver-suite: driverContractCases (the base seam),
eventLogContractCases and metricsContractCases (the optional methods),
and — for drivers claiming multi-application support over one backend —
isolationContractCases(makePartitionedDriver) and
meshContractCases(makePartitionedDriver), which grade partition
isolation and the mesh's cooperative layer against the same bar the
bundled drivers pass.
