Skip to main content

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

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.

AI provider

Webhook public URL

Exchange mailboxes

Control plane entitlement (licensing)

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 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 SQLite-backed job queue (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 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) 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: 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. 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. 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 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.