Skip to main content

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 versioned year.month.day.x:
  • year — two digits (26 for 2026)
  • month, day — no leading zeros (7, not 07)
  • x — same-day release sequence, starting at 0
The version string lives in two places, kept in sync:
  • PRODUCT_VERSION in packages/server/src/controlplane/routes.ts
  • the VERSION file at the repository root
It is reported as 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.
Verification: full server suite (333 tests) passing; typecheck + lint clean. Verified live in a browser against a local dev-mode portal: register → open org → invite a teammate (pending invitation appears) → revoke (removed), with the last owner correctly non-removable and zero portal console errors. Upgrade notes (26.7.16.0 → .1): no migration, no config change — UI only.

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.
Live commercial services (2026-07-16):
  • Account portal deployed to Fly.io at https://account.findmydata.io (+ findmydata-account.fly.dev), production + Clerk idp mode, 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.toml already carries the production issuer + key).
Verification: full server suite (331 tests) — 11 new member-management tests (store idempotency, last-owner invariant, owner-only guards, invite auto-accept, route integration incl. CSRF + non-member 404). Typecheck + lint clean. Reviewed adversarially (tenant-isolation / invite-identity / content-boundary lenses). Upgrade notes (26.7.15.10 → 26.7.16.0): no migration — the 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 unless FMD_PLAIN_API_KEY_REF resolves. 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.com host does not resolve; the correct global endpoint is core-api.uk.plain.com (overridable via FMD_PLAIN_API_URL). The Plain machine-user key is scoped to least privilege (customer:create, customer:edit, thread:create).
Adversarial review: content-boundary, correctness, and robustness lenses with per-finding verification; four confirmed findings fixed — org-name boundary leak (removed), a GraphQL null-field 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 14/user/mo,Enterprise14/user/mo**, **Enterprise 29/user/mo (all connectors + priority support). Price ids wired in .env.example; rationale
Verification: 311 server tests passing (seat quantity + storage covered), typecheck + lint clean. Upgrade notes (26.7.15.8 → .9): no migration (the 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 be mock (synthetic data, never a live tenant), and live label writes + directory sync are forbidden. demoMode is surfaced at GET /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.io with Cloudflare Access + Tunnel (free ≤ 50 users, no open ports) — with a cost summary.
Verification: 311 server tests passing (5 new demo-mode guards), server + web typecheck + lint + build clean; the demo banner renders from /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 calls updateAccountPlan on the shared registry, so the deployment picks up the paid edition on next renewal. customer.subscription.deleted reverts 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.
Verification: 304 server tests passing (14 new incl. a signed webhook that actually upgrades the account), typecheck + lint clean; upgrade UI verified in a browser; adversarially reviewed. Activation (operator): create Products/Prices in Stripe, add a webhook to …/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 (rejects alg: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: idp mode requires FMD_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.
Verification: 290 server tests passing (12 new incl. forgery/tamper/expiry/ alg/azp), typecheck + lint clean; adversarially reviewed (JWT-verification lens). Activation (operator): create a Clerk application, add "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/preview returns 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).
Verification: 277 server tests passing (12 new), typecheck + lint clean; adversarially reviewed (multi-lens + per-finding verification). Upgrade notes (26.7.15.4 → .5): no customer-side migration. The deployment gains a read-only /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 on FMD_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 a SHA256SUMS over every artifact.
Verification: 263 server tests passing (14 new in account/downloads.test.ts
  • Control admin release lifecycle), typecheck + lint clean; SBOM generator produces valid CycloneDX; adversarially reviewed (multi-lens + per-finding verification).
Upgrade notes (26.7.15.3 → .4): no customer-side migration. Downloads are optional and off unless 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/, entrypoint account-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.
Verification: 249 server tests passing (12 new in 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/, entrypoint control-main.ts): its own isolated database, its own port, machine endpoints for activate, renew, telemetry, and deployment status, 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, supersedes the prior token, and revokes it (replay resistance).
  • Live online path on the deployment: POST /api/controlplane/activate-online (activation code) and online renewal against FMD_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.
Verification: 237 server tests passing (30 new in 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 2 carrying 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 to control.findmydata.io is gated on FMD_CONTROL_PLANE_URL.
  • Non-destructive license enforcement (built). Starting a new scan requires an active license with the scanning feature (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.
Upgrade notes (26.7.15.0 → .1): migration 0013 applies automatically on boot. New env vars are all optional (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/disabled honestly 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/00010012) apply automatically and transactionally on boot — both the API process (main.ts) and the standalone worker (worker-main.ts) run migrate() before serving; applied migrations are tracked in schema_migrations and 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 the FMD_GRAPH_REMEDIATION_* identifiers are unset by default. Unconfigured capabilities report blocked/disabled honestly rather than failing. Full reference: installation guide, “Configuration reference”.
  • Runtime: Bun 1.3.14. No Node.js is required or used (ADR-0001).