> ## Documentation Index
> Fetch the complete documentation index at: https://docs.alfiz.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# createApplication: Alfiz Local Provider Setup Guide

> createApplication wires catalog, storage, and ancestry into the Alfiz Application, a fully local authorization engine with no external dependency.

`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

```ts theme={null}
import { createApplication } from "@alfiz/application";

function createApplication<Cat extends AnyCatalog>(
  options: Omit<ApplicationOptions, "catalog"> & { catalog: Cat },
): AlfizApplication<Cat["$pattern"], Cat["$scope"]>
```

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

<ParamField body="catalog" type="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.
</ParamField>

<ParamField body="storage" type="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](/api/storage-drivers) for details.
</ParamField>

<ParamField body="ancestry" type="AncestryResolver" optional>
  The ancestry seam: a function (or async function) that resolves a scope instance's ancestor chain from your application's own tables.

  ```ts theme={null}
  type AncestryResolver = (scope: ScopeId) => ScopeId[] | Promise<ScopeId[]>
  ```

  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.
</ParamField>

<ParamField body="orgRoot" type="boolean" default="true" optional>
  Controls whether this Application is the authoritative writer of organizational-domain data. See [orgRoot behavior](#orgroot-behavior) below.
</ParamField>

<ParamField body="clock" type="() => number" optional>
  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).
</ParamField>

<ParamField body="ids" type="() => string" optional>
  A function that generates unique IDs for new rows. Defaults to `randomUUID` from `node:crypto`. Override in tests if you need deterministic IDs.
</ParamField>

<ParamField body="events" type="{ persist?: boolean; retention?: { maxAgeMs?: number; maxRows?: number } }" optional>
  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](#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](/enforcement/audit).
</ParamField>

<ParamField body="sod" type="{ enforce?: &#x22;off&#x22; | &#x22;reject&#x22; }" optional>
  What a grant write violating a catalog-declared [separation-of-duty constraint](/access/separation-of-duties) 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.
</ParamField>

<ParamField body="audit" type="{ hashChain?: boolean }" optional>
  `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](/enforcement/audit).
</ParamField>

<ParamField body="groupTopologyTtlMs" type="number" default="30000" optional>
  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.
</ParamField>

<ParamField body="metrics" type="{ bucketMs?: number; retentionMs?: number }" optional>
  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](/api/metrics). 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.

  <Note>
    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.
  </Note>
</ParamField>

## Return value

<ResponseField name="AlfizApplication" type="AlfizApplication">
  An `AlfizApplication` instance implementing the `AlfizProvider` contract. Pass this as the `provider` when constructing an `AlfizClient` via `createAlfizClient`.
</ResponseField>

## `orgRoot` behavior

<Tabs>
  <Tab title="orgRoot: true (default)">
    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.
  </Tab>

  <Tab title="orgRoot: false">
    The org root lives elsewhere; this Application holds organizational-domain data as a **synced read model**. Local writes to org-domain data (global-scope grants, groups, reporting edges, user provisioning) are rejected with `ProviderWriteRejectedError`. Use this setting in federated deployments where a central service manages the org layer and the local Application only handles resource-scoped grants.
  </Tab>
</Tabs>

## 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 `"*"`.

```ts theme={null}
import { parentPointerResolver } from "@alfiz/core";

// The simplest conforming implementation: wrap a single parent-pointer lookup.
const ancestry = parentPointerResolver((scope) => myDb.parentOf(scope));
```

`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:

```ts theme={null}
const ancestry: AncestryResolver = async (scope) => {
  const ancestors: ScopeId[] = [];
  let current = await myDb.parentOf(scope);
  while (current) {
    ancestors.push(current);
    current = await myDb.parentOf(current);
  }
  ancestors.push("*");
  return ancestors;
};
```

<Note>
  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.
</Note>

## 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.

```ts theme={null}
import { createApplication, startEventPoller } from "@alfiz/application";
import { prismaDriver } from "@alfiz/prisma";

const app = createApplication({
  catalog,
  storage: prismaDriver(prisma),
  ancestry,
  events: { persist: true }, // durable invalidation log (the default with this driver)
});

