Skip to content

ADR-0038: Daymate Data-Encryption & Privacy Doctrine (Three-Axis, DB-Only Layer-2 Ceiling)

Proposed Daymate Territory-Specific

Date: 2026-07-09 Status: Proposed — not ratified. DAY-0279's explicit purpose is external expert review; this ADR captures the war-room-corrected decision frame so the review starts from a sound threat model. Acceptance waits on (1) the external expert review and (2) the three operational confirmations (Open Questions 1–3). Commander to ratify thereafter. Territory: daymate (daymate-api server + daymate-app device). AVG (Art. 9 special-category + Art. 32 appropriate measures); ISO 27001 aspirational. Ticket: DAY-0279 (Back-to-code Kendo, project 8). Campaign: campaigns/daymate/2026-07-09-data-encryption-privacy-policy-day0279.md. Evidence: Surveyor / Sapper / Adjutant field reports (2026-07-09) under reports/daymate/field/. Related: DAY-0117 (device SQLCipher), DAY-0111 (reflection/notes, unbuilt), DAY-0192 (feedback scrub). Principle #8 (HTTP timeouts — adjacent Art. 32), Principle #10 (rotation-invariant cache keys — same encrypted-cast arch-test family, queue #24).

Context

Daymate serves users largely in a zorg-/begeleidingscontext, so every free-text field a user types is potentially AVG Art. 9 (health) data — an activity title ("naar de psychiater"), a task, a note, a reflection — not only nominally-"health" fields. Mood is merely the most explicit structured example; the bulk of the risk is in the open strings. Two databases are in play: the device App-DB (drift/SQLite) and the server API-DB (networked managed MySQL).

The governing insight, confirmed on recon: "encrypted yes/no" is never one question. Each datasoort has its own threat and its own tool —

  • confidentiality of content → encryption
  • who, as an authenticated user (incl. companion), may read → authorization / consent
  • can no one silently rewrite history → integrity / tamper-evidence

Conflating these produces controls that read stronger on paper than in fact. The recon found DAY-0279's framework sound but carrying two overclaims and a mis-drawn scope axis (full analysis in the campaign report).

Decision

1. Adopt the three-axis separation (confidentiality / authorization / integrity) as daymate's standing frame for any data-protection question. Encryption is never proposed as a substitute for authorization or integrity.

2. Device (App-DB) — full-DB encryption is the main control. SQLCipher over the drift DB, key in platform secure storage (DAY-0117). This is genuine defense-in-depth on an untrusted device (file and key protected by different mechanisms), with the honest caveat that on a rooted/jailbroken device both file and key may be reachable — so it is the main control, not an absolute one.

3. Server (API-DB) Layer 1 — at-rest is a baseline to verify, not assume. The DB is a separate networked managed host, not a Fly volume (no [mounts] in fly.production.toml; CA-pinned TLS at config/database.php:62 is a managed-MySQL fingerprint). Therefore the wish is corrected to: identify the provider, then verify that provider's at-rest posture, and ensure backups are encrypted at rest. Both are pending operational confirmation (Open Questions 1–3).

4. Server (API-DB) Layer 2 — selective per-column app-level encryption on user-entered free text, scoped to DB-only compromise.

  • Scope axis is "free text vs. structured/queryable," not "health field vs. rest." Every user-entered free-text column is in scope. Recon confirmed zero query/search/sort/unique surface on any of them, so encryption costs no feature today. The list is widened beyond DAY-0279 to include the omitted users.phone, users.name, auth_providers.device_name, feedbacks.{name,email,description} alongside activities.name / activities_v4.label / activities_v4.description.
  • reflection/notes (DAY-0111) encrypt-at-birth — build them with the encrypted cast in their create migration before any query surface can accrete. Cheapest and most sensitive.
  • Protection is explicitly bounded to DB-only compromise. The per-column key (APP_KEY-derived) is co-resident with DB_PASSWORD in one Fly secrets bundle; there is no KMS. Layer 2 therefore covers leaked DB credential, insider DBA, stolen mysqldump, raw disk — and does NOT cover a stolen secrets bundle or a compromised application server (the app holds the key and decrypts on read). This ceiling is documented so any DPIA rests on the real control, not an assumed one. Lifting it requires an external KMS the platform does not currently provide — a separate, larger decision.

