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

# Installation guide

# Installation guide

This guide is for an engineer installing and configuring the Find My Data platform — locally for evaluation and development, or on Azure for a single-tenant production deployment. It covers prerequisites, the seeded demo state, the complete `FMD_*` configuration reference, production boot lockouts, worker topology, and the feature-gate matrix. Current release: **26.7.15.0** (scheme `year.month.day.x`; see `VERSION` at the repo root). For what is implemented vs. mocked vs. gated, [STATUS.md](../STATUS.md) is the authoritative honesty ledger.

## Prerequisites

* **[Bun](https://bun.sh) ≥ 1.3** (tested with **1.3.14**). Bun is the only requirement — no Node.js, no database server, no Docker, no cloud credentials for the local path.

```bash theme={null}
curl -fsSL https://bun.sh/install | bash
```

## Local installation

```bash theme={null}
git clone <your-clone-url> FindMyData
cd FindMyData
bun install          # install workspace dependencies
bun run seed         # create + migrate the dev database and seed the demo org
bun run dev          # start API (http://localhost:8710) + web (http://localhost:5173)
```

`bun run dev` (`scripts/dev.ts`) starts two processes: the API server (`packages/server`, Bun with `--watch`, default port **8710**) and the Vite web dev server (`packages/web`, port **5173**). Open **[http://localhost:5173](http://localhost:5173)** and sign in with the development persona picker (see [What the seed creates](#what-bun-run-seed-creates)).

Verify the API directly:

* `GET http://localhost:8710/api/health` → `{"status":"ok"}`
* `GET http://localhost:8710/api/ready` → readiness plus per-integration modes (see [Readiness and honest integration modes](#readiness-and-honest-integration-modes))

Other root scripts (from the root `package.json`):

| Command             | What it does                                                                                                    |
| ------------------- | --------------------------------------------------------------------------------------------------------------- |
| `bun test`          | Run the test suite (`packages/shared` + `packages/server`) — 196 passing at this release                        |
| `bun run typecheck` | Strict TypeScript across all packages                                                                           |
| `bun run lint`      | oxlint over all package sources                                                                                 |
| `bun run migrate`   | Apply database migrations without seeding                                                                       |
| `bun run worker`    | Run the durable worker as a standalone process (see [Worker topology](#worker-topology-embedded-vs-standalone)) |
| `bun run loadgen`   | Generated-event load harness (see [capacity model](capacity-model.md))                                          |
| `bun run build`     | Production build of the web app                                                                                 |

The dev database and artifacts live under `packages/server/data/` (gitignored). To reset:

```bash theme={null}
rm -rf packages/server/data && bun run seed
```

### Seed guards

`bun run seed` (`packages/server/src/fixtures/seed-cli.ts`) is a development-only operation with two hard refusals:

* Exits with an error if `FMD_ENV` is not `development`.
* Exits with an error if the database already contains a tenant — it never overwrites. Delete the data directory to reseed.

## What `bun run seed` creates

The seed builds the complete **Meridian Grove Holdings** demo state (a fictional mid-market retail/logistics group — all people, groups, and documents are synthetic) by running the same services and scan pipeline the UI uses:

* Tenant + org profile (industries Retail/Logistics; geographies United States/European Union) and an uploaded governance policy document.
* A sensitivity-label catalog fixture.
* An AI-generated taxonomy draft (deterministic mock AI provider), then governance-approved **Human Resources** and **Finance** business domains with owners assigned.
* A mock Microsoft 365 connector instance (`Microsoft 365 (deterministic fixture)`), discovered source scopes (the HR screening drive is marked as HR's expected/authoritative location), and a **completed baseline inventory scan of every scope through the real durable pipeline**.

Seeded personas (`packages/server/src/fixtures/identity.ts`) — one per visible role, plus a second domain owner so cross-domain isolation is always demonstrable and a second governance admin so propose ≠ approve separation of duties works on model releases:

| Persona       | Title                        | Role                                                       |
| ------------- | ---------------------------- | ---------------------------------------------------------- |
| Avery Chen    | Platform Administrator       | `platform_admin`                                           |
| Sam Whitfield | Governance Lead              | `governance_admin`                                         |
| Lena Fischer  | Model Governance Lead        | `governance_admin` (second admin for separation of duties) |
| Priya Raman   | HR Operations Director       | `domain_owner` (Human Resources)                           |
| Diego Alvarez | Finance Controller           | `domain_owner` (Finance)                                   |
| Morgan Ellis  | Information Security Manager | `remediation_approver`                                     |
| Ren Nakamura  | Internal Auditor             | `auditor`                                                  |

Fixture groups referenced by the mock tenant's permission grants: `FMD-CDX HR Screening`, `FMD-CDX HR Owners`, `Project Phoenix Team`.

## Configuration reference

All configuration is environment variables, parsed and validated in [`packages/server/src/kernel/config.ts`](../packages/server/src/kernel/config.ts) (Zod schema; invalid values fail boot). Copy [.env.example](../.env.example) to `.env` as a starting point. Booleans are enabled only by the exact string `true`.

### Secret-reference indirection (`*_REF`)

Variables ending in `_REF` never hold a secret. Their value is the **name of another environment variable** that holds the secret. Example:

```bash theme={null}
FMD_GRAPH_CLIENT_SECRET_REF=GRAPH_CLIENT_SECRET   # points at the env var below
GRAPH_CLIENT_SECRET=<the actual secret>            # e.g. injected from Key Vault
```

This keeps secret values out of configuration files and templates; in the Azure deployment the pointed-at variable is a Key-Vault-backed Container App secret resolved by managed identity. Readiness treats a capability as configured only when the reference actually **resolves** to a value — a dangling reference reports `blocked`, never `live`.

### Core

| Variable             | Type / values                 | Default              | Effect                                                                                                                                                                                                                                                                                                      |
| -------------------- | ----------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FMD_ENV`            | `development` \| `production` | `development`        | Environment. `production` activates the [boot lockouts](#production-boot-lockouts).                                                                                                                                                                                                                         |
| `FMD_AUTH_MODE`      | `dev` \| `entra`              | `dev`                | `dev` = seeded persona picker (banner-marked, never for production); `entra` = Microsoft Entra OIDC sign-in **seam** — the code flow is not implemented in this build and its endpoint responds `501` until it is (see [STATUS.md](../STATUS.md) and [permissions manifest §1.2](permissions-manifest.md)). |
| `FMD_API_PORT`       | integer 1–65535               | `8710`               | API listen port.                                                                                                                                                                                                                                                                                            |
| `FMD_DB_PATH`        | string (path)                 | `./data/fmd.db`      | SQLite database file; created and migrated at boot ([ADR-0003](adr/ADR-0003-persistence-single-sqlite-behind-role-interfaces.md)).                                                                                                                                                                          |
| `FMD_ARTIFACT_DIR`   | string (path)                 | `./data/artifacts`   | Root directory of the purpose-flagged object/evidence artifact store.                                                                                                                                                                                                                                       |
| `FMD_SESSION_SECRET` | string, min 8 chars           | `dev-only-change-me` | Session-cookie signing secret; also derives the per-subscription `clientState` for change-notification verification. The default value is refused in production.                                                                                                                                            |

### Connector mode

| Variable             | Type / values     | Default | Effect                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| -------------------- | ----------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FMD_CONNECTOR_MODE` | `mock` \| `graph` | `mock`  | `mock` = deterministic fixture M365 tenant (the fully exercised path). `graph` = live Microsoft Graph adapter — constructed only when the Graph identifiers below are present and the secret reference resolves; otherwise every use raises an explicit `BlockedError` (never a silent mock fallback, [ADR-0012](adr/ADR-0012-cdx-strategy.md)). The live connector was validated **read-only** against the Contoso CDX tenant ([ADR-0013](adr/ADR-0013-live-graph-connector.md), [CDX runbook](cdx-test-runbook.md)). |

### Microsoft Graph identifiers and secret references

Two separate app registrations by design — read/scan vs. remediation — for blast-radius containment (see [permissions manifest §1.1](permissions-manifest.md)). All are strings with no default; unset means the corresponding live path is `blocked`.

| Variable                                  | Identity    | Effect                                                                |
| ----------------------------------------- | ----------- | --------------------------------------------------------------------- |
| `FMD_GRAPH_TENANT_ID`                     | read/scan   | Entra tenant ID for the Graph connector.                              |
| `FMD_GRAPH_CLIENT_ID`                     | read/scan   | Client ID of the read/scan app registration.                          |
| `FMD_GRAPH_CLIENT_SECRET_REF`             | read/scan   | Name of the env var holding the read/scan client secret.              |
| `FMD_GRAPH_REMEDIATION_CLIENT_ID`         | remediation | Client ID of the separate remediation (label-write) app registration. |
| `FMD_GRAPH_REMEDIATION_CLIENT_SECRET_REF` | remediation | Name of the env var holding the remediation client secret.            |

### AI provider

| Variable                       | Type / values                           | Default           | Effect                                                                                                                                                                                                                                                                                                                                                  |
| ------------------------------ | --------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FMD_AI_PROVIDER`              | `mock` \| `azure-openai` \| `anthropic` | `mock`            | Provider behind the AI gateway ([ADR-0005](adr/ADR-0005-ai-provider-gateway.md)). `mock` is deterministic, needs no credentials, and is the only provider exercised by tests. The Azure OpenAI and Anthropic adapters are built but **unvalidated against live endpoints this run** — readiness reports them `blocked` until their credentials resolve. |
| `FMD_AZURE_OPENAI_ENDPOINT`    | string                                  | —                 | Azure OpenAI endpoint (required for `azure-openai`).                                                                                                                                                                                                                                                                                                    |
| `FMD_AZURE_OPENAI_DEPLOYMENT`  | string                                  | —                 | Azure OpenAI deployment name (required for `azure-openai`).                                                                                                                                                                                                                                                                                             |
| `FMD_AZURE_OPENAI_API_KEY_REF` | string (ref)                            | —                 | Name of the env var holding the Azure OpenAI API key.                                                                                                                                                                                                                                                                                                   |
| `FMD_ANTHROPIC_API_KEY_REF`    | string (ref)                            | —                 | Name of the env var holding the Anthropic API key (required for `anthropic`).                                                                                                                                                                                                                                                                           |
| `FMD_ANTHROPIC_MODEL`          | string                                  | `claude-sonnet-5` | Anthropic model identifier (optional; the adapter defaults to `claude-sonnet-5` when unset).                                                                                                                                                                                                                                                            |

### Webhook public URL

| Variable                 | Type                           | Default | Effect                                                                                                                                                                                                                                                                                                                             |
| ------------------------ | ------------------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FMD_WEBHOOK_PUBLIC_URL` | string (public HTTPS base URL) | —       | Base URL Microsoft Graph can reach for change notifications. The receiver endpoints are `<base>/api/webhooks/graph/notifications` and `<base>/api/webhooks/graph/lifecycle`. Without it, live subscription creation raises `BlockedError` and readiness reports `changeNotifications: blocked`. Mock-mode simulation needs no URL. |

### Exchange mailboxes

| Variable                 | Type                 | Default | Effect                                                                                                                                                                                                           |
| ------------------------ | -------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FMD_EXCHANGE_MAILBOXES` | comma-separated list | empty   | Least-privilege allowlist of the only mailboxes the live Exchange connector may read. Pair with an Exchange Online application access policy so the `Mail.Read` grant is tenant-enforced, not just app-enforced. |

### Control plane entitlement (licensing)

| Variable                     | Type                                    | Default | Effect                                                                                                                                                                                                                                |
| ---------------------------- | --------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FMD_ENTITLEMENT_TOKEN`      | string (`<payload>.<sig>` signed token) | —       | Signed entitlement issued by Find My Data Control, carrying plan (`trial`/`standard`/`enterprise`), limits (max domain owners / connected sources / assets), expiry, and an offline grace window. Verified locally — no network call. |
| `FMD_ENTITLEMENT_SECRET_REF` | string (ref)                            | —       | Name of the env var holding the verification secret. This build verifies with HMAC-SHA256 (shared secret); production intent is an asymmetric public key from Control — same interface.                                               |

Without both variables the deployment runs as `unlicensed`. `GET /api/controlplane/entitlement` reports state (`active` / `grace` / `expired` / `invalid` / `unlicensed`), plan, expiry, and usage vs. limits. Past `expiresAt` but inside `offlineGraceDays`, the state is `grace` and the product keeps running.

### Feature flags

All are booleans, default `false`, enabled only by the literal string `true`. See the [feature-gate matrix](#feature-gate-matrix) for what each flag needs beyond itself.

| Variable                           | Gates                                                                                                                                                                                                                     |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FMD_FEATURE_PURVIEW_LABEL_READ`   | Live sensitivity-label **observation** on the Graph connector (protected `extractSensitivityLabels`). Off: the connector reports label state `unknown` — honestly, never guessing.                                        |
| `FMD_FEATURE_PURVIEW_LABEL_WRITE`  | Live sensitivity-label **assignment** (protected, metered `assignSensitivityLabel`). Also enforced at action execution time: a live-mode label action is refused when the flag is off.                                    |
| `FMD_FEATURE_ENTRA_DIRECTORY_SYNC` | **Reserved.** Declared in the config schema but not consumed anywhere else in this build — the live directory-sync path (`User.Read.All` ownership intelligence) is not wired. Fixture principals are the exercised path. |
| `FMD_FEATURE_CHANGE_NOTIFICATIONS` | Live Graph change-notification subscriptions (creation and renewal).                                                                                                                                                      |
| `FMD_FEATURE_TEAMS_CONNECTOR`      | Live Teams connector (per-channel message reads, gated on the protected `ChannelMessage.Read.All` permission).                                                                                                            |
| `FMD_FEATURE_EXCHANGE_CONNECTOR`   | Live Exchange connector (selected-mailbox message delta).                                                                                                                                                                 |

## Production boot lockouts

`buildConfig` in [`config.ts`](../packages/server/src/kernel/config.ts) enforces two startup invariants when `FMD_ENV=production`. Both throw `ConfigError` before the server binds a port — the process refuses to boot (behavior locked by `kernel/config.test.ts`):

1. **Dev auth is refused.** `FMD_AUTH_MODE=dev` fails with:
   `FATAL: FMD_AUTH_MODE=dev is not permitted when FMD_ENV=production. Configure FMD_AUTH_MODE=entra with a valid Entra application.`
2. **The default session secret is refused.** `FMD_SESSION_SECRET=dev-only-change-me` (or unset, which yields the default) fails with:
   `FATAL: default FMD_SESSION_SECRET is not permitted when FMD_ENV=production.`

Independently of boot, dev-session cookies are rejected at request level in production even if present. The documented seam contract ([ADR-0004](adr/ADR-0004-authentication-dev-mode-and-entra.md), [permissions manifest §1.2](permissions-manifest.md)) requires external identities to map to internal principals by `(issuer, subject)` — never by email or display name.

## Worker topology: embedded vs. standalone

The scan pipeline runs on a durable SQLite-backed job queue ([ADR-0002](adr/ADR-0002-modular-monolith-and-worker.md), [ADR-0007](adr/ADR-0007-scan-pipeline-and-stage-caching.md)). Two ways to run workers, same code path (`startWorkerLoop`):

* **Embedded (default).** `bun run dev` / the API process (`packages/server/src/main.ts`) starts a worker loop in-process (worker ID `embedded-<pid>`). The embedded worker also owns the **periodic sweep**: renewing due change-notification subscriptions, stamping reconcile freshness, and expiring unrenewable subscriptions.
* **Standalone (optional).** `bun run worker` runs `packages/server/src/worker-main.ts` as a separate process (worker ID `worker-<pid>`) against the same SQLite database and queue, with graceful `SIGINT`/`SIGTERM` shutdown. Use it to scale scanning independently of the API; multiple workers can pull from the shared queue. Note the standalone worker does **not** run the subscription sweep — that stays with the embedded worker in the API process.

## Readiness and honest integration modes

`GET /api/ready` ([`packages/server/src/routes/health.ts`](../packages/server/src/routes/health.ts)) returns `status` (`ready`/`degraded`, HTTP 503 when the database check fails), `env`, and one honest mode per integration:

* `mock` — deterministic fixture path (exercised by tests).
* `live` — the feature is enabled **and** its credentials actually resolve (the `*_REF` points at a present value), not merely configured.
* `blocked` — configured or enabled but a prerequisite is unmet. Never silently faked.
* `disabled` — not enabled.

When `FMD_CONNECTOR_MODE=mock`, the `changeNotifications`, `teamsConnector`, and `exchangeConnector` capabilities report `mock` regardless of their flags (the dev simulator exercises those paths).

One caveat, stated plainly in the [permissions manifest §5](permissions-manifest.md): readiness computes from configuration, so setting `FMD_FEATURE_PURVIEW_LABEL_WRITE=true` with resolving remediation credentials shows `purviewLabelWrite: live` even though execution still fails at validation time without Microsoft's protected-API enablement (the action transitions to a blocked state with the reason). Do not set feature flags ahead of their prerequisites.

## Feature-gate matrix

Extra prerequisites are what the capability needs **beyond** `FMD_CONNECTOR_MODE=graph` with resolving read/scan Graph credentials (`FMD_GRAPH_TENANT_ID`, `FMD_GRAPH_CLIENT_ID`, `FMD_GRAPH_CLIENT_SECRET_REF` → resolving). Build-state legend: **live-validated** = exercised against a real tenant this run; **mock-validated, live gated** = full code path tested against fixtures, live path behind the gate and unvalidated; **not built** = no consumer.

| Flag                               | `/api/ready` key      | Capability                                                                                                                      | Extra prerequisites for live                                                                                                                                                                                               | Build state                                                                                            |
| ---------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| — (connector itself)               | `connector`           | Graph read/scan: scope discovery, `driveItem` delta, metadata, permissions, bounded content                                     | Admin consent for read permissions (`Sites.Selected` per-site grants recommended; CDX pilot used `Sites.Read.All` + `Files.Read.All`)                                                                                      | **Live-validated read-only** against the CDX tenant ([ADR-0013](adr/ADR-0013-live-graph-connector.md)) |
| `FMD_FEATURE_PURVIEW_LABEL_READ`   | `purviewLabelRead`    | Read assigned sensitivity labels via `extractSensitivityLabels`                                                                 | Protected-API access (`InformationProtectionContent.Read.All`)                                                                                                                                                             | Mock-validated, live gated; off = label state `unknown`                                                |
| `FMD_FEATURE_PURVIEW_LABEL_WRITE`  | `purviewLabelWrite`   | Assign/remove Purview labels (`assignSensitivityLabel`) through the draft → validate → approve → execute → verify state machine | Remediation identity (`FMD_GRAPH_REMEDIATION_CLIENT_ID` + `..._SECRET_REF` resolving), Microsoft protected/metered API enablement, write permission, synchronized label catalog ([manifest §2.4](permissions-manifest.md)) | State machine mock-validated (tested); live write gated                                                |
| `FMD_FEATURE_CHANGE_NOTIFICATIONS` | `changeNotifications` | Graph `/subscriptions` wake-up hints (delta stays authoritative)                                                                | `FMD_WEBHOOK_PUBLIC_URL` — a public HTTPS endpoint Graph can validate and reach                                                                                                                                            | Receiver + lifecycle + reconcile mock-validated via dev simulator; live gated                          |
| `FMD_FEATURE_TEAMS_CONNECTOR`      | `teamsConnector`      | Teams channels/messages (files canonicalize to SharePoint `driveItem`s)                                                         | `ChannelMessage.Read.All` consent — a protected/metered Graph capability needing Microsoft approval + licensing                                                                                                            | Mock-validated, live gated                                                                             |
| `FMD_FEATURE_EXCHANGE_CONNECTOR`   | `exchangeConnector`   | Selected-mailbox message delta + attachments                                                                                    | `Mail.Read` consent scoped by an Exchange application access policy; `FMD_EXCHANGE_MAILBOXES` allowlist                                                                                                                    | Mock-validated, live gated                                                                             |
| `FMD_FEATURE_ENTRA_DIRECTORY_SYNC` | —                     | Directory-property sync for ownership intelligence                                                                              | n/a                                                                                                                                                                                                                        | **Not built** — flag reserved, no consumer in this build                                               |

Full permission-by-permission detail, least-privilege alternatives, and protected-API caveats: [permissions manifest](permissions-manifest.md).

## Azure deployment

Single-tenant, customer-hosted deployment as Bicep — see [infra/README.md](../infra/README.md) for the full flow and rationale; this is the summary. The template provisions a user-assigned **managed identity**, **Key Vault** (RBAC), a **storage account + file share** for the SQLite database and artifacts, a **Log Analytics** workspace, and an **Azure Container App** running the API with the embedded worker, ingress on port 8710.

```bash theme={null}
# 1. Validate the template
az bicep build --file infra/main.bicep

# 2. Fill in infra/main.bicepparam (non-secret identifiers only)

# 3. Deploy
az deployment group create -g <rg> -f infra/main.bicep -p infra/main.bicepparam

# 4. Set secret values out-of-band (never committed; Key Vault names:
#    fmd-session-secret, fmd-graph-client-secret)

# 5. Restart the Container App to pick them up
```

Key properties of the deployment:

* **No secrets in source control.** Key Vault secrets are placeholders set out-of-band; the app reads them through Key-Vault-backed Container App secrets resolved by managed identity. The `*_REF` indirection is preserved: the template sets `FMD_GRAPH_CLIENT_SECRET_REF=GRAPH_CLIENT_SECRET` and binds `GRAPH_CLIENT_SECRET` to the Key Vault secret.
* **Production lockout active.** The template sets `FMD_ENV=production` and `FMD_AUTH_MODE=entra`; the app refuses to boot with dev auth or the default session secret.
* **Honest scale posture.** This first build is SQLite on an Azure File share with the worker embedded in the API replica. The documented scale path (managed PostgreSQL, a separate worker Container App, private endpoints, Azure OpenAI) is deliberately **not provisioned** to avoid implying it is done — see [infra/README.md](../infra/README.md) and [ADR-0003](adr/ADR-0003-persistence-single-sqlite-behind-role-interfaces.md).

After deploying, follow the post-deploy checklist in [infra/README.md](../infra/README.md): grant the Entra app the read scopes from the [permissions manifest](permissions-manifest.md), then confirm `GET /api/ready` reports the integration modes you expect — nothing claims to be live until its credentials resolve.

## Related documentation

* [STATUS.md](../STATUS.md) — implemented / mocked / gated honesty ledger
* [Architecture](architecture.md) and [ADRs](adr/)
* [Permissions manifest](permissions-manifest.md) — Microsoft Graph permissions and protected-API notes
* [CDX test runbook](cdx-test-runbook.md) — live-tenant validation record and procedure
* [Threat model](threat-model.md) · [Capacity model](capacity-model.md) · [Roadmap](roadmap.md)
