Skip to main content
createApplication wires together your catalog, your storage driver, and your object-hierarchy resolver into the Alfiz Application, a library-embedded authorization engine that runs entirely in-process. The Application implements the full provider contract (grants, revokes, roles, groups, access requests, directory ingestion, the audit log) against your own database. No external service is involved at runtime: every can() check resolves locally.

Signature

Like createAlfizClient, the factory is generic over the catalog: it threads the catalog’s derived pattern and scope-id unions onto the returned Application, so the write paths autocomplete too. createGrant, createRevoke, createRole, submitRequest, and notifyScopeMoved all take typed pattern and scope arguments at the call site, which is what makes seeding scripts and data migrations catch a typo’d pattern at compile time. Construct with new AlfizApplication(...) and you lose that inference.

Parameters

AnyCatalog
required
The catalog produced by defineCatalog(). The Application validates every grant and revoke against it, so unknown patterns, scope-type mismatches, and group references in key positions are all rejected at write time.
StorageDriver
required
The storage driver the Application reads and writes through. Use memoryDriver() during development and tests; use prismaDriver from @alfiz/prisma in production. You can also implement the StorageDriver interface yourself over any other database. See Storage Drivers for details.
AncestryResolver
The ancestry seam: a function (or async function) that resolves a scope instance’s ancestor chain from your application’s own tables.
The resolver must return the ancestor chain of scope, ordered nearest-first, ending at the global scope "*". The chain excludes scope itself. For multi-parent scope types the result is the deduplicated union of all parents’ chains.Omit this field only for fully-global deployments where every scope is "*". Any catalog that declares scope types with parent set (i.e., any scoped permission tree) requires an ancestry resolver. Without one, scoped checks will not walk the hierarchy.
boolean
default:"true"
Controls whether this Application is the authoritative writer of organizational-domain data. See orgRoot behavior below.
() => number
A function that returns the current time as epoch milliseconds. Defaults to Date.now. Override in tests to control time-based behavior (grant expiry, request duration validation).
() => string
A function that generates unique IDs for new rows. Defaults to randomUUID from node:crypto. Override in tests if you need deterministic IDs.
{ persist?: boolean; retention?: { maxAgeMs?: number; maxRows?: number } }
Persist invalidation events to a sequenced log in your database, exposing them on the provider as epoch. This is the signal clients use (via revalidateAfterMs) to tighten cross-process staleness from a blind TTL to a single revalidation window. Since 0.7.0, persistence defaults ON whenever the storage driver implements the event methods (appendEvents, headSeq, eventsSince, pruneEvents), which both bundled drivers do. Pass { persist: false } to opt out, or an explicit { persist: true } to demand it (construction then fails loudly against an incapable driver, where the auto default degrades honestly to the pre-epoch contract instead). See Cross-process invalidation below.Retention defaults to 7 days / 100 000 rows, pruned opportunistically. Retention only needs to cover the longest interval between one client’s revalidations; a client whose cursor predates retention gets a gap and busts everything. Event-log pruning never touches the audit log.
{ enforce?: "off" | "reject" }
What a grant write violating a catalog-declared separation-of-duty constraint does. "off" (default): constraints are detective only, so read listSodViolations(). "reject": a user-subject grant that would create a new violation is rejected with code conflict; group- and role-shaped writes always pass through and remain the report’s job.
{ hashChain?: boolean }
hashChain: true makes every audit entry carry a SHA-256 hash over its canonical serialization plus the previous entry’s hash, making edits, deletions, and reordering detectable with verifyAuditChain. Chained appends serialize through runExclusive("audit", …), so multi-node deployments need the driver’s cross-process lock for the chain to stay linear. Off by default. See Audit log.
number
default:"30000"
How long (in milliseconds) the group-parent topology is cached inside the Application. This cache is what lets getSubjectAccess (the closure fan-out behind every check miss) skip re-reading every group in the organization on every miss. Local group writes and ingested foreign events bust it synchronously; this TTL only bounds staleness for group writes made by another process against the same database. Set to 0 to disable and restore the per-miss scan.
{ bucketMs?: number; retentionMs?: number }
Store permission-usage metrics: rolling counter buckets keyed by grant id, revoke id, role id, permission key, and scope type, fed by reportMetrics and read back by getGrantUsage, getRevokeUsage, getRoleUsage, getPermissionUsage, and getScopeTypeUsage, the data behind the revocation safeguard. Off by default.bucketMs is the bucket granularity, defaulting to one day; storage is bounded by attributed rows × retention ÷ granularity, so this is the knob that trades resolution for rows. retentionMs defaults to 90 days, pruned opportunistically off the write path.Requires a storage driver implementing recordMetrics, readMetrics, and pruneMetrics, backed by the AlfizMetric model in the Prisma fragment. Construction throws when they are missing, on the same reasoning as events.persist: silently accepting metrics that go nowhere would make every safeguard read a confident zero.
A deployment that only wants numbers in its own metrics stack does not need this. Point the client’s metrics.observer at OpenTelemetry or a local aggregator and store nothing here.

Return value

AlfizApplication
An AlfizApplication instance implementing the AlfizProvider contract. Pass this as the provider when constructing an AlfizClient via createAlfizClient.

orgRoot behavior

The Application is the org root: it owns organizational-domain data (groups, roles, global grants and revokes, the reporting tree) and is the sole writer of it. This is the correct setting for standalone single-organization deployments.

The ancestry resolver

The ancestry resolver is the bridge between Alfiz’s permission evaluation engine and your application’s object hierarchy. Because you own the hierarchy (a document lives in a folder, a folder lives in a workspace), only your code knows which ancestors a given scope has. The resolver is called during scoped can() checks; its output is the ancestor chain Alfiz uses to walk from the checked scope up to "*".
parentPointerResolver handles single-parent hierarchies and multi-parent cases (returning an array of parents), deduplication, and cycle detection. Implement the resolver directly for more complex cases:
If you move a resource (change its parent pointer), call app.notifyScopeMoved(scope) from the same code path. This emits the scope invalidation event that immediately busts cached ancestor chains. Without it, staleness is bounded only by the client’s object-chain TTL (default 60 seconds).notifyScopeMoved returns a Promise<void> that resolves once the move event is durable (with events.persist on), so await it before returning success from the endpoint that performed the move if you need other processes to see the invalidation before the response ships. Fire-and-forget callers need no change.

Cross-process invalidation

The in-process invalidation stream never leaves the process that wrote the event, so in a multi-node or serverless deployment the client’s TTLs are the only bound on how quickly a revocation on one instance takes effect on another. Turning on events.persist appends every invalidation event to a sequenced log in your database before the write returns, exposing it as provider.epoch. Clients configured with revalidateAfterMs then validate their caches against that log with one constant-cost single-row read per window.
Compatibility:
  • The Prisma schema fragment ships two new additive models (AlfizEpoch, AlfizEvent). Merge and migrate as usual, with no backfill and no seed. See the @alfiz/prisma README.
  • Multi-node deployments should already be passing a database advisory lock to prismaDriver; event appends serialize under the same lock.
  • Custom storage drivers keep compiling, because the four event methods and getRoles are all optional. When events.persist is on and the driver doesn’t implement the event methods, construction fails loudly rather than silently degrading.
See the client’s revalidateAfterMs and cacheStore options for the read-side configuration.

Application methods overview

The returned AlfizApplication exposes the full provider surface. Key method groups:
Call deleteSubject(subject, provenance) from the same code path that deletes a user or service account. Call deleteScope(scope, provenance) from the same code path that deletes a resource. These remove stranded grant and revoke rows; skipping them means a reused id inherits the previous principal’s access.

Setup examples