Minimente — Technical Decision Record

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.


TD0. What this document is, and the authority it claims

TD0.1 Purpose

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:

  1. Closes every technical open question as an Architecture Decision Record (ADR-01…ADR-15, §TD2), with alternatives, forcing requirements, risks, and revisit triggers.
  2. Splits every mixed question: the engineering half is closed here; the residual founder/legal/clinical half is restated as a question answerable in one sitting without technical background (§TD3, §TD4).
  3. States what is now unblocked (§TD5) and where the frozen documents contain tensions no technical decision can dissolve (§TD5.3).

TD0.2 On ownership

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).


TD1. Disposition table — all 35 open questions

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.


TD2. Architecture Decision Records

Format per ADR: Decision · Closes / gives effect to · Alternatives and why they lost · Forced or constrained by · Risks and revisit triggers · Cascades.


ADR-01 — Technology stack, hosting, and EU residency

Closes OPEN-24. Resolves the dependency in OPEN-BR-07. Unblocks §11 Phase −1 item 0c and Phase 1 item 7.

A. Decision

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:

B. Alternatives and why they lost

C. The parts of OPEN-24 the PRD demanded be addressed explicitly

  1. RLS as the enforcement mechanism. Supabase is Postgres with RLS native; the anonymity promise is enforced as declarative policies in 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.
  2. EU region for every component — enumerated in the table above. The one globally-distributed component (Cloudflare CDN) carries only the app shell and published content — no personal data ever transits it; all authenticated API traffic terminates in Frankfurt/Netherlands. Client IP addresses do touch Cloudflare (as they touch any CDN); this is disclosed in the sub-processor register below for the DPIA to assess.
  3. Sub-processor register for the DPIA (OPEN-11 input):
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
  1. iOS Web Push — the material risk to FR-NOT-03, stated plainly. On iOS/iPadOS, Web Push works only for web apps installed to the Home Screen, and iOS offers no programmatic install prompt — the user must perform a manual Share → Add to Home Screen flow that most users have never done. A daily-reminder feature (FR-NOT-03) whose delivery on the platform half the workforce carries depends on an unprompted manual install is a real product risk, not a footnote. Mitigations in v1: a guided, illustrated install flow shown once on iOS; email as the suggested default reminder channel on non-installed iOS (FR-NOT-02's fallback doing real work); the in-app inbox always. Trigger condition for native shells (the decision OPEN-24 was asked to state): if, after 4 weeks of pilot, the proportion of iOS users with a working push channel (installed + permission granted) is below half the Android/desktop rate, or iOS activation lags in a way attributable to install friction, then Capacitor shells are built in the following phase. Capacitor wraps the existing PWA (AD-02 preserved: one codebase, one renderer) and simultaneously unlocks haptic pacing (the Vibration API is absent from Safari on all Apple platforms — FR-BR-36) and reliable background notification delivery. This closes OPEN-BR-07: haptics are feature-detected, available on Android Chrome in v1, absent on iOS until the shells land, and no content instructs "feel the taps" unless the mode is active. One further risk on the record: Apple's 2024 DMA-era wobble over Home-Screen web apps in the EU was reversed, but the EU market (OPEN-09) means platform-policy risk on installed PWAs is non-zero; the Capacitor path is also the hedge against that.
  2. AD-07 and AD-09 realisation — specified in ADR-06 and ADR-08 respectively.

D. Forced or constrained by

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).

E. Risks and revisit triggers

F. Cascades

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.


ADR-02 — Tenancy, RLS policy design, and the anonymity enforcement model

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.

A. Decision

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.

  1. Roles and claims. JWTs carry 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.
  2. User-data tables (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).
  3. The aggregate path is a separate table, not a view. A nightly 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).
  4. Seat management without identity leakage (the OPEN-19 mechanism). Enrolment is by org invite code (default) or verified work-email domain (org option). Claiming a seat creates a pseudonymous 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.
  5. Per-org configuration (OPEN-22 engineering half). 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.

