> ## Documentation Index
> Fetch the complete documentation index at: https://docs.findmydata.io/llms.txt
> Use this file to discover all available pages before exploring further.

# ADR 0019 support portal diagnostic bundles

# ADR-0019: Support portal + consent-based diagnostic bundles

**Status:** Accepted · 2026-07-15

## Context

With the customer journey runnable through evaluate → download → install →
activate (ADR-0016–0018), the last major lifecycle surface (web design §8) is
**support**: a customer needs to raise an issue and, when helpful, share
operational diagnostics — without exposing any of their data. The design's hard
constraint: diagnostic bundles are generated **inside** the customer environment,
previewable, redacted, size-limited, explicitly approved, retention-limited,
access-logged, and free of content/secrets by default.

## Decisions

### 1. The bundle is built in the deployment, held to the telemetry discipline

`support/diagnostics.ts` builds a `DiagnosticBundleSchema.strict()` bundle from
the deployment: product/version, non-secret config (env, auth mode, connector
mode, feature flags), migration count + latest name, entitlement state/edition,
scanner counts, health **bands** (queue/dead-letter/scale), and error
**categories** (`RETRY_CLASSES`) with **banded counts** — never raw counts, never
the free-text `safe_diagnostic`. The deployment id is a non-reversible tenant
hash. This is the same `.strict()` allowlist + `SENSITIVE_FIELD_DENYLIST` guard
pattern as fleet telemetry (ADR-0014 §5), extended for richer operational
diagnostics.

### 2. Preview-before-send is a first-class step

`GET /api/support/diagnostics/preview` (capability `operations.read`) returns the
**exact** bundle a support upload would contain — generated locally, nothing
sent. The customer sees precisely what they would share before any deliberate
action. There is no deployment-side endpoint that transmits the bundle anywhere;
uploading is a separate, explicit action the customer takes on the Account portal.

### 3. The vendor re-validates on ingest — the boundary holds on both ends

Uploading a bundle to a support case (`POST …/support-cases/:id/bundle`)
**re-parses it through the same `DiagnosticBundleSchema`** before storing. So even
if a deployment were compromised or misconfigured to attach extra fields, the
vendor plane rejects anything off the allowlist (400) — customer content cannot
be stored in the vendor DB. The strict schema is the boundary on the build side
*and* the ingest side.

### 4. Support is membership-guarded but not entitlement-gated

Support-case routes require org membership (non-members get 404, no existence
leak) with CSRF on mutations, but are **not** blocked by account suspension or
expiry — a customer with a lapsed license must still be able to get support.
Reading a stored bundle enforces account ownership + retention expiry and
**logs the access** (`bundle_access_log`); bundles are retention-limited
(`FMD_ACCOUNT_SUPPORT_RETENTION_DAYS`, default 30) and returned as metadata in
case listings.

### 5. Reuse a capable platform later; keep the contract clean now

Per the design, a capable ticketing/knowledge platform is preferable to building
commodity support software. This build implements the **contracts** — case
lifecycle, the consent-based bundle, retention, access logging — so a real
platform can be integrated behind them without changing the customer-facing model.

## Consequences

* The full lifecycle is now runnable: visit → register → evaluate → download →
  activate → **support** (raise a case, preview + optionally share a redacted
  diagnostic) → convert.
* Support staff never gain access to the customer's installed platform or data;
  they receive only what the customer explicitly, visibly approved.
* Out of scope (future): a real ticketing/KB integration, in-portal case
  threading/replies, bundle size caps beyond the schema's inherent bound, and
  automated retention sweeps.

## Verification

`support/diagnostics.test.ts` (7) — allowlist/denylist guard, pseudonymous id,
error categories as banded counts (safe\_diagnostic text proven absent),
per-tenant scoping, and the preview route (bundle shape + capability gate + tenant
id absent). `account/support.test.ts` (5) — case create (CSRF + membership),
cross-account 404 on list/create/upload, valid-bundle upload + access-logged read,
and **rejection of a content-carrying bundle on ingest (400)**. All 277 server
tests pass; typecheck + lint clean; adversarially reviewed.
