Release notes
This document is for everyone tracking Find My Data releases — operators upgrading a deployment, engineers looking for what changed and why, and evaluators who need an honest statement of what is validated live versus built-but-gated. It defines the versioning policy and records each release, newest first. The current state of every capability (including what is mocked or feature-gated) is always STATUS.md; this file records what changed per release.Part 1 — Versioning policy
Releases are versionedyear.month.day.x:
- year — two digits (
26for 2026) - month, day — no leading zeros (
7, not07) - x — same-day release sequence, starting at
0
The version string lives in two places, kept in sync:
PRODUCT_VERSIONinpackages/server/src/controlplane/routes.ts- the
VERSIONfile at the repository root
productVersion in the control-plane fleet-health telemetry payload (inspectable at GET /api/controlplane/telemetry-preview; see ADR-0014, decision 5).
The version fields in package.json files (currently 0.1.0 in the root and in packages/server, packages/shared, packages/web) are internal workspace versions and are not the release version.
Part 2 — Release 26.7.16.1 (2026-07-16)
A same-day increment over 26.7.16.0 that adds the member-management UI to the Account portal, so the routes shipped in 26.7.16.0 are usable from the browser. What’s new- Members panel in the portal org view (
account/ui.ts): a “Team members” table (each row shows email + name + role, the signed-in user marked “you”), a “Pending invitations” table with a Revoke control, and an invite-a-teammate form (email + owner/member). Owner-only controls (Make owner/member, Remove, Revoke) render only for owners; the last owner shows a non-actionable “last owner” label — mirroring the server’s invariant. All actions call the 26.7.16.0 routes with CSRF and refresh in place.
Part 2b — Release 26.7.16.0 (2026-07-16)
The first release on 2026-07-16. It adds org member management to the Account portal and records the commercial services going fully live (the day’s infra work carries no product-code change of its own). What’s new- Org member management (
account/store.ts+service.ts+app.ts, ADR-0024): an org owner can invite teammates by email and manage roles, lifting the single-owner limitation from ADR-0017. Two roles (owner|member); the last owner can never be removed or demoted (an org is never orphaned). Invitations are an email allowlist (org_invites) that auto-accepts on sign-in — composing with Clerk JIT (verified email) and dev auth; an invite only ever adds to the inviter’s own org. Every route is membership-guarded (non-member → 404), mutations are owner-only + CSRF with the target bound to the same account (no cross-org IDOR), and each records a PII-light control event.
- Account portal deployed to Fly.io at https://account.findmydata.io
(+
findmydata-account.fly.dev), production + Clerkidpmode, TLS issued. - Stripe billing enabled — secrets set on Fly + a signed webhook at
/account/v1/billing/webhook(verified live: an unsigned/bad-sig POST → 400). - Plain support mirror enabled — the machine-user key is set as a Fly secret.
- Docs live at https://docs.findmydata.io (Mintlify, auto-deploys from
docs/). - Clerk production instance on
findmydata.io— DNS records added; when its Frontend API finishes provisioning, the portal flips from the Clerk dev instance (deploy/fly/account.fly.tomlalready carries the production issuer + key).
org_invites table
is created idempotently. No config changes required for existing single-owner orgs.
Part 2c — Release 26.7.15.10 (2026-07-15)
A same-day increment over 26.7.15.9 that wires the Account portal’s support cases to Plain (plain.com) and verifies the path live. What’s new- Plain support mirror (
account/support-plain.ts): when a support case is opened in the portal, it also opens a Plain thread so the team works it in Plain. The portal’s own case store stays the source of truth for the customer; Plain is the agent-side workflow. Feature-gated: off unlessFMD_PLAIN_API_KEY_REFresolves. See ADR-0023. - Off the response path: the case commits and returns 201 first; the mirror
runs as a guarded background task, so a slow/hung/unreachable/erroring Plain (or a
transient DB error while recording the outcome) can never delay or fail case
creation. Each outcome records an event (
support_case_mirrored_plain/support_case_mirror_failed). - Content boundary: only case metadata crosses (opener email/name, subject, severity, portal case id) — never customer content, and not the org display name (only the opaque account id). The thread body is a fixed content-free summary.
- Live verification created a real customer + thread in the Find My Data Plain
workspace. This caught and fixed a real endpoint bug — the previously hardcoded
core-api.plain.comhost does not resolve; the correct global endpoint iscore-api.uk.plain.com(overridable viaFMD_PLAIN_API_URL). The Plain machine-user key is scoped to least privilege (customer:create,customer:edit,thread:create).
TypeError mislabeled as unreachable (optional-
chained to surface the real error), the response-path latency (moved off-path), and
non-2xx status classification. Endpoint idempotency (duplicate cases/threads on
manual double-submit) is a pre-existing portal property, noted as a follow-up.
Verification: full server suite (320 tests) passing — 9 Plain tests including a
“case creation not blocked by a hung Plain” regression, the null-field regression,
and a content-boundary assertion — plus a live end-to-end thread creation.
Typecheck + lint clean.
Upgrade notes (26.7.15.9 → .10): no migration. Operator sets
FMD_PLAIN_API_KEY_REF (a Fly/Key Vault secret) to enable the mirror; leaving it
unset keeps cases local-only. See docs/deploy-fly.md.
Part 2d — Release 26.7.15.9 (2026-07-15)
A same-day increment over 26.7.15.8 that makes billing per-seat and records the live Stripe pricing. What’s new- Per-seat (per-user) pricing: checkout takes a seat count → the Stripe
Checkout line-item quantity, with adjustable-quantity on so the buyer can tune
it; the licensed seat count is stored on the account (
cp_accounts.stripe_seats). The webhook extracts and records it on upgrade. - Stripe Products created (test mode) with pricing benchmarked to comparable
DSPM/data-governance tools: Standard 29/user/mo
(all connectors + priority support). Price ids wired in
.env.example; rationale- go-live steps in docs/billing-pricing.md.
stripe_seats column is
added idempotently). Operator sets FMD_STRIPE_* and creates the webhook per the
billing doc.
Part 2e — Release 26.7.15.8 (2026-07-15)
A same-day increment over 26.7.15.7 that adds a fail-closed demo mode and the demo + dev environment runbook. What’s new- Demo mode (
FMD_DEMO_MODE=true,kernel/config.ts): the deployment refuses to boot unless it’s safe as a public demo — the connector must bemock(synthetic data, never a live tenant), and live label writes + directory sync are forbidden.demoModeis surfaced atGET /api/ready; the web UI shows a persistent “Demo environment — synthetic data only” banner. See ADR-0022. - Runbook (docs/demo-and-dev-environments.md):
hosting the disposable demo (Azure Container Apps + scheduled reset + Turnstile)
and protecting
dev.findmydata.iowith Cloudflare Access + Tunnel (free ≤ 50 users, no open ports) — with a cost summary.
/api/ready.
Upgrade notes (26.7.15.7 → .8): no migration. FMD_DEMO_MODE is off by
default; setting it enforces the synthetic-only guarantees above.
Part 2f — Release 26.7.15.7 (2026-07-15)
A same-day increment over 26.7.15.6 that adds Stripe billing / paid conversion — an evaluation converts to a paid subscription without reinstalling. What’s new- Stripe Checkout (
account/billing-stripe.ts+ routes): an org owner starts an upgrade → the portal creates a subscription Checkout Session → Stripe-hosted payment (PCI stays with Stripe). The upgrade UI shows on the org card. See ADR-0021. - Signed webhook = source of truth: the entitlement change happens only on a
verified
checkout.session.completed— HMAC-SHA256 over the raw body, constant- time compare, 300s replay tolerance. It callsupdateAccountPlanon the shared registry, so the deployment picks up the paid edition on next renewal.customer.subscription.deletedreverts to evaluation; re-deliveries are idempotent. - Authorized + gated: checkout is member-guarded + CSRF (price resolved server-side from the edition); billing is feature-gated (needs both secrets + a price map); the Stripe secret key stays in the environment.
…/account/v1/billing/webhook, then set FMD_STRIPE_SECRET_KEY_REF,
FMD_STRIPE_WEBHOOK_SECRET_REF, and FMD_STRIPE_PRICE_STANDARD /
FMD_STRIPE_PRICE_ENTERPRISE.
Part 2g — Release 26.7.15.6 (2026-07-15)
A same-day increment over 26.7.15.5 that implements production identity for the Account portal with Clerk — the last piece for a production-grade portal. What’s new- Clerk IdP (
account/idp-clerk.ts): the portal verifies a Clerk session token (RS256 JWT) locally against Clerk’s public JWKS — no Clerk secret key. Pins RS256 (rejectsalg:none/HS256), checks the signature before any claim, and validates issuer / expiry / not-before / authorized-party / a verifiable email. See ADR-0020. POST /account/v1/session/idp: exchanges a verified Clerk token for the portal’s own opaque session, JIT-provisioning the user by email. The rest of the portal (CSRF, membership guards) is unchanged and IdP-agnostic.- Mode guards:
idpmode requiresFMD_CLERK_ISSUER+FMD_CLERK_PUBLISHABLE_KEY(both public); dev self-service routes return 501 in idp mode; production still refuses dev auth. The UI loads Clerk only in idp mode.
"email": "{{user.primary_email_address}}" to the session-token customization,
then set FMD_ACCOUNT_AUTH_MODE=idp, FMD_CLERK_ISSUER, and
FMD_CLERK_PUBLISHABLE_KEY. No customer-side migration.
Part 2h — Release 26.7.15.5 (2026-07-15)
A same-day increment over 26.7.15.4 that builds the support portal + consent- based diagnostic bundles — completing the customer lifecycle (…activate → support → convert). What’s new- Diagnostic bundle (deployment side,
support/diagnostics.ts): a strict- allowlisted, redacted operational bundle — versions, non-secret config/feature flags, migration status, health bands, and error categories (RETRY_CLASSES) with banded counts. No content, names, paths, identities, secrets, or error messages; pseudonymous deployment id. Same.strict()+ denylist discipline as fleet telemetry. See ADR-0019. - Preview-before-send:
GET /api/support/diagnostics/previewreturns the exact bundle a support upload would contain — generated locally, nothing sent. - Support cases (Account portal): create/list cases, and explicitly upload
a previewed bundle to a case. The vendor re-validates the bundle against the
same strict schema on ingest, so customer content can never be stored;
bundles are retention-limited (
FMD_ACCOUNT_SUPPORT_RETENTION_DAYS, default 30) and reads are access-logged. Membership-guarded (not entitlement-gated — support works for expired accounts).
/api/support/diagnostics/preview; the portal’s support routes
are additive.
Part 2i — Release 26.7.15.4 (2026-07-15)
A same-day increment over 26.7.15.3 that builds entitled downloads + software supply chain — the download step of the customer journey. What’s new- Release catalog in the vendor registry (
cp_releases: version, channel, sha256, size, min-edition, yank), managed by Bearer-admin Control endpoints (/control/v1/admin/releases[...]). See ADR-0018. - Entitled downloads in the Account portal: a licensed org lists its
edition-permitted releases and mints a short-lived HMAC-signed URL — issued
only after membership + account-active + edition checks. The download endpoint
is token-authenticated (no session), re-checks yank/expiry so a yank takes
effect immediately, is path-traversal safe (basename only), and returns
the
x-checksum-sha256. Feature-gated onFMD_DOWNLOADS_DIR; production requires an explicit URL-signing secret. - Supply chain: the release workflow now attaches a CycloneDX SBOM
(
scripts/sbom.ts, full install tree) and aSHA256SUMSover every artifact.
account/downloads.test.ts
- Control admin release lifecycle), typecheck + lint clean; SBOM generator produces valid CycloneDX; adversarially reviewed (multi-lens + per-finding verification).
FMD_DOWNLOADS_* is configured on the Account portal.
Part 2j — Release 26.7.15.3 (2026-07-15)
A same-day increment over 26.7.15.2 that builds the Account portal (account.findmydata.io) — the human commercial front door on top of the Control
service. Run with bun run --cwd packages/server account.
What’s new
- Account portal (
packages/server/src/account/, entrypointaccount-main.ts): self-service registration → 30-day evaluation → a one-time activation code, plus authenticated views of your organizations, deployments + live license status, activation codes, and control events. Self-contained brand-consistent UI at/account. See ADR-0017. - Human identity, distinct from Control: opaque server-side sessions, CSRF on
state changes, membership-guarded org views (404 for non-members). Dev auth is
email-only; production requires an external IdP (
FMD_ACCOUNT_AUTH_MODE=idp), which disables dev-login/register until wired. - One registry, two views: the portal shares the vendor database with the
Control service — registration/evaluation reuse
createAccount+createActivationCode. Interop is tested: a portal-minted code activates on Control and the deployment appears in the portal. - Content boundary unchanged: the portal knows emails + org names only.
account/account.test.ts
incl. the portal↔Control interop), typecheck + lint clean; the register →
evaluation → dashboard flow verified in a browser.
Upgrade notes (26.7.15.2 → .3): no customer-side migration. The portal is
optional and separate (shares the vendor DB); new FMD_ACCOUNT_* env vars apply
only to it.
Part 2k — Release 26.7.15.2 (2026-07-15)
A same-day increment over 26.7.15.1 that builds the vendor control-plane service (control.findmydata.io) — closing the loop that ADR-0015 left as
“deployment-side only”. This is a separate deployable (bun run --cwd packages/server control) that holds the entitlement signing key and the
pseudonymous account registry; the customer product never runs it.
What’s new
- Vendor Control service (
packages/server/src/vendor/, entrypointcontrol-main.ts): its own isolated database, its own port, machine endpoints foractivate,renew,telemetry, and deploymentstatus, plus admin-gated (Bearer) account and activation-code management. See ADR-0016. - Signed activation + renewal: an activation code is exchanged for a
freshly-signed entitlement bound to a pseudonymous deployment and the account’s
plan (edition/features/capacity/term). Renewal re-issues with an extended term,
supersedesthe prior token, and revokes it (replay resistance). - Live online path on the deployment:
POST /api/controlplane/activate-online(activation code) and online renewal againstFMD_CONTROL_PLANE_URL. A token returned by Control is still verified locally against the deployment’s own public key and re-bound to the tenant before it is trusted — a spoofed Control cannot inject a license (regression-tested). Online renewal falls back to a local re-verify if Control is unreachable. - Content boundary preserved: telemetry ingest re-validates the same
.strict()fleet-health schema; production config guards refuse HMAC/ephemeral keys and a missing admin credential.
vendor/vendor.test.ts),
typecheck + lint clean; the service boots and serves a full activation flow.
Upgrade notes (26.7.15.1 → .2): no customer-side migration. The vendor
service is optional and separate; new FMD_CONTROL_* env vars apply only to it.
Deployments gain the optional online activation/renewal path when
FMD_CONTROL_PLANE_URL is set (offline paste activation is unchanged).
Part 2l — Release 26.7.15.1 (2026-07-15)
A same-day increment over 26.7.15.0 that applies handoff update-001 (commercial web, account, licensing, support, demo, Cloudflare). It builds the deployment-side licensing on the existing control plane; the vendor-hosted Account/Control/Support/Demo services and live Cloudflare changes are captured as design + roadmap. Verification: 204 tests passing, typecheck/lint/build clean.- Versioned, signed entitlement (built).
entitlementVersion 2carrying edition, feature flags, capacity meters, term, connector families, offline/renewal policy, issuer, and a rotating key id. Verified locally by Ed25519 (production — the deployment holds only the vendor public key) or HMAC (dev), selected by the token header. - Activation / renewal / deactivation (built). Offline paste-a-token activation is the auditable no-outbound path; renewal re-verifies within a grace window; every event is logged (
licensing_events, migration 0013) and audited. Live outbound activation tocontrol.findmydata.iois gated onFMD_CONTROL_PLANE_URL. - Non-destructive license enforcement (built). Starting a new scan requires an active license with the
scanningfeature (LicenseError, 402). Unlicensed is permissive; expired/over-capacity never restricts reading data, audit, or export. - Fleet-health telemetry (extended). Adds edition + the vendor deployment pseudonym while staying strict-allowlisted (no content, names, or raw asset counts).
- Marketing landing page (built, minimal). One self-contained static page (
site/index.html), brand-consistent, honest (customer-hosted, not a consumer locator). - Cloudflare domain runbook (docs).
docs/cloudflare-runbook.md, inspect-first + reversible; no live DNS/Access changes made. - Decisions. ADR-0015 records the commercial-web + human/machine-plane separation and the content-boundary invariant.
FMD_ENTITLEMENT_KEY_ID, FMD_ENTITLEMENT_PUBLIC_KEY_REF, FMD_CONTROL_PLANE_URL); with none configured the product remains unlicensed-permissive (unchanged behavior).
Part 2m — Release 26.7.15.0 (2026-07-15)
The first formally versioned release. It consolidates everything built to date: the full vertical slice, live Microsoft 365 (CDX) validation, enterprise operations, eight roadmap features, and two adversarial-review hardening passes. Runtime is Bun 1.3.14 (no Node.js). Verification at release: 196 tests passing, typecheck/lint/build clean. Honesty legend used below — this product never overstates its state:- Live-validated — exercised against the live Contoso CDX tenant (
m365x93338811), read-only. - Built, mock-validated — fully implemented and tested against the deterministic mock tenant/fixtures; the live path is feature-gated and reports
blocked/disabledhonestly until configured. - Not built — explicitly absent (listed in STATUS.md and the roadmap).
Platform foundation (vertical slice)
The complete product loop, runnable locally with zero credentials: onboard → AI-drafted taxonomy with human approval → scoped owner assignment → durable inventory/delta scans → explainable classification with evidence → owner review → scoped Analyst → approval-gated remediation → hash-chained audit.
State: built, mock-validated by design — the mock tenant is the default and CI path (ADR-0012). Demo org is the fictional Meridian Grove Holdings; all fixture people and documents are synthetic.
Live Microsoft 365 validation
What is live-validated (read-only, CDX tenant, app registration
FMD-CDX-Reader): client-credentials auth, 34 drives discovered, 174 real assets via delta, deduplicated permission sets, real-content classification, and re-score-without-refetch proven live (1.4s vs 151s). After the extractor registry landed, content extraction rose from 62/174 to 172/174 files (only 2 genuine binaries unsupported). Live data also surfaced a real over-classification bug — generic evidence no longer creates type-specific candidates — fixed in the live-connector commit (b54445b).
What is built, mock-validated, live path gated: Purview label read (FMD_FEATURE_PURVIEW_LABEL_READ) and write (FMD_FEATURE_PURVIEW_LABEL_WRITE, separate remediation identity, refuses to run unconfigured). Neither has been exercised live; the write path additionally requires protected/metered assignSensitivityLabel API enablement (see the permissions manifest). Labels observed on CDX were honestly reported unknown because the reader app lacks label permissions.
Not built: Graph write path for the main connector (read-only app), Entra ID sign-in (seam exists; OIDC flow returns 501).
Enterprise operations
State: fleet, metrics, and admin UI are built and tested (fleet runs in-process or standalone via
bun run worker). The connector probe is live-validated — it acquired an app-only token and read a CDX site. Container/K8s packaging and signed work envelopes remain roadmap items.
Near-real-time: change notifications
A notification is treated as a hint, never trusted content (ADR-0014, decision 1). State: built, mock-validated; a dev simulator drives the full receive→reconcile path, and a simulated notification was validated against live CDX — it triggered a real read-only delta reconcile with 4.9s measured lag. Live Graph subscriptions require a public HTTPS webhook URL (
FMD_WEBHOOK_PUBLIC_URL) plus FMD_FEATURE_CHANGE_NOTIFICATIONS; without them the path reports blocked/disabled.
Governed learning
State: live-validated — the loop ran end-to-end on live CDX data: real HR candidates, SoD blocked self-approval, promotion rescored 174 assets. Nothing material auto-applies. Evaluation currently sweeps the ensemble decision threshold; detector-definition releases would add a feature recompute (documented follow-on).
Semantic intelligence
State: live-validated with the deterministic feature-hash default provider — 172 embeddings indexed on CDX; queries return owner-scoped related HR documents. The Azure OpenAI embeddings adapter is implemented but config-gated and not exercised; the Anthropic adapter honestly reports no first-party embeddings. Per-model namespaces keep mock and provider vectors from ever mixing.
New modalities: Teams and Exchange
State: both built, mock-validated, live path feature-gated. The Teams mock proves canonicalization creates zero duplicate assets; the Exchange mock proves attachment similarity (merit-letter attachment →
identical_text 0.95, zero assets created). Live paths require FMD_FEATURE_TEAMS_CONNECTOR + ChannelMessage.Read.All (protected API) and FMD_FEATURE_EXCHANGE_CONNECTOR + Mail.Read on selected mailboxes (FMD_EXCHANGE_MAILBOXES) respectively — neither has been run live. Message bodies are reduced to bounded, identifier-redacted previews; only detector signals are retained (ADR-0014, decision 2).
Deployment & commercial
State: built, not live-exercised. The Bicep template has not been compiled with
az bicep build (no Azure CLI in the build environment), and no actual Control endpoint exists to receive telemetry (deliberately out of scope; nothing leaves the deployment). The honest scale path — Postgres, separate worker app, private endpoints — is documented, not implied as done.
Hardening
After the second pass,
/api/ready distinguishes live / blocked / disabled by whether a secret reference actually resolves — no capability claims to be live until its credentials are present.
Known limitations
See STATUS.md — specifically its “Mocked / feature-gated (honest list)” table and “Known limitations” section — for the authoritative, current list (single-writer SQLite scale triggers, in-memory mock-tenant state, Teams/Exchange domain-ownership mapping, read-only finding transitions in the UI, and others).Upgrade notes
- Database: all 12 migrations (
packages/server/migrations/0001…0012) apply automatically and transactionally on boot — both the API process (main.ts) and the standalone worker (worker-main.ts) runmigrate()before serving; applied migrations are tracked inschema_migrationsand never re-run. No manual migration step. - Environment variables: every variable introduced since the vertical slice is optional. Feature flags (
FMD_FEATURE_PURVIEW_LABEL_READ,FMD_FEATURE_PURVIEW_LABEL_WRITE,FMD_FEATURE_ENTRA_DIRECTORY_SYNC,FMD_FEATURE_CHANGE_NOTIFICATIONS,FMD_FEATURE_TEAMS_CONNECTOR,FMD_FEATURE_EXCHANGE_CONNECTOR) default to off;FMD_WEBHOOK_PUBLIC_URL,FMD_EXCHANGE_MAILBOXES,FMD_ENTITLEMENT_TOKEN,FMD_ENTITLEMENT_SECRET_REF, and theFMD_GRAPH_REMEDIATION_*identifiers are unset by default. Unconfigured capabilities reportblocked/disabledhonestly rather than failing. Full reference: installation guide, “Configuration reference”. - Runtime: Bun 1.3.14. No Node.js is required or used (ADR-0001).