B. Alternatives and why they lost

C. Forced or constrained by

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).

D. Risks and revisit triggers

E. Cascades

ADR-14 (RLS test suite), ADR-09 (safeguarding config destination), Phase 1 item 8, Phase 3 item 16.


ADR-03 — Identity and authentication

Completes the OPEN-19 closure. Gives effect to NFR-SEC-03.

A. Decision

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).

B. Alternatives and why they lost

C. Risks

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.


ADR-04 — Content pipeline: source of truth, schema encoding, publish validation

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.

A. Decision

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:

  1. Validation: screen types against the §6.0 library; every 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.
  2. Build: compile to an immutable, hash-versioned JSON content bundle per locale, containing only 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).
  3. Activate: upload the bundle to Cloudflare Pages/static storage; write a 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.

B. Alternatives and why they lost

C. Risks and revisit triggers

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.

D. Cascades

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).


ADR-05 — Offline and sync

Gives effect to NFR-PLAT-04, FR-CORE-05, FR-PROG-02/03, FR-ET-06, FR-SRCH-05 (offline search).

A. Decision

B. Alternatives and why they lost

C. Risks

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.


ADR-06 — Notifications, timers, and the wall-clock architecture

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.

A. Decision

Two timing regimes, explicitly separated — this is the concrete realisation of AD-07:

  1. Wall-clock timers (Focus Window, reminders, daily nudge) are server-anchored. Starting a Focus Window POSTs to the API, which stores 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).
  2. Sub-second pacing (breathing cycles) is purely local, driven by 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.

B. Alternatives and why they lost

C. Risks and revisit triggers

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.


ADR-07 — Free-text encryption, key separation, and telemetry scrubbing

Gives effect to AD-05, NFR-SEC-01/02/05/06, FR-DATA-02.

A. Decision

B. Alternatives and why they lost

C. Cascades

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.


ADR-08 — Client-side search: concrete build

Gives effect to AD-09, FR-SRCH-01…07, FR-TK-10, FR-BR-45. Closes the engineering half of OPEN-27.

A. Decision

B. Alternatives and why they lost

C. Risks

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.


ADR-09 — Safeguarding and personalisation rule mechanics

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.

A. Decision

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).

  1. One signposting component ("Need urgent help?") rendering the org-overlaid crisis resources from ADR-02 §A.5, mounted on: Home, Emergency Tools, "Right now", every breathing surface (FR-BR-26.2), the search crisis-pin (NFR-SAFE-07), and the onboarding disclaimer flow (NFR-SAFE-01). One component, one config source, no copies to drift.
  2. Rules as data, one engine. PR-01/02/03 and NFR-SAFE-03 are rows in a 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).
  3. Distinct-day evaluation (NFR-SAFE-03 v4 amendment): the engine evaluates check-in predicates over distinct calendar days in the user's timezone, implemented once in the engine, property-tested (multiple same-day completions collapse to one day's signal).
  4. PR-03 encoded, not invented: condition shape 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.
  5. The crisis-term evaluator is built once and built regardless. A local, normalised term-list matcher in 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.
  6. Employer-notification impossibility (NFR-SAFE-05): safeguarding tables sit behind the ADR-02 wall — no org-side read path exists, and the aggregation job's SQL never touches them. Verified by the ADR-14 pgTAP suite.
  7. FR-BR-25/26's declared breathing signals emit into 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.

B. Alternatives and why they lost

C. Cascades

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.


ADR-10 — Check-in control implementation and scale configuration

Closes the engineering half of OPEN-26. Gives effect to NFR-A11Y-02, FR-STD-02, the §7.3/§8.3 scale-coupling notes.

A. Decision

B. Residual

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.


ADR-11 — Iconography and asset policy

Closes the engineering half of OPEN-05. Gives effect to NFR-A11Y-06, §6.3's icon recommendation.

