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:
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)
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_ENVis notdevelopment. - 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.
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 inpackages/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:
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 isblocked.
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, defaultfalse, 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):
- Dev auth is refused.
FMD_AUTH_MODE=devfails with:FATAL: FMD_AUTH_MODE=dev is not permitted when FMD_ENV=production. Configure FMD_AUTH_MODE=entra with a valid Entra application. - 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.
(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 usesFOR 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 IDembedded-<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 workerrunspackages/server/src/worker-main.tsas a separate process (worker IDworker-<pid>) against the same database and queue, with gracefulSIGINT/SIGTERMshutdown. 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*_REFpoints at a present value), not merely configured.blocked— configured or enabled but a prerequisite is unmet. Never silently faked.disabled— not enabled.
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 beyondFMD_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.- 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
*_REFindirection is preserved: the template setsFMD_GRAPH_CLIENT_SECRET_REF=GRAPH_CLIENT_SECRETand bindsGRAPH_CLIENT_SECRETto the Key Vault secret. - Production lockout active. The template sets
FMD_ENV=productionandFMD_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.
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 theSites.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
- 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 matchFMD_ENTRA_REDIRECT_URIexactly. Add a client secret and store its value in your secret store, not in source. - Delegated permissions. Add the delegated Microsoft Graph scopes
openid,profile,emailand grant admin consent. No application permissions are needed for sign-in (those belong to the separate Graph read/scan app). - 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), andFMD_ENTRA_REDIRECT_URI. For a national cloud also setFMD_ENTRA_LOGIN_HOST. EnsureFMD_SESSION_SECRETis a real non-default value. - 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) asexternal_idbefore they sign in (or setFMD_ENTRA_AUTO_PROVISION=trueto JIT-create a role-less principal, then grant capability). An unrecognized, non-provisioned user is refused withidentity_not_provisioned. - Confirm it is live.
GET /api/readyshould reportintegrations.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 logsentra_login_failedwith a correlation id) — never a silent dev fallback.
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)
- Consent
Sites.Selected. On the Graph read/scan app registration, add the application permissionSites.Selectedand grant admin consent. By itself it grants access to nothing. - Grant the specific sites. For each site to scan, grant the app
read(orwritefor remediation) viaPOST /sites/{id}/permissionsor admin tooling. Record each site’s identifier. - List them. Set
FMD_SP_SITESto 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). - 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 fromFMD_SP_SITESblocks its persisted drive at the next scan and drains it on re-register. LeavingFMD_SP_SITESempty keeps the broaderSites.Read.Allpilot 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.
Related documentation
- STATUS.md — implemented / mocked / gated honesty ledger
- Architecture and ADRs
- Permissions manifest — Microsoft Graph permissions and protected-API notes
- Threat model · Capacity model · Roadmap