Skip to main content
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.20.0 (scheme year.month.day.x; see VERSION at the repo root). For what is implemented vs. mocked vs. gated, STATUS.md is the authoritative honesty ledger.

Prerequisites

  • Bun ≥ 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.

Local installation

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 and sign in with the development persona picker (see What the seed creates). Verify the API directly: Other root scripts (from the root package.json): The dev database and artifacts live under packages/server/data/ (gitignored). To reset:

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: 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 (Zod schema; invalid values fail boot). Copy .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:
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

Object store

The purpose-flagged object/evidence artifact store is pluggable. local (the default) writes under FMD_ARTIFACT_DIR; s3 targets your own S3/R2/MinIO bucket; azure-blob targets an Azure Blob container. The purpose gate is preserved on every backend, and a mis-set backend fails boot rather than silently falling back to local disk (s3 without a bucket, or azure-blob without account + container, refuses to start).

Connector mode

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). All are strings with no default; unset means the corresponding live path is blocked.

Entra OIDC sign-in

The production user sign-in path (FMD_AUTH_MODE=entra, ADR-0035). All strings, no default; sign-in reports live at /api/ready only when the tenant, client id, a resolvable secret reference, and the redirect URI are all present. This is a separate app registration from the Graph read/scan and remediation apps — a confidential web app with a redirect URI, not an app-only daemon. Users are bound to internal principals by the immutable (issuer, subject) pair — never by email — so pre-provision each person’s principal with their directory object id (oid) as external_id; the first real sign-in claims that identity. See Operator validation for the end-to-end setup and test.

AI provider

Webhook public URL

Exchange mailboxes

Control plane entitlement (licensing)

Without both variables the deployment runs as unlicensed. Alternatively, activate online: set FMD_CONTROL_PLANE_URL and use POST /api/controlplane/activate-online with a one-time activation code from the account portal — the returned entitlement is verified locally and tenant-bound before trust, and online renewal falls back to a local re-verify on outage. 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 for what each flag needs beyond itself.

Production boot lockouts

buildConfig in 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, permissions manifest §1.2) 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 database-backed job queue (SQLite by default; the optional PostgreSQL backend uses FOR UPDATE SKIP LOCKED) (ADR-0002, ADR-0007). 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 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) 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, every connector-family capability (changeNotifications, teamsConnector, exchangeConnector, onedriveConnector, azureStorageConnector, smbConnector, sqlConnector, gdriveConnector, boxConnector) reports mock regardless of its flags (the dev fixtures exercise those paths). One caveat, stated plainly in the permissions manifest §5: 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 at this release; mock-validated, live gated = full code path tested against fixtures, live path behind the gate and unvalidated; not built = no consumer. Full permission-by-permission detail, least-privilege alternatives, and protected-API caveats: permissions manifest.

Azure deployment

Single-tenant, customer-hosted deployment as Bicep — see 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.
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. The template provisions the zero-dependency SQLite-on-file-share default with the worker embedded in the API replica. The application’s optional PostgreSQL 16 backend is built and load-validated (FMD_DB_BACKEND=postgres, 2M assets), but this template deliberately does not provision managed PostgreSQL, a separate worker Container App, private endpoints, or Azure OpenAI — configure those separately when you need them; see infra/README.md and ADR-0003.
After deploying, follow the post-deploy checklist in infra/README.md: grant the Entra app the read scopes from the permissions manifest, then confirm GET /api/ready reports the integration modes you expect — nothing claims to be live until its credentials resolve.

Operator validation (Entra OIDC + Sites.Selected)

Both of these paths are built and validated live on a real Microsoft 365 tenant: the Entra sign-in path end-to-end on 2026-07-19 (app registration → consent → real user sign-in → session), and the Sites.Selected least-privilege posture on 2026-07-20 (per-site grants, tenant-wide read permissions retired, verified with the app’s own token). Each deployment still performs these steps in its own tenant — an app registration, a redirect URI, admin consent, per-site grants, and a real user sign-in are things the software cannot provision for you. Nothing here changes code; it configures your tenant and your deployment’s environment.

Entra OIDC sign-in

  1. Register a sign-in app. In Entra ID → App registrations, create a new registration (confidential web app). Add a Web redirect URI equal to https://<your-host>/api/auth/entra/callback — it must match FMD_ENTRA_REDIRECT_URI exactly. Add a client secret and store its value in your secret store, not in source.
  2. Delegated permissions. Add the delegated Microsoft Graph scopes openid, profile, email and grant admin consent. No application permissions are needed for sign-in (those belong to the separate Graph read/scan app).
  3. Configure the deployment. Set FMD_AUTH_MODE=entra, FMD_ENTRA_TENANT_ID, FMD_ENTRA_CLIENT_ID, FMD_ENTRA_CLIENT_SECRET_REF (pointing at the env var that holds the secret), and FMD_ENTRA_REDIRECT_URI. For a national cloud also set FMD_ENTRA_LOGIN_HOST. Ensure FMD_SESSION_SECRET is a real non-default value.
  4. Pre-provision the first user. Because identity binds by (issuer, subject) and never by email, create the internal principal(s) with each person’s directory object id (oid) as external_id before they sign in (or set FMD_ENTRA_AUTO_PROVISION=true to JIT-create a role-less principal, then grant capability). An unrecognized, non-provisioned user is refused with identity_not_provisioned.
  5. Confirm it is live. GET /api/ready should report integrations.entraSignIn: "live". Visit /api/auth/entra/login — you should be redirected to your tenant’s consent/sign-in, and after signing in land back authenticated with a session cookie. A failed sign-in redirects to /?login_error=<code> (and logs entra_login_failed with a correlation id) — never a silent dev fallback.
Common failure codes on the callback: state_mismatch (cookie/redirect mismatch, usually a wrong redirect URI or a dropped cookie), bad_signature / unknown_signing_key (JWKS/tenant mismatch), identity_not_provisioned (step 4), not_configured (a required FMD_ENTRA_* var is missing/unresolved).

Sites.Selected (least-privilege SharePoint)

  1. Consent Sites.Selected. On the Graph read/scan app registration, add the application permission Sites.Selected and grant admin consent. By itself it grants access to nothing.
  2. Grant the specific sites. For each site to scan, grant the app read (or write for remediation) via POST /sites/{id}/permissions or admin tooling. Record each site’s identifier.
  3. List them. Set FMD_SP_SITES to the comma-separated site path identifiers, each {host}:/sites/{name} (not the composite {host},{coll}, {site} id — it contains commas and would be split apart).
  4. Confirm least privilege. Run discovery; only the listed sites resolve, and the tenant-wide search=* enumeration is never issued. A site whose grant hasn’t propagated (403/404) is skipped, not fatal. De-selection is enforced at scan time: removing a site from FMD_SP_SITES blocks its persisted drive at the next scan and drains it on re-register. Leaving FMD_SP_SITES empty keeps the broader Sites.Read.All pilot behavior; a value that reduces to no valid entries refuses to boot rather than silently scanning the whole tenant.

Gated live connectors (Graph read, Purview, Teams/Exchange, storage, SQL, Drive, Box)

The connector live paths are similarly built and mock-validated but gated on real credentials/consent. Follow ops/cdx-test-runbook.md for the Microsoft path (app registrations → Sites.Selected → real delta + label observation → gated label write), and the per-connector env vars above plus the feature-gate matrix for the rest. In every case GET /api/ready is the source of truth — a capability is live only when its credentials actually resolve.