A. Decision

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.


ADR-12 — Audio-optional shipping

Closes the engineering half of OPEN-15. Gives effect to AD-06, NFR-A11Y-03, UX-05, FR-BR-51.

A. Decision

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.


ADR-13 — Breathing recommender: v1 scope and thresholds

Closes OPEN-BR-04 and OPEN-BR-05. Gives effect to FR-BR-07…18, FR-BR-50, AD-BR-03.

A. Decision

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"

B. Why this is engineering's call, and the honest caveat

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.


ADR-14 — Test and CI strategy

Gives effect to FR-BR-50, §11 item 18, NFR-A11Y-01 verification, NFR-SEC-04 verification, AD-BR-03's testable determinism.

A. Decision

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:

  1. RLS negative tests (pgTAP, against an ephemeral local Supabase): for every user-data table, assert the 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.
  2. Rules golden files (FR-BR-50, extended to §7.3/§8.3 rules): fixed (history, need-state, check-in) inputs → expected (action, reason-key) outputs, versioned with the rule content; any behavioural rule edit must change the golden file in the same commit. Plus a client/server determinism property test: the same engine build runs the same fixtures in a browser context and in Node and must agree (AD-BR-03).
  3. Content CI (§11 item 18, automated): placeholder-copy detection, mojibake detection, locale leakage, FR-BR-49 rule validation, scale-version pinning (ADR-10), emoji-in-icon-field rejection (ADR-11), clinically_unreviewed release gating (ADR-09).
  4. Accessibility and offline E2E (Playwright): axe-core scans on every screen type; keyboard-only completion of a full session; 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).

ADR-15 — Post-programme module revisit semantics

Closes the engineering half of OPEN-14 (FR-POST-04's residual detail).

A. Decision

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.


TD3. Residual questions for Minni

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.


TD5. What is now unblocked — and what still is not

TD5.1 An engineer can start the moment this document is accepted

TD5.2 Still not reachable, and by whom

TD5.3 Corners no technical decision can dissolve — stated for the record

  1. iOS daily reminders on a PWA. FR-NOT-03 vs AD-02 is a genuine platform conflict: Apple gates Web Push behind manual Home-Screen installation. ADR-06 mitigates (guided install, email fallback, in-app inbox) and ADR-01 defines the measured Capacitor escape hatch — but v1 iOS reminder reach will be structurally worse than Android/desktop, and the pilot must measure it rather than hope.
  2. "Anonymous" is the wrong word. Magic-link auth means the system holds an email address; encryption is server-side (ADR-07) because cross-device resume requires it. The true, strong, defensible claims are: your employer cannot see your individual data — enforced by the database and tested in CI and what you write is encrypted, and any staff access is break-glass and audited. Copy and marketing must never say "anonymous" or "we cannot read it" unqualified. This is a wording discipline, not a fixable gap.
  3. The passive safety signal keeps thinning. Every v4/A1 surface that lets a user benefit without checking in ("Right now", Toolkit, breathing, search) reduces NFR-SAFE-03's data. The frozen documents already accepted the consequence — always-visible signposting is the primary net — but §8.3's eventual detection design must be built knowing check-ins are a shrinking window, and no architecture choice changes that.
  4. Small orgs see an empty dashboard. k≥10 plus weekly granularity means a 12-seat pilot org may see "not enough data" for weeks. FR-ORG-02 turns this into a trust asset, but sales must set the expectation — a pilot below ~25 seats will produce a thin dashboard story for the buyer.
  5. Content remains the critical path. Nothing in this document shortens Q4/Q5/C1. Engineering can now build everything; the product still cannot launch until roughly three emergency-tool scripts, the safeguarding copy, and the vocabularies exist. That was v4's Top Risk 1 and it is unchanged — merely no longer hidden behind an unratified stack.

End of Technical Decision Record TD1.0. PRD v4.0 and Addendum A1.0 remain frozen and unmodified.