Document type: Companion to PRD v4.0 and Addendum A1.0 — standalone, mergeable Applies to: Minimente — Stress Management (Programme #1) and all future programmes Version: TD1.0 — DECISION RECORD Date: 2026-07-27 Owner: Engineering. Content, programme-shape, legal and clinical decisions remain with Minni and external review, and are listed — precisely — in §TD3 and §TD4. Status of PRD v4.0 and the Breathing Addendum: Frozen. This document modifies neither. Where a decision here gives effect to a v4/A1 requirement, the requirement ID is cited; where it resolves an OPEN item, the resolution is recorded here and the frozen documents' rows stand as historical record.
PRD v4.0 leaves 26 open questions (§10) and the Breathing Addendum leaves 9 (§A10). A previous pass treated all 35 as requiring founder sign-off. That was wrong for a specific subset: questions whose answer is engineering, architecture, infrastructure, data-model, security, or interaction-implementation judgement are engineering's to close, and a PRD described as implementation-ready that cannot name its own stack is not implementation-ready. OPEN-24 was the clearest case — and it cascades: OPEN-BR-07 (haptic pacing) is explicitly blocked on it, and §11 Phase 1 item 7 (the content schema) "cannot be specified against an unknown client platform".
This document therefore does three things:
Several §10 rows name "Minni + eng" as owner. Where the substance of the row is engineering judgement (the stack, the enrolment mechanics, threshold encoding, haptics feasibility), this document takes the position that the decision is engineering's, makes it, and marks it CLOSED (technical). Minni retains a veto over anything in this document, as she does over everything — but her sign-off is not a precondition for starting build on the CLOSED items. The Phase 0 item-1 working session (v4 §11) ratifies the product-shape provisionals; it does not need to re-litigate infrastructure.
Conversely, nothing here touches what is genuinely hers or external: programme structure, module content, authored copy, positioning, pricing, market; DPIA, controller/processor, safeguarding copy and vocabulary, clinical validity of thresholds. Those remain OPEN and are restated in §TD3/§TD4 so they can be cleared in, at most, two sittings (one for Minni, one for legal/clinical).
Statuses: CLOSED (technical) — decided here, build may start. SPLIT — engineering half closed here; residual named and re-stated in §TD3/§TD4. OPEN — Minni / legal / clinical — not an engineering decision; restated in §TD3/§TD4. Two v4 rows (OPEN-17, OPEN-23) were already dispositioned in v4 itself and are listed for completeness only.
| ID | One-line summary | Disposition | Where the answer lives |
|---|---|---|---|
| OPEN-01 | 14 vs 15 days | OPEN — Minni (folds into structure ratification) | §TD3 Q1 |
| OPEN-02 | Module 11 copy incomplete | OPEN — Minni (authoring count reduced by 1: Calm Breathing branch references BR-1 per A7.1) | §TD3 Q4 |
| OPEN-03 | Modules 12–15 body copy missing | OPEN — Minni | §TD3 Q4 |
| OPEN-04 | Module titles/themes; M13 topic | OPEN — Minni | §TD3 Q4 |
| OPEN-05 | Corrupted icons / celebration asset | SPLIT — icon technology and asset policy closed | ADR-11; residual §TD3 Q6 |
| OPEN-06 | Emergency Tool scripts don't exist | SPLIT — runtime/count-independence closed; two scripts retired by BR-1/BR-4 (A7.4) | ADR-04, ADR-13; residual §TD3 Q5 |
| OPEN-07 | PR-03 threshold undefined, contradicts MVP | SPLIT — encoding, destination mechanism, distinct-day evaluation closed; threshold values clinical | ADR-09; residual §TD4 C2 |
| OPEN-08 | "Mood" promised, never collected | OPEN — Minni (engineering note: zero-cost either way — ADR-04) | §TD3 Q9 |
| OPEN-09 | Finnish vs English market | OPEN — Minni (i18n architecture already binding; EU residency chosen regardless — ADR-01) | §TD3 Q10 |
| OPEN-10 | No crisis-safety flow | SPLIT — all mechanics closed and build-ready; option choice, copy, vocabulary remain | ADR-09; residual §TD3 Q13, §TD4 C1 |
| OPEN-11 | Controller/processor, DPIA | OPEN — legal (engineering input delivered: full sub-processor register, ADR-01 §D) | §TD4 L1 |
| OPEN-12 | Pricing unvalidated | OPEN — Minni | §TD3 Q11 |
| OPEN-13 | Sequence-lock / missed-day behaviour | OPEN — Minni (ratification only; FR-CORE-04 fully specifies it; zero engineering residual) | §TD3 Q2 |
| OPEN-14 | Post-programme state | SPLIT — revisit/re-run implementation default closed | ADR-15; residual §TD3 Q3 |
| OPEN-15 | Audio production unplanned | SPLIT — v1 ships text+pacer; audio is a content increment needing no release | ADR-12; residual §TD3 Q8 |
| OPEN-16 | Duplicate reflection prompt M1/M2 | OPEN — Minni | §TD3 Q7 |
| OPEN-17 | Outputs had no destination | Adopted in v4 (FR-CORE-09) — not open | — |
| OPEN-18 | Core+Extensions vs linear | OPEN — Minni | §TD3 Q1 |
| OPEN-19 | Anonymity vs seat management | CLOSED (technical) | ADR-02, ADR-03 |
| OPEN-20 | Outcome measurement vs no diagnostics | OPEN — Minni (accept/decline §4.4's recommendation) | §TD3 Q12 |
| OPEN-21 | Teams integration | OPEN — Minni (pilot discovery item, per §9.4) | §TD3 Q14 |
| OPEN-22 | Org dashboard residuals | SPLIT — per-org configuration data model and enforcement closed; visual design and pilot-report format remain | ADR-02 §C; residual §TD3 Q15 |
| OPEN-23 | Source A next steps stale | Superseded in v4 (§11) — not open | — |
| OPEN-24 | Technology stack never ratified | CLOSED (technical) | ADR-01 (+ ADR-06, ADR-07) |
| OPEN-25 | Tier ladder defined two ways | OPEN — Minni | §TD3 Q11 |
| OPEN-26 | 5- vs 10-point check-in scale | SPLIT — control is scale-config-driven, both variants built, default 10-point, arithmetic rescale prevented by construction; final choice after testing | ADR-10; residual §TD3 Q16, §TD4 C3 |
| OPEN-27 | Search synonym + crisis-term vocabularies | SPLIT — index build, term-evaluation machinery, authoring format closed; vocabularies themselves authored/reviewed externally | ADR-08, ADR-09; residual §TD3 Q13, §TD4 C1 |
| OPEN-28 | Per-module Daily-Action endings | OPEN — Minni | §TD3 Q7 |
| OPEN-BR-01 | Module 1's taught exercise | OPEN — Minni (Option A recommended, per A5.2) | §TD3 Q17 |
| OPEN-BR-02 | Breathing library size | OPEN — Minni (phased five recommended; engineering note: architecture identical at 3, 5, or phased — zero build coupling) | §TD3 Q17 |
| OPEN-BR-03 | Unexpressible situations (anxiety/panic) | OPEN — clinical + Minni (confirm the refusal) | §TD4 C4 |
| OPEN-BR-04 | History-based recommendation in v1 | CLOSED (technical) — deferred to post-pilot; capture ships now | ADR-13 |
| OPEN-BR-05 | Recommender thresholds chosen, not evidenced | CLOSED (technical) — adopted as content-data defaults, flagged unevidenced, pilot-revisited | ADR-13 |
| OPEN-BR-06 | Outcome-capture wording | OPEN — Minni | §TD3 Q17 |
| OPEN-BR-07 | Haptic pacing vs stack | CLOSED (technical) — resolved by ADR-01's platform decision | ADR-01 §C, ADR-06 |
| OPEN-BR-08 | Confirmatory clinical pass on evidence grades | OPEN — clinical | §TD4 C5 |
| OPEN-BR-09 | How honest to be about effect size | OPEN — Minni | §TD3 Q18 |
Tally: 5 closed outright (OPEN-19, OPEN-24, OPEN-BR-04, OPEN-BR-05, OPEN-BR-07), 8 split with the engineering half closed (OPEN-05, -06, -07, -10, -14, -15, -22, -26, -27 — nine, counting OPEN-27), and the remainder left open because they genuinely belong elsewhere — every one of them is content, programme shape, positioning, pricing, legal posture, or clinical validity.
Format per ADR: Decision · Closes / gives effect to · Alternatives and why they lost · Forced or constrained by · Risks and revisit triggers · Cascades.
Closes OPEN-24. Resolves the dependency in OPEN-BR-07. Unblocks §11 Phase −1 item 0c and Phase 1 item 7.
TypeScript end-to-end, PWA-first, Supabase as the data platform, one EU-hosted API service. Concretely:
| Layer | Choice | EU residency |
|---|---|---|
| Language | TypeScript everywhere (client, server, content tooling, rule engine) | n/a |
| Client | React 19 + Vite + vite-plugin-pwa (Workbox) — installable PWA, static app shell, client-rendered | Static assets only; no personal data (see below) |
| Client state/data | TanStack Query + Router, Zustand, Dexie (IndexedDB), MiniSearch, i18next, Radix UI primitives + Tailwind CSS | On-device |
| Data platform | Supabase, project region eu-central-1 (Frankfurt): Postgres 15+ with RLS (AD-04), Supabase Auth (magic link, NFR-SEC-03), Supabase Storage (audio/illustrations, AD-06) |
Frankfurt |
| API service | One Node.js (Node 24 LTS) modular-monolith service (Hono) implementing §9.1's five logical services — Identity/Org glue, Content, Programme & Response, Notification, Reporting & Aggregation — as modules of a single deployable. Hosted on Railway, region europe-west4 (Netherlands) |
Netherlands |
| Static hosting/CDN | Cloudflare Pages for the PWA shell and published content bundles | Global CDN — carries no personal data (app shell, published content, search index only); all API calls terminate in the EU |
| Scheduling / jobs | pg-boss (Postgres-backed job queue — no Redis, no second datastore) | Frankfurt (in-DB) |
| Push | Web Push (VAPID) via the web-push library; payloads end-to-end encrypted per RFC 8291 |
Payloads unreadable by push relays (see ADR-06) |
| Email (magic links + reminder fallback) | Brevo (EU-domiciled, EU data), wired as Supabase Auth custom SMTP and as the Notification module's fallback channel | EU |
| Error tracking | Sentry, EU data-residency organisation, with the ADR-07 scrubbing policy. No third-party analytics SDK at all — product events live in our own tables | EU |
| Repo / CI | GitHub + GitHub Actions; pnpm monorepo | Code only — no user data |
| Native shells (conditional) | Capacitor wrapping the same PWA, triggered per ADR-06 §D | n/a |
Monorepo layout (Phase −1 item 0d):
minimente/
├── apps/web # PWA client
├── apps/api # Hono service (the five §9.1 modules)
├── packages/content # content source (YAML) + Zod schemas + publish pipeline + index build
├── packages/rules # deterministic rule engine, shared client+server (AD-BR-03) + golden files
├── packages/shared # types, API contracts (Zod), i18n keys
└── supabase/ # SQL migrations, RLS policies, pgTAP tests, seed
Initial dependency set — the package manifest an engineer can act on:
apps/web: react, react-dom, typescript, vite, vite-plugin-pwa, workbox-window, @tanstack/react-router, @tanstack/react-query, zustand, dexie, minisearch, i18next, react-i18next, @radix-ui/react-dialog, @radix-ui/react-radio-group, @radix-ui/react-progress, @radix-ui/react-toast, tailwindcss, @supabase/supabase-js, zodapps/api: hono, @hono/node-server, pg, kysely (typed queries; migrations are SQL-first via the Supabase CLI), pg-boss, web-push, jose, zod, @sentry/nodepackages/content: zod, yaml, minisearch, tsxpackages/rules: zero runtime dependencies by design (pure TS — AD-BR-03's client/server-identical evaluation depends on this)vitest, @testing-library/react, playwright, @axe-core/playwright, eslint, prettier, pnpm; pgtap extension in the database (ADR-14)supabase/ migrations, tested in CI (ADR-02, ADR-14). This is the single strongest reason for the choice: NFR-SEC-04's "technically incapable" is delivered by the database engine, not by API discipline.| Sub-processor | Role | Personal data touched | Region |
|---|---|---|---|
| Supabase (on AWS) | Database, auth, object storage | All user data; auth email | eu-central-1 (Frankfurt) |
| Railway (on GCP) | API compute | All user data in transit/processing | europe-west4 (Netherlands) |
| Cloudflare | Static shell + content CDN | Client IPs, no application data | Global edge |
| Brevo | Transactional email | Email address, message metadata; reminder bodies generic by default (FR-NOT-04) | EU |
| Sentry (EU org) | Error telemetry | Scrubbed events only (ADR-07); no free text, no identifiers beyond internal user id | EU |
| Apple / Google / Mozilla push services | Web-push relay | Encrypted payloads (RFC 8291) — content unreadable by the relay; endpoint existence reveals a subscription exists | Vendor-operated |
| GitHub | Source code, CI | None | — |
AD-02 (PWA-first, one codebase), AD-04 (RLS), NFR-SEC-03 (magic link / SSO), NFR-SEC-04 ("technically incapable"), NFR-PRIV-08 (EU residency, documented sub-processors), NFR-PLAT-01/04 (parity, offline), NFR-A11Y-01 (mature accessible primitives — hence Radix), FR-BR-36 (haptics reality), AD-BR-03 (rule engine identical client/server — hence shared pure-TS package).
fra/ams or Scaleway in a day.Unblocks Phase −1 (scaffold), Phase 1 item 7 (content schema — ADR-04), item 8 (data model/privacy design — ADR-02/07); resolves OPEN-BR-07; determines the concrete form of ADR-02 through ADR-14.
Closes OPEN-19. Closes the engineering half of OPEN-22 (per-org configuration data model). Gives effect to NFR-SEC-04, NFR-PRIV-05/10, FR-ORG-01…03, AD-04.
The employer-anonymity promise is enforced as Postgres RLS policies plus a physically separate aggregate path. The org-admin role has no read path to any user-level table — not a filtered path, no path.
user_id, org_id, role ∈ {member, org_admin} as custom claims (set via Supabase auth hook). The API service passes the user's JWT through to Postgres so RLS applies even to API-mediated queries — the service does not run user reads as a superuser. A distinct service_role is used only by the aggregation job and the publish pipeline, never by request-serving code paths.users, enrollments, session_states, daily_checkins, screen_responses, module_completions, toolkit_items, action_plan_entries, emergency_tool_events, right_now_events, reminders, personalisation_events, programme_feedback, safeguarding_events, exercise_outcomes, breathing_recommendation_events): single policy pattern — user_id = auth.uid() for select/insert/update; no policy exists for org_admin on any of these tables, so default-deny applies. "Technically incapable" (NFR-SEC-04) is therefore a property of the engine, verified by CI (ADR-14: pgTAP tests enumerate every user table and assert org_admin selects return zero rows / permission errors).service_role job (pg-boss) computes rollups into org_aggregates (org_id, iso_week, metric, value, cohort_n) — and the k≥10 gate (NFR-PRIV-05) and weekly-or-coarser granularity (NFR-PRIV-10) are enforced at write time by the job: rows with cohort_n < 10 or sub-weekly granularity are never written, so no query, bug, or future dashboard feature can reach around the gate — the ungated numbers do not exist on the org-admin side of the wall. org_admin has a select policy on org_aggregates (own org only) and on nothing else. Free text never enters the rollup job's queries at all (NFR-PRIV-06 by construction — the job's SQL selects only numeric/enum columns).users row; the user may register with any email address, including a personal one — the invite code, not the email domain, is what proves seat entitlement. Email lives only in Supabase's auth schema; the application schema stores auth_identifier_hash only (§7.4). org_seats.claimed_by_user_id is readable by no org-facing role: org admins see seat counts (claimed/unclaimed/revoked) via org_aggregates, and revoke by seat id, not by person. The org therefore knows how many seats are active, never which employee is which account. This resolves the anonymity-vs-licensing tension: licensing needs countable seats; it never needed identifiable ones.organizations.crisis_resources_config (already in §7.4) is given a concrete schema, validated by Zod at write time: { country_key, resources: [{label, phone?, url?, description}], occupational_health?: {label, phone?, url?} }. Country-default resource sets (Finland: 112, MIELI Crisis Helpline; UK: 999, Samaritans) ship as content data; the org config overlays them. This is the destination table for PR-03 (ADR-09) and NFR-SAFE-02's signposting card. The admin UI to edit it is Phase 3 (unchanged); until then it is set at contract time by Minimente staff.WHERE clause from a breach of the core promise. No path at all is auditable at a glance and testable in CI.NFR-SEC-04 (the load-bearing requirement), NFR-PRIV-04/05/06/10, FR-ORG-01…03, NFR-SAFE-05 (no employer notification — the aggregate wall is also why safeguarding events can never leak), §3.1 (data model must not make an org-less user impossible — org_id is nullable by design for the future B2C user).
ADR-14 (RLS test suite), ADR-09 (safeguarding config destination), Phase 1 item 8, Phase 3 item 16.
Completes the OPEN-19 closure. Gives effect to NFR-SEC-03.
Passwordless email magic link via Supabase Auth in v1; org SSO (SAML) deferred to Phase 2 as already scoped in §4.2. Specifics: magic-link emails sent through Brevo (custom SMTP) from a neutral sender identity (no "mental health" in sender name or subject — NFR-PLAT-05 applies to inboxes too, which may be work inboxes); access tokens ~1 hour, rotating refresh tokens; per-device sessions with a user-visible device list and revocation (NFR-SEC-03); API-level rate limiting on auth endpoints (the §9.1 "gateway" responsibilities — authn/z, rate limiting — are middleware in the Hono service for v1, not a separate gateway product).
Magic link means the system holds an email address — the product is pseudonymous with respect to the employer, not anonymous in the absolute. Copy must never say "anonymous" unqualified; NFR-PRIV-04's actual wording ("your employer cannot see…") is the honest and correct formulation. Flagged in §TD5.3 because marketing will be tempted.
Gives effect to FR-CORE-01/02/03/06, NFR-I18N-01/02, FR-SRCH-07, FR-BR-48/49. Closes the engineering halves of OPEN-06 (count-independence) and OPEN-08 (zero-cost note). Unblocks Phase 1 item 7.
Git is the CMS for v1. Content lives in packages/content as YAML files — one per module, exercise, rule set, vocabulary, and locale — validated by Zod schemas that encode §6.0's screen-type contracts, §7.4's exercise shape, and A8.2's breathing_config. A publish pipeline (CI job) performs, in order:
EX_* screen references an existing exercise (FR-CORE-06); taught_exercise_id present per module (FR-CORE-07); locale completeness per NFR-I18N-02; FR-BR-49's rule checks (no non-final exercise referenced, no hold-protocol recommended for high-activation states, every need-state has a terminal fallback, no unreferenced reason_key); scale-version pinning (ADR-10); the §11 item-18 content QA checks (no placeholder copy, no mojibake — non-UTF-8-sane glyph detection — no Finnish strings in an EN bundle) run here as blocking CI, not as a manual read-through.copy_status = final items for release channels (placeholder content is included only in the internal channel — FR-CORE-03), plus the MiniSearch index built at publish time over released content only (FR-SRCH-03, ADR-08) and the crisis-term list (ADR-09).content_versions row; clients discover the new version via a version endpoint and refresh their cache. Rollback = re-activate the previous row.Emergency Tools are content rows, not code (FR-ET-08): the 6-vs-7 tool-count question (OPEN-06) has zero engineering coupling — the drawer renders whatever exercises carry the emergency_tool surface tag, so Minni's count decision and each script's arrival are pure content publishes. Likewise mood (OPEN-08): adding a mood item to the check-in is one content field plus daily_checkins.extra — no schema change, no release; the decision is purely whether the pitch keeps the word.
Git-mediated authoring puts engineering in Minni's loop for every copy edit — acceptable at current cadence, and the revisit trigger is stated above. The validator set must grow with every incident: any content defect that reaches a release channel becomes a new blocking check the same week.
ADR-08 (index build lives here), ADR-09 (vocabularies are content files here), ADR-10 (scale-version pinning), ADR-13 (rule content), ADR-14 (content CI).
Gives effect to NFR-PLAT-04, FR-CORE-05, FR-PROG-02/03, FR-ET-06, FR-SRCH-05 (offline search).
session_states.draft_answers autosaves on blur/navigation (FR-PROG-03) into the outbox.packages/rules engine locally; the server re-evaluates on sync and is authoritative on conflict (AD-BR-03, verbatim).Safari's storage-eviction policies for non-installed web apps can purge IndexedDB under storage pressure after prolonged disuse — one more reason the iOS install flow (ADR-06) matters. The outbox is flushed eagerly and the server holds truth, so eviction loses at most unsynced drafts; the sync interval is aggressive (on visibility change and every mutation) to shrink that window.
Gives effect to AD-07, FR-M7-05/06, FR-NOT-01…07, FR-BR-32. Implements the iOS strategy and native trigger declared in ADR-01 §C.4.
Two timing regimes, explicitly separated — this is the concrete realisation of AD-07:
started_at, returns authoritative ends_at and server_now; the client renders the countdown from a monotonic clock (performance.now()) plus the server-time offset, so backgrounding, tab suspension, clock skew, and device sleep cannot corrupt it. The completion notification is scheduled server-side (pg-boss job at ends_at) and delivered as push — so it fires even if the tab is dead; the client additionally checks for elapsed timers on every resume/visibility change (the in-app path when no push channel exists). Ending early cancels the job (FR-M7-06: a valid outcome, not a failure).performance.now() (FR-BR-32). Server anchoring at this granularity is meaningless and harmful; AD-07's reasoning — never trust setInterval across backgrounding — applies, its mechanism (server anchor) does not. Recorded so no one "fixes" the pacer with network calls.Delivery pipeline (FR-NOT-02's channels made concrete): the Notification module resolves each due reminder to channels in order — Web Push subscription if present and live → email via Brevo if opted in → in-app inbox always. Quiet hours and timezone (FR-NOT-05) are computed in the user's timezone at schedule time and re-checked at fire time (DST safety). Reminder bodies default to generic copy; user-authored content (e.g. boundary_statement) is embedded only on explicit opt-in (FR-NOT-04), and such bodies are not sent via email at all in v1 — mail sits unencrypted in a possibly-work inbox; push (E2E-encrypted to the device, RFC 8291) and in-app are the only carriers for user-authored text. Nothing in any payload names mental health (NFR-NOT-07 / NFR-PLAT-05 — neutral app name and copy in notification surfaces).
iOS posture and the native trigger: per ADR-01 §C.4 — guided install flow, email-default suggestion on non-installed iOS, and the measured Capacitor trigger. Restated here because this ADR is where the risk lives: FR-NOT-03 (daily reminder) is the requirement most at risk on the chosen stack, the mitigation is the fallback chain above, and the escape hatch is measured, not vibes-based.
web-push + pg-boss is a few hundred lines.Push-subscription rot (expired endpoints) is routine: subscriptions are health-checked on delivery failure and the user is prompted to re-enable on next open. The Capacitor trigger is the structural revisit. Email deliverability for magic links is operationally critical from day one (before push exists at all) — Brevo domain authentication (SPF/DKIM/DMARC) is a Phase −1 setup task, not an afterthought.
Gives effect to AD-05, NFR-SEC-01/02/05/06, FR-DATA-02.
screen_responses.value_text where is_sensitive, daily_checkins.optional_text, the thought-dump journal, reflection_letter, toolkit_items free-text payloads, reminders.body_override): AES-256-GCM via node:crypto; per-user data keys (DEKs) wrapped by a versioned master key (KEK) that lives only in the API runtime's secret store (Railway), never in the database or its backups. A database dump, a Supabase console session, or a stolen backup therefore yields ciphertext for every reflection ever written — this is AD-05's "narrower blast radius" made concrete, over and above Supabase's at-rest disk encryption (NFR-SEC-01, which alone would not survive a console-level read).admin_access_log before the decrypt path will run.beforeSend — error class, stack, release, route pattern (never URL with params), internal user id only. Request bodies, breadcrumb input values, and console args are dropped wholesale. No session replay. No third-party analytics SDK exists to leak anything (ADR-01); product metrics come from our own event tables. Search queries never appear anywhere by construction (FR-SRCH-05 — they never leave the device).Server-side free text is ciphertext → any future server-side scanning (NFR-SAFE-04 option 3) would need an explicit decrypt pipeline — an intentional speed bump that makes option 3 a deliberate act, never a drift. Personal-index search remains possible because the client holds plaintext after authenticated fetch (ADR-08). Export (NFR-PRIV-07) decrypts server-side through the same audited path, actor = the user.
Gives effect to AD-09, FR-SRCH-01…07, FR-TK-10, FR-BR-45. Closes the engineering half of OPEN-27.
Retrieval quality is exactly as good as the OPEN-27 vocabulary — an authoring dependency, stated in FR-SRCH-02 and unchanged here. Search ships dark until the vocabularies exist (§11 item 15a), but everything in this ADR is buildable now against a stub vocabulary.
Closes the engineering halves of OPEN-10 and OPEN-07. Gives effect to NFR-SAFE-01…07, FR-PR-01…03, FR-SRCH-06, FR-BR-25/26, PR-01…03 encoding.
Everything §8.3 needs that is mechanism is decided and buildable now; everything that is copy, vocabulary, or clinical validity is packaged as three precise external asks (§TD3 Q13, §TD4 C1/C2).
personalisation_rules content set structurally identical to A8.2's breathing_recommendation_rules (ordered, declarative JSONB conditions, no code), evaluated by the shared packages/rules engine — client-side for offline, server authoritative (AD-BR-03). Firings log per FR-PR-03 / NFR-SAFE-06 (de-identified, rule-id-only for safeguarding).stress_rating ≥ S on ≥ N distinct days of the last M check-in days, shipped with placeholder values (S=7, N=3, M=5) that are flagged clinically_unreviewed: true in content data — the publish pipeline refuses to include an unreviewed rule in a release-channel bundle, so the clinical sign-off (§TD4 C2) is structurally enforced, not remembered. Destination: the tier-aware signposting card (org occupational-health + public resources) per §7.3's own recommendation — the Tier-1 contradiction is resolved by pointing at the ADR-02 org config, which exists precisely for this.packages/rules, consuming a content-data term list (OPEN-27b). It is required anyway for search (FR-SRCH-06/NFR-SAFE-07, which is independent of the NFR-SAFE-04 choice — v4 says so explicitly). Consequence, and this materially reframes Minni's decision: NFR-SAFE-04 option 2 (client-side keyword prompt on free-text inputs) is the same evaluator wired to input fields — near-zero incremental engineering. Her choice between options 1 and 2 is therefore about the promise wording and governance posture, not cost or schedule; option 3 remains a governance-gated non-option for v1 (§8.3's own warning, reinforced by ADR-07's encryption speed bump). The evaluator runs on-device only; no text or match content is transmitted; on match it renders the signposting card and logs rule-id-only per NFR-SAFE-06.safeguarding_events in the de-identified form, exactly as A5.6 specifies — data ready for whenever §8.3's detection thresholds are clinically designed, no retrofit.Unblocks Phase 2 item 12 (safeguarding flow before external testing) up to the copy dependency. §TD4 C1/C2 are the only remaining blockers to shipping it.
Closes the engineering half of OPEN-26. Gives effect to NFR-A11Y-02, FR-STD-02, the §7.3/§8.3 scale-coupling notes.
scale_version: "10pt-v1"), not code. Both 5- and 10-point variants are built; the Phase 2 internal test (§11 item 13) runs both, as v4 requires.scale_version its thresholds were derived against, and the publish pipeline rejects any bundle whose active scale and rule scale-versions disagree. If the test picks 5-point, the build fails until clinically re-derived thresholds (§TD4 C3) are published — engineering cannot quietly divide by two, which is exactly the prohibition v4 wrote in prose, now enforced by tooling.The 5-vs-10 choice stays with Minni + design after testing (§TD3 Q16); the re-derivation, if needed, with clinical (§TD4 C3). Nothing further from engineering.
Closes the engineering half of OPEN-05. Gives effect to NFR-A11Y-06, §6.3's icon recommendation.
Lucide as the product icon set (MIT, consistent stroke style, tree-shakeable React components), with aria-label/alt text equal to the semantic category name; raw emoji are banned from rendered content — the ADR-04 validator rejects emoji in icon-designated content fields, making the §6.3 recommendation enforceable rather than advisory. The M14 Recovery Wheel gets eight Lucide glyphs mapped from the §6.3 semantic-intent table; the M15 celebration is a static illustration with a subtle animated variant gated on prefers-reduced-motion (NFR-A11Y-05) — no confetti-by-default, consistent with the product's visual restraint (NFR-BR-07's register, applied product-wide). Alternatives: Phosphor/Heroicons — interchangeable; Lucide chosen for breadth and maintenance. Residual for Minni + design (§TD3 Q6): confirm the eight meanings (the mojibake reconstruction) and the celebration art direction — the technology carries whatever she confirms.
Closes the engineering half of OPEN-15. Gives effect to AD-06, NFR-A11Y-03, UX-05, FR-BR-51.
v1 ships text-and-pacer only, product-wide — extending FR-BR-51's rule for breathing to every audio-guided exercise. The architecture already treats audio as an optional per-exercise asset (exercises.audio_asset_url NULL, AD-06): the exercise runtime renders transcript + pacer always, and audio controls iff an asset exists and the user opts in (no autoplay — FR-M1-04). Adding audio later is a content publish per exercise — file to Supabase Storage, URL on the row, zero code. The "headphones recommended" hint (NFR-PLAT-03) renders only when an asset exists. This is fully sanctioned by the spec — UX-05 makes audio never-required, and OPEN-15's own proposed resolution names text-only as "fully viable". Residual for Minni (§TD3 Q8): whether/when to record, voice, languages, budget — a production plan with no engineering dependency and no launch coupling.
Closes OPEN-BR-04 and OPEN-BR-05. Gives effect to FR-BR-07…18, FR-BR-50, AD-BR-03.
v1 ships the safety and evidence-default layers in full, and defers the history-based layers to post-pilot. All capture ships now. Concretely, of §A4's ordered layers:
| Ships in v1 | Deferred to post-pilot |
|---|---|
BR-R-00 safety gate; BR-R-10 suppression (discomfort ×2 → protocol excluded — this is a safety rule and needs history capture, which ships); first-time defaults BR-R-01…06; favourites (simplified layer 3: a favourited, non-suppressed protocol is recommended — the completion-rate qualifier joins the deferred set); explanations (FR-BR-12d), single-recommendation-plus-browse (FR-BR-10) |
BR-R-11 ("worked for you before"), BR-R-13 ("you usually complete"), recency window and anti-ossification offer (FR-BR-12b/c) — the layers whose only justification is the personalisation literature the addendum itself grades "scarce and inconclusive" |
exercise_outcomes ships complete from day one (cheap, free-text-free per FR-BR-46, and it is what makes the deferral decision measurable — §A10's own proposed resolution).evidenced: false flag; the deferred layers' thresholds are present but dormant. They are revisited against pilot data (§11 item 22) and are never presented as tuned — the flag exists so no one can later mistake a default for a finding.The rules engine, data model, and player are identical under either scope — what this decides is build sequencing and claim surface, which is engineering judgement: fewer active rules means a smaller test matrix before pilot, and no implicit product claim that history-based recommendation improves anything (the addendum was explicit that it cannot claim that). The product-visible consequence — users will not see "because it worked for you before" until post-pilot — is flagged to Minni in §TD3 Q17; if she wants that surface at launch anyway, the cost is small and this ADR flips without architectural consequence. That veto path is stated here so the closure is honest rather than territorial.
Gives effect to FR-BR-50, §11 item 18, NFR-A11Y-01 verification, NFR-SEC-04 verification, AD-BR-03's testable determinism.
GitHub Actions pipeline, blocking on every merge: lint + typecheck → unit (Vitest) → rules golden files → content validation (the full ADR-04 §A.1 battery) → database tests → E2E → build.
The suite's four load-bearing, non-generic components:
org_admin role cannot select, insert, update, or delete; assert a member cannot read another user's rows; assert org_aggregates never contains a row with cohort_n < 10 or sub-weekly granularity (belt-tests on the ADR-02 write-time gate). A new user-data table without its pgTAP entries fails CI via a catalog-diff check — the anonymity promise is regression-tested, not remembered.clinically_unreviewed release gating (ADR-09).prefers-reduced-motion asserted to force the visual_reduced pacer with zero animated properties (NFR-BR-05 — tested, not trusted); an offline scenario completing today's module and an Emergency Tool with the network disabled, then syncing (NFR-PLAT-04); a grep-level guard that no search-endpoint call path exists (ADR-08).Closes the engineering half of OPEN-14 (FR-POST-04's residual detail).
Implementation default: completed modules are revisitable read-only (the user sees the content and their own answers), and every exercise is re-runnable everywhere, forever (FR-POST-04's unconditional half, via the exercise library — FR-CORE-06). Full module re-runs (a fresh pass writing new responses) are not built in v1; the capability is a content/config flag on the module, so if Minni ratifies re-runs under OPEN-14, enabling them is additive (new module_completions row per pass; historical answers preserved, never overwritten — the data model above already supports it). Rationale: read-only revisit satisfies every stated post-programme need (§7.9), avoids ambiguity about which answers feed the Toolkit and trends, and keeps the OPEN-14 ratification a pure product choice with no schema consequence. Residual for Minni (§TD3 Q3): ratify §7.9 as a whole and say whether re-running a whole module should exist.
Every question below can be answered in one sitting, without technical background. Where v4 or the addendum made a recommendation, it is restated with its reasoning in one line. Answering Q1–Q7 in one working session is v4's Phase 0 item 1; Q8–Q18 can trail it by days without blocking build.
Q1 — Programme shape (the big one). Should the programme be (a) a 10-session core plus a browse-anytime library of 5 extra topics, unlocked after the core, or (b) a single 15-day sequence as your July content document laid it out? Option (a) — recommended — means the product can launch with only the fully written sessions 1–10; the five unfinished topics arrive later as additions; and the awkward "14-day vs 15-day" wording problem disappears because neither number is used anywhere. You'd also tell us where the closing session (reflection letter, future plan, feedback) belongs: as the finale of the core, or shown when someone finishes. Option (b) means launch waits for all fifteen sessions to be written, and you must also pick one number — 14 or 15 — for every piece of marketing and welcome copy.
Q2 — Pacing. Confirm: sessions unlock in order as each is completed — a keen user may do several in one day; the app suggests spacing but never blocks; nothing is ever labelled "Locked"; missing days carries no penalty. (This replaces the old one-per-calendar-day idea. Everything is built; this is a yes/no.)
Q3 — After the programme. Confirm: once someone finishes, their home screen becomes their Toolkit first, with the quick-help entry, emergency tools, optional light check-ins, and the extras library. And one small extra: should people be able to redo a whole session from scratch (writing fresh answers), or is revisiting what they wrote plus re-running any exercise enough? We've built the second; the first can be switched on later if you want it.
Q4 — The unwritten sessions. Three sub-decisions, in this order: (i) Session 13's topic — is it "Understanding Your Emotional Needs" (your July version) or "Meaning & Motivation" (the March plan)? These are different sessions, and the text is unwritten either way, so you are choosing what to write, not what to rename. (ii) Confirm the final titles for sessions 12, 14, 15. (iii) Then supply the missing text: sessions 12–15 in full, plus session 11's teaching text and its three remaining movement-branch scripts (the breathing branch is now covered by the new breathing library, so it's three, not four). First check whether your earlier draft still exists in your files or Drive history — the export says "keep your current text", which suggests a draft that didn't survive.
Q5 — Quick-help tools. (i) Is the tool list six or seven — is "Progressive relaxation" in or out? (ii) Then author the remaining scripts. Good news: the breathing work already covers "Breathing reset" and "Grounding", so what's left is the thought-dump journal intro, the desk stretch, the safe-place exercise — and progressive relaxation only if you keep it. These tools are visible from day one and the "help me right now" entry can't switch on until they exist, so this is the highest-value short writing task in the project.
Q6 — Icons and celebration. The eight Recovery Wheel categories lost their icons in the export. Confirm the intended meaning of each (our best guesses: Rest 🌿, Connection 🤝, Movement 🏃, Nature 🌳, Creativity 🎨, Learning 🧠, Joy & Play 🎲, Purpose ✨ — we'll render them as proper drawn icons, not emoji), and tell design what the end-of-programme celebration should look like.
Q7 — Small copy calls. (i) Sessions 1 and 2 end with the identical reflection question ("Where did you notice stress in your body today?") — intentional, or should one change? (ii) For each session, pick how it ends: do a tiny action right now / save today's tool / a "when X happens, I'll Y" intention / set a reminder / just an insight. (Reminders should be the exception, not the default.) One pass over the ten-session list.
Q8 — Audio. Version 1 will ship with written guidance and an on-screen pacer, no recorded audio — it's fully usable that way, and the silent-office design means audio was always optional. Your decision is only: do you ever want recorded voice, and if so — whose voice, which languages, and when? Adding it later needs no engineering work.
Q9 — "Mood". The pitch materials promise daily energy, stress, and mood tracking, but no session asks about mood. Either add a mood question to the daily check-in (costs nothing to add — say the word) or drop "mood" from the pitch. Which?
Q10 — Market. Finland first, English-speaking first, or both? This decides which crisis helplines ship by default, which language the missing text gets written in first, and where legal review happens. The product is built translation-ready either way.
Q11 — Money. (i) The three price tiers (€4–8 / €15–30 / €120–300 per user/month) have never been tested with a buyer — validate Tier 1 in the pilot, treat the rest as indicative. (ii) Your two decks define Tier 2 differently: message a real therapist vs smarter automatic personalisation. Different costs, different value story, different price. Settle the ladder before quoting anyone. Neither blocks the build.
Q12 — Measuring outcomes. The buyer wants numbers; the product refuses clinical questionnaires. The recommendation stands: use only the product's own simple ratings (stress, energy, confidence, the feedback screen) — no PHQ-9-style instruments. Confirm you're comfortable selling on that basis.
Q13 — Safety wording (with legal/clinical, but your voice). The safety machinery is built: the always-visible "Need urgent help?" card, the onboarding disclaimer, the supportive check-in prompt, and a fully private on-device word-check that can show the help card if someone types crisis language. Your decisions: (i) the wording of the disclaimer, the help card, and the supportive prompt — your voice, reviewed clinically; (ii) whether the word-check on things people write is on or off at launch. It costs nothing either way and never sends anything anywhere — the honest difference is the promise: with it off you can say "no one and nothing looks at what you write"; with it on you say "the app itself, privately on your device, may show you help resources if it spots concerning words". Recommendation: on.
Q14 — Microsoft Teams. No decision now — it's a question to ask the pilot customer.
Q15 — Employer dashboard. The metrics, privacy gates, and configuration are settled. Still wanted from you and design: how it should look, and the format of the pilot report you'll hand the HR buyer.
Q16 — The rating scale. Check-ins use tap-to-select buttons (no more sliders). We'll test 5 buttons vs 10 buttons with real people before the pilot; until then it's 10 (which keeps every existing safety threshold valid as-is). After the test, you and design pick. If you pick 5, the safety thresholds get clinically re-derived — the system physically won't let us just halve them.
Q17 — Breathing (three quick calls). (i) Session 1 currently teaches Box Breathing — which holds the breath, carries cautions for pregnancy and heart/lung conditions, and is the weakest-evidenced option in the library. Recommendation: teach Longer-Exhale Breathing instead (no holds, safest, the same exercise the app recommends everywhere else); Box stays available for people who like it. Swap or keep? (ii) Library size: launch with three (longer-exhale, grounded, box — only one new script to write) and add the other two soon after — confirm this phasing, or change it. (iii) Write the one-line question shown after an exercise ("How do you feel now?" — steadier / about the same / that felt uncomfortable) in your words. One heads-up, not a question: at launch the app recommends by evidence and your favourites; "because this worked for you before" suggestions come after the pilot. If you want them at launch, say so — it's a small change.
Q18 — Honesty about how well breathing works. The research says: some people find it helps a lot, most a little, and on average it's hard to distinguish from simply taking five minutes off. The app already frames it as "try it for a couple of weeks and see if it works for you". How plainly do you want to say the rest — quietly honest, or openly honest as a trust differentiator? Draft two or three framings; we'll test them internally.
L1 — Controller/processor and DPIA (legal — blocks pilot contract, not build). Decide formally: Minimente as data controller for employee wellbeing data, employer as neither controller nor processor of it (the recommendation — the alternative gives the employer arguable access rights to employee health data, destroying the product's core promise). Then complete the DPIA before pilot launch. Engineering inputs are ready: the full sub-processor register (ADR-01 §C.3), the RLS/aggregation enforcement design (ADR-02), the encryption and break-glass design (ADR-07), the new exercise_outcomes data class (A11.1 flagged it for the DPIA), and the retention recommendation (NFR-PRIV-09: free text active + 12 months — needs legal ratification). One forward flag: when org SSO arrives in Phase 2, the DPIA must cover what the employer's identity provider can observe (ADR-03).
C1 — Crisis-term vocabulary (clinical — blocks shipping search and the free-text prompt, not building them). Review and sign off the crisis/self-harm term list that the on-device matcher uses (for search routing always; for free-text prompting if Minni turns it on — Q13). Also review the search synonym vocabulary's clinical adjacency (e.g. "catastrophising" → thought reframing). Both are plain content files; the format is fixed, only the words are needed.
C2 — Safeguarding and personalisation thresholds (clinical). The rules are built and ship with placeholder values that the release pipeline refuses to publish until marked clinically reviewed: supportive-prompt trigger (currently: stress ≥9 on 3 of the last 5 distinct days, or any 10), high-stress suggestion (≥7), low-energy suggestion (≤4), and the "repeated high stress → professional-support signpost" rule (7 on 3 of 5 distinct days, placeholder). Confirm or replace the values; the mechanism, distinct-day counting, and destinations are settled.
C3 — Scale re-derivation (clinical — conditional). Only if the user test picks a 5-point check-in scale: re-derive every threshold in C2 for the new scale. The build system blocks arithmetic rescaling by engineering; this is yours alone. If 10-point wins, nothing to do.
C4 — The two situations breathing will not model (clinical + Minni). Confirm the addendum's refusal: no "acute anxiety" or "panic" routes exist, because they would require clinical labels the product forbids and because breathing for panic is contested-to-counterproductive in the current evidence. The nearest state, "It's all too much", routes to grounding plus help resources. Confirm, or propose a non-clinical framing — but do not resolve it by adding a clinical label.
C5 — Evidence-grade confirmation pass (clinical). One pass over the five breathing protocols and fifteen mapped situations confirming the addendum's evidence grades (several sources were verified at abstract level only). Fold into the C1 review session — one sitting covers both.
copy_status = placeholder; the CI suite including RLS negative tests and content QA (ADR-14).End of Technical Decision Record TD1.0. PRD v4.0 and Addendum A1.0 remain frozen and unmodified.