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

# Directory Sync and Row Reconciliation: Closing the Silent Gaps

> Authoritative directory imports that actually deprovision, and the reconciliation report that finds rows referencing users and resources your application deleted.

Two failure modes in any authorization store are silent by nature: a directory sync that only ever *adds* (so offboarding never propagates), and grant rows referencing identifiers the host application deleted (so a reused id resurrects dead access). Alfiz 0.7.0 gives both a first-class answer.

## Authoritative directory import

`importDirectory` is upsert-only by default. That is safe for partial snapshots and exactly wrong for offboarding: a user who disappears from the directory keeps `active: true`, their memberships, and their reporting edge until someone notices. `{ authoritative: true }` makes the snapshot the truth of every dataset it carries:

```ts theme={null}
const result = await app.importDirectory(snapshot, "scim:okta", {
  authoritative: true,
});
// result.deactivatedUsers      → users absent from snapshot.users, now inactive
// result.removedMemberships    → directory-group memberships swept
// result.clearedReportingEdges → manager edges the directory no longer asserts
```

Per dataset, precisely:

| Snapshot carries | Authoritative behavior                                                                                                                                                                                                                                                                                                                                                      |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `users`          | Stored users absent from it are **deactivated**, so every check answers no within the staleness bound. They are never deleted: rows stay for audit, and `deleteSubject` remains the deliberate id-retirement path.                                                                                                                                                          |
| `memberships`    | Users the map no longer lists lose their memberships in **directory-managed groups** (the snapshot's `groups` section, plus condensation-minted virtual parents). Locally-authored groups are never touched, because the directory is authoritative only over what it owns. A memberships map with no `groups` section skips the sweep with a warning rather than guessing. |
| `reportingEdges` | Stored edges the snapshot does not assert are **cleared**. A stale edge silently routes approvals to someone who left, which is worse than an unpopulated one that at least fails loudly at policy creation.                                                                                                                                                                |

Datasets the snapshot omits are untouched in either mode, groups are never deleted here (a vanished directory group may still be referenced by grants, and that finding belongs to the reconciliation below), and every deprovisioning action is audited with its reason.

Wire your SCIM endpoint or HRIS export to produce a `DirectorySnapshot` and run authoritative imports on a schedule; the warnings array is your sync-health signal, and a growing `deactivatedUsers` count with an empty directory diff is the classic sign the mapping broke.

## Row reconciliation

Grants and revokes reference subjects and scopes as **opaque strings**, by design, since your users and resources live in your tables. The database therefore offers no referential backstop. The documented discipline is calling `deleteSubject` / `deleteScope` / `notifyScopeMoved` alongside your own deletions and moves; `reconcileRows` is the detection that discipline used to lack:

```ts theme={null}
const report = await app.reconcileRows({
  userExists: async (id) => (await db.user.count({ where: { id } })) > 0,
  scopeExists: async (scope) => resolveResource(scope) !== null,
});
// report.orphanedGrants     rows whose user/org/scope no longer exists,
//                             plus group rows naming no stored group
// report.orphanedRevokes
// report.danglingRoleGrants rows naming a role this store no longer defines
```

You supply existence predicates over *your* tables; group- and role-referencing rows are checked against Alfiz's own storage with no callback. Predicates you omit are simply not checked. Implicit subjects (`directs:x`, `orgof:x`) check the user behind them.

**Detection is write-free**, so run it nightly and alert on non-empty. Then sweep deliberately:

```ts theme={null}
await app.reconcileRows({
  userExists,
  scopeExists,
  sweep: true,
  provenance: { kind: "system", note: "nightly reconciliation" },
});
```

Sweeping deletes orphans through the same audited paths as any other removal, emits the invalidation events, and records a summary entry. Two boundaries are deliberate. `danglingRoleGrants` are **reported and never swept**, because the row's subject and scope are real and deleting it could mask a role-sync bug. And a sweep without provenance is rejected before anything is touched.

<Warning>
  The sharp edge this exists for: grants key on id **strings**. If your host reuses identifiers (serial user ids, re-slugged resources), a stranded row is not garbage. It is live access waiting for the id to come back. Run detection before enabling reuse-prone id schemes, and keep `deleteSubject` in the offboarding path rather than relying on reconciliation to catch up.
</Warning>

## How the three layers fit

* **The write-path discipline** (`deleteSubject`, `deleteScope`, `notifyScopeMoved`) is the design: cleanup in the same code path as the deletion.
* **Authoritative sync** keeps the *organizational* data (users, memberships, edges) true to the directory on a cadence.
* **Reconciliation** is the backstop that finds what both missed, with a paper trail when it sweeps.