// Optional: on long-lived nodes, tail the log for push-like invalidation
// between client revalidations. Correctness never depends on this. It's
// sugar over the same log the client already validates against.
const poller = startEventPoller(app, { intervalMs: 5_000 });
process.on("SIGTERM", () => poller.stop());
```

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](/api/create-alfiz-client) for the read-side configuration.

## Application methods overview

The returned `AlfizApplication` exposes the full provider surface. Key method groups:

| Category      | Methods                                                                                                                          |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| **Grants**    | `createGrant`, `createGrants`, `deleteGrant`, `listGrants`, `countGrants`                                                        |
| **Revokes**   | `createRevoke`, `deleteRevoke`, `listRevokes`                                                                                    |
| **Roles**     | `createRole`, `updateRole`, `deleteRole`, `listRoles`                                                                            |
| **Groups**    | `createGroup`, `updateGroup`, `deleteGroup`, `setGroupParents`, `setGroupMembership`, `getGroupMembers`, `dissolveVirtualParent` |
| **Users**     | `setUserActive`, `setReportingEdge`, `getReportingEdges`                                                                         |
| **Requests**  | `submitRequest`, `decideRequest`, `cancelRequest`, `listRequests`, `listApproverQueue`                                           |
| **Directory** | `importDirectory`                                                                                                                |
| **Cleanup**   | `deleteSubject`, `deleteScope`                                                                                                   |
| **Catalog**   | `publishCatalog`, `getPublishedCatalog`                                                                                          |
| **Audit**     | `listAuditEvents`                                                                                                                |
| **Events**    | `onInvalidate`, `notifyScopeMoved`, `ingestEvents`, `epoch`                                                                      |

<Note>
  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.
</Note>

## Setup examples

<CodeGroup>
  ```ts Development (memory driver) theme={null}
  import { defineCatalog, createAlfizClient, parentPointerResolver } from "@alfiz/core";
  import { createApplication, memoryDriver } from "@alfiz/application";

  export const catalog = defineCatalog({
    namespaces: ["docs"],
    permissions: {
      "docs.files.read": { scopes: ["docs.folder", "docs.doc"] },
      "docs.files.update_file": { scopes: ["docs.folder", "docs.doc"] },
      "docs.files.delete": { scopes: ["docs.folder"] },
    },
    scopeTypes: {
      "docs.folder": { parent: null },
      "docs.doc": { parent: "docs.folder" },
    },
  });

  // Stand-in for your own tables. In production this reads your rows.
  const parents = new Map<string, string | null>([
    ["docs.doc:123", "docs.folder:42"],
    ["docs.folder:42", null],
  ]);

  const app = createApplication({
    catalog,
    storage: memoryDriver(),
    // `docs.doc` declares a parent, so it IS hierarchical, and omitting `ancestry`
    // installs a resolver that returns no ancestors, and a check at
    // `docs.doc:123` would then miss a grant on its folder. Omit `ancestry`
    // only when every scope type is `parent: null`.
    ancestry: parentPointerResolver((scope) => parents.get(scope) ?? null),
  });

  export const alfiz = createAlfizClient({ catalog, provider: app });
  ```

  ```ts Production (Prisma driver) theme={null}
  import { defineCatalog, createAlfizClient, parentPointerResolver } from "@alfiz/core";
  import { createApplication } from "@alfiz/application";
  import { prismaDriver } from "@alfiz/prisma";
  import { PrismaClient } from "@prisma/client";

  const prisma = new PrismaClient();

  export const catalog = defineCatalog({
    namespaces: ["docs"],
    permissions: {
      "docs.files.read": { scopes: ["docs.folder", "docs.doc"] },
      "docs.files.update_file": { scopes: ["docs.folder", "docs.doc"] },
      "docs.files.delete": { scopes: ["docs.folder"] },
    },
    scopeTypes: {
      "docs.folder": { parent: null },
      "docs.doc": { parent: "docs.folder" },
    },
  });

  const app = createApplication({
    catalog,
    storage: prismaDriver(prisma),
    // `parentPointerResolver` wraps a SYNCHRONOUS lookup; an async database read
    // implements `AncestryResolver` directly, which may return a promise.
    ancestry: async (scope) => {
      const ancestors: string[] = [];
      let current: string | null = scope;
      while (current) {
        const row = await prisma.resource.findUnique({
          where: { id: current.split(":")[1] },
          select: { parentId: true, parentType: true },
        });
        if (!row?.parentId) break;
        current = `${row.parentType}:${row.parentId}`;
        ancestors.push(current);
      }
      return ancestors;
    },
    orgRoot: true,
  });

  export const alfiz = createAlfizClient({ catalog, provider: app });
  ```
</CodeGroup>