5. Structured/queryable signals stay plaintext — with honest justification.

  • Mood (executions.mood, an ordinal int; Art. 9) may be left plaintext only against a named future aggregation feature — none exists in SQL today (weekly-mail renders per-row and would survive encryption). Absent a named feature, reconsider encrypting it.
  • Email is queried equality-only everywhere; leaving it plaintext is a policy call, not a technical necessity. If ever encrypted, a deterministic blind index rescues every lookup — no feature breaks. Email is not hashed (identifier + comms channel; a deterministic hash is enumerable and weak at rest).

6. Companion inzage is an authorization/consent problem, not encryption. As long as the key belongs to the server (not the individual user), companion access neither needs nor is protected by encryption. The AVG control there is the consent model: explicit koppeling-approval, category granularity (activity_updates / weekly_updates / future share_reflections / share_mood), revocability (ontkoppelen), and optional inzage-audit for Art. 9 data.

7. Audit is an integrity + retention problem, not confidentiality. Audit rows are ids/timestamps/enums that must stay queryable — do not per-column-encrypt them. Log references, not content. The real control is append-only + tamper-evidence (hash-chain), preferably an external sink so a compromised app/insider cannot rewrite history; plus a defined retention term (audit metadata is itself personal data recording a care relationship). Proportionality for an AVG-minimum territory is a Commander call, not automatic gold-plating.

What we deliberately do NOT do

  • No E2EE — incompatible with companion inzage, weekmails, content-notifications (server must decrypt).
  • No generic "encrypt every column" — ids/FK/timestamps/enums cost queryability and buy no privacy; the cut is free-text-vs-structured.
  • No email hashing — encryption + blind index if confidentiality is ever wanted.
  • No presenting Layer 2 as app-compromise or secrets-bundle protection — it is DB-only.
  • No claiming "all free text encrypted at rest" while jobs/failed_jobs.payload and sessions hold the same Art. 9 free text in cleartext blobs outside any per-column scheme (ties to DAY-0192). Either scope the claim to the modeled columns or extend scrubbing to the framework tables.

Consequences

  • Greenfield. No encrypted cast, custom cast, or key-management code exists in daymate-api today — key-rotation tooling, blind-index infrastructure, and the loss of LIKE/ORDER BY/unique on any encrypted column are all net-new surface. Raises the effort estimate; argues for treating email's blind index as a discrete later decision rather than bundling it.
  • Enforcement (Level 1). On acceptance, a Pest architecture test asserts the in-scope columns carry an encrypted cast (mirrors Principle #10 / queue #24's encrypted-cast arch test on other territories), plus an invariant that no raw-SQL sink returns decrypted data. This is the durable guarantee; a static rule alone is evadable.
  • Honest DPIA. The Layer-2 ceiling (DB-only) must appear in the DPIA verbatim; a compromised app or leaked secrets bundle exposes all plaintext regardless of Layer 2.
  • In-transit adjacency. MYSQL_ATTR_SSL_CA is unset — app↔MySQL TLS may be unpinned (Art. 32). Separate hardening ticket, tracked out of this ADR.
  • Portability. The three-axis frame and the "all free text is Art. 9" partition transfer directly to wijs (resident care PII), emmie (NEN 7510), and codebook (minors + care-data ingress). This ADR is the reference when those territories face the same question; it is not auto-applied.

Open Questions (block acceptance)

  1. DB provider identity + live co-residence confirm — one read-only fly secrets list -a daym-apiv3-production (names only) under a Daymate-org token resolves both. Trigger: Commander (or ally) one-shot run; the war-room Fly identity is not a member of the Daymate org.
  2. Provider at-rest attestation — follows Q1 (provider console, managed case).
  3. Backups — existence, destination, at-rest encryption; no app-managed backup exists in-repo, so this is a provider/operational fact. Trigger: same Daymate-org access as Q1.
  4. Expert-review dispositions — scope of "free text," the query-vs-encrypt line for any future-searchable field, blind-index operational cost for email, audit scope/retention/sink. These are the DAY-0279 expert questions; their answers may amend §Decision before ratification.

Operational-confirmation gaps 1–3 carry a scheduled-one-shot trigger (not a bare Resolve By), per operational-confirmation-signal doctrine — they wait on a run someone schedules, not on a date.

Architecture documentation for contributors and collaborators.