Páginas · Health Debug
Phase 1 — ToGO Backend & Database Plan
Project: Health Debug (healthdebug.com) — greenfield build
Phase: 1 of 6 (ToGO backend + PostgreSQL + auth)
Status: Planning only. Implementation starts only after the UI design (Figma / Claude design) is finalized. Items that depend on that design are flagged inline as PENDING DESIGN.
Reference: The older togo-based codebase at github.com/fadymondy/health-debug is reference only — nothing is ported wholesale.
Purpose
This document plans the Phase 1 backend: the ToGO (Go, API-first) service that is the Single Source of Truth (SSOT) for all temporal and protocol state logic, backed by PostgreSQL (ToGO postgres image), with TOGO auth for identity. Every client built in later phases (React web, Chrome extension, Swift, Kotlin, Electron) is a presentation and sensor-collection layer only — clients never compute or hardcode protocol business logic. This plan covers project structure, auth integration, the sqlc + Atlas workflow, the strict NOT NULL schema rule, the expand-contract migration playbook, a schema sketch for users/auth and the Hydration Engine, REST API conventions, the protocol-engine module layout, and the testing strategy.
Non-Negotiable Design Constraints (restated for the backend)
These two product opinions shape backend design decisions throughout this document:
- The Protocol is Categorical, Not Quantitative. Inputs are evaluated by WHAT they are, not HOW MUCH. Food is strictly
SafeorTrigger-bearing(by Trigger Family). Consequence for schema/engine design: food-related tables carry category membership, not quantity/portion columns, and no quantitative AI deduction logic exists anywhere in the food path. (Hydration's 250ml units are not an exception to this opinion — they are a fixed unit of logging, not a quantitative deduction.) - The App NEVER Invents Medical Certainty. The AI layer (BYOK, proxied server-side) is strictly prohibited from diagnosing, prescribing, or guessing drug categories; its sole job is behavioral pattern analysis, and final output must always defer to "Consult your doctor." Consequence: the AI proxy is a separate module with enforced server-side prompt/output constraints, and no engine ever produces a "diagnosis"-shaped state.
1. Proposed ToGO Project Directory Structure
API-first Go service booted through the ToGO Microkernel (config, hooks, plugin registry). Feature areas register as plugins with the kernel; the kernel owns lifecycle, config loading, and hook dispatch.
Note: the exact scaffold conventions of the ToGO framework (file names, plugin interface signatures, kernel boot API) must be confirmed against the ToGO framework itself before implementation. The tree below is the proposed shape, not a claim about ToGO's generated layout.
Notes:
- One plugin per engine (decided 2026-09-01). Every protocol engine is packaged as its own ToGO plugin registered with the kernel's plugin registry: the plugin owns its routes, schema/queries, config, and hook subscriptions, so each engine can be developed, enabled, disabled, and tested in isolation. Auth and aiproxy are likewise plugins. Hooks carry cross-cutting concerns (e.g., an
entry.loggedhook that later phases — smart alerts, future gamification badges — can subscribe to without modifying engine code). The concrete hook names are a Phase 1 implementation decision. - Each engine plugin's
core/package has zero imports ofstoreorhttp. This is the enforcement point for "engines are pure state machines" (§8): the plugin wrapper does the wiring; the core stays pure. aiproxyscope in Phase 1: the execution sequence defines Phase 1 as backend + PostgreSQL + auth; whether the BYOK proxy ships inside Phase 1 or is only stubbed (table + plugin slot reserved) is undefined in the spec — flagged as an open question. Either way, the invariant is fixed now: keys are encrypted in the backend, ALL AI calls are proxied server-side, and no API key ever reaches a client.
2. TOGO Auth Integration (Identity)
- TOGO auth is the identity provider. The backend does not roll its own credential storage, password hashing, or session issuance — it integrates the TOGO auth component via the kernel's plugin registry.
- Middleware boundary: an auth middleware in
internal/http/middlewarevalidates the credential on every request and injects the authenticated user identity into the request context. All/api/v1/*routes except health/auth endpoints require it. - App-side user row: the application keeps its own
usersrow keyed to the TOGO auth identity (see schema sketch, §6) so that engine state, entries, and future doctor/B2B relationships have a stable local foreign key. Which tables TOGO auth itself provisions (credentials, sessions, tokens) vs. which the app owns is to be confirmed against TOGO auth's actual schema — the plan assumes TOGO auth owns credentials/sessions and the app owns the profile/domain row. - Token format (JWT vs. opaque), refresh semantics, and supported sign-in methods (email/password, social, passkeys) are undefined in the spec and determined by what TOGO auth provides — open question.
- Future-state doctor and B2B roles are not in Phase 1 scope, but the
userssketch reserves nothing special for them — role/permission modeling will be added later via expand-contract migrations rather than speculatively now.
3. sqlc + Atlas Workflow
Single flow for every schema change: Atlas migration → sqlc regeneration → handler/engine wiring. No step is ever skipped or reordered.
Rules:
db/schema/is the declarative source of truth for Atlas;db/migrations/is the append-only, generated (then human-reviewed) history. Migrations are never edited after merge.- Generated code in
internal/store/sqlc/is committed and never hand-edited; CI fails ifsqlc generateproduces a diff. - Because every column is
NOT NULLwith a default (§4), sqlc generates plain Go types (string,int32,time.Time) instead ofsql.Null*/pointer types — this is a deliberate compounding benefit, not a coincidence.
4. Strict Schema Rule: Every Column NOT NULL With a Defined Default
The rule: every column in every table is declared NOT NULL and carries an explicit DEFAULT.
Rationale:
- Eliminates tri-state logic.
NULLintroduces a third truth value into every comparison and a "missing vs. empty vs. zero" ambiguity into every read. For a system whose entire job is deterministic protocol state machines, ambiguity in stored state is a correctness hazard. Absence, where it is meaningful, is modeled explicitly (a sentinel default like''/0, a status enum value, or a separate table row) — never asNULL. - Cleaner generated code. sqlc maps
NOT NULLcolumns to plain Go types. Engines receive concrete values, not nullable wrappers, keeping the pure state-machine functions (§8) free of nil-handling branches. - Expand-contract becomes mechanically safe. A new column with a
NOT NULL DEFAULTcan be added while old application code is still running and still inserting rows without that column — Postgres fills the default. This is the property that makes the expand phase of §5 zero-downtime by construction. - Defaults are documented semantics. The default value is a stated, reviewed decision ("an unlogged source is
'unknown'", "a fresh counter is0") living in the schema itself, instead of being scattered across application code.
5. Expand-Contract Migration Playbook
The rule: all schema changes strictly follow the expand-contract pattern. No migration may break code that is currently deployed; old and new code must both run correctly against the schema at every intermediate step. atlas migrate lint in CI is the enforcement gate for accidental contract-first changes.
Playbook (the only allowed shape of a breaking change):
| Step | Kind | Action | Deployed code that must keep working |
|---|---|---|---|
| 1 | Expand (migration) | Add the new column, NOT NULL with a default (§4 makes this safe) | Old code, unaware of the column |
| 2 | Deploy (code) | Dual-write: new code writes both old and new columns | Old + new code side by side |
| 3 | Backfill (migration/job) | Batched UPDATE fills the new column for historical rows | New code |
| 4 | Deploy (code) | Switch reads: sqlc queries updated to read the new column; regenerate; handlers switch | New code only |
| 5 | Verify | Confirm no reader/writer of the old column remains (query audit + observation window) | — |
| 6 | Contract (migration) | Drop the old column | New code |
Concrete example sequence — hypothetical future change: replacing a raw hydration_entries.volume_ml column with the canonical units column (1 unit = 250ml):
- Expand: migration adds
units(smallint NOT NULL DEFAULT 0) tohydration_entries. Old code keeps inserting onlyvolume_ml; the default keeps inserts valid. - Dual-write deploy: handlers write both
volume_mlandunitson every new entry. - Backfill: batched migration job sets
units = volume_ml / 250for all historical rows (small batches to avoid long locks). - Switch reads: sqlc queries are rewritten against
units;sqlc generate; handlers and the hydration engine consumeunitsonly. - Verify: confirm zero remaining reads/writes of
volume_mlindb/queries/and code; hold an observation window. - Contract: final migration drops
volume_ml.
Renames follow the same shape (add new → dual-write → backfill → switch → drop old); direct ALTER ... RENAME on a live column is prohibited.
6. PostgreSQL Schema Sketch — Users/Auth + Hydration Engine
Sketch only: tables and key columns, not full DDL. Every column shown is NOT NULL with a default per §4. All timestamps are timestamptz; all IDs are UUIDs with generated defaults. Schema sketches for the other seven engines will follow these same rules and are deliberately deferred to the detailed engine planning pass before implementation — they are not defined here.
6.1 Users / Auth
Owned by TOGO auth (to be confirmed against TOGO auth's actual schema): credentials, sessions/tokens, verification state.
users — app-owned domain row, keyed to the TOGO auth identity:
| Column | Sketch | Notes |
|---|---|---|
id | uuid, default generated | app-side PK; FK target for all engine tables |
auth_id | text/uuid, default ''-style sentinel until linked | reference to the TOGO auth identity; exact type follows TOGO auth |
email | text, default '' | identity/display; source of truth may live in TOGO auth (confirm) |
display_name | text, default '' | PENDING DESIGN — final profile fields depend on the finalized UI profile/onboarding screens |
locale | text, default e.g. 'en' | drives ICU/i18n incl. Arabic RTL responses; default value is a decision to record |
timezone | text, default TBD | required to resolve "day" boundaries and pre-sleep/post-wake windows server-side; default value and day-boundary policy are an open question (§Open Questions) |
theme_preference | text enum-ish, default TBD | dark/light per color spec; PENDING DESIGN — whether theme is stored server-side or per-client is a UI-phase decision |
created_at / updated_at | timestamptz, default now() |
6.2 Hydration Engine
Protocol facts (fixed by spec): water logged in 250ml units; daily cap 5000ml (= 20 units); 30-second cooldown between entries, enforced server-side.
hydration_entries — append-only log, one row per accepted entry:
| Column | Sketch | Notes |
|---|---|---|
id | uuid, default generated | |
user_id | uuid, default sentinel + FK users.id | |
units | smallint, default 1 | 1 unit = 250ml; canonical storage is units, never free-form ml. Whether a single entry may carry >1 unit is undefined in the spec — open question; PENDING DESIGN (depends on whether the UI offers a multi-tap/bulk control) |
logged_at | timestamptz, default now() | server-received time — the cooldown and cap are evaluated against server time, never client-claimed time |
source | text, default 'unknown' | which client class logged it (web/extension/apple/android/desktop); enum values finalized when clients exist |
idempotency_key | text, default '' | pairs with a uniqueness constraint on (user_id, idempotency_key) for non-empty keys; see §7 idempotency |
created_at | timestamptz, default now() |
Derived state (no extra table required initially): the day's total (cap check against 5000ml) and the last-entry timestamp (30s cooldown check) are computed by query over hydration_entries inside the entry transaction. A per-day rollup table is a possible later optimization — introduced only via expand-contract if query cost demands it, not speculatively.
Server-side enforcement sketch (transaction on POST entry):
Concurrency: the cooldown/cap read-then-insert runs under a per-user serialization mechanism (e.g., transactional advisory lock per user or equivalent) so two simultaneous requests cannot both pass the checks — the exact mechanism is an implementation decision, the invariant (no double-accept) is fixed and covered by an integration test (§9).
7. REST API Conventions
- API-first, JSON over HTTPS. The ToGO REST API is the SSOT; every client speaks only this API.
- Versioning: URL-path versioned — all Phase 1 routes under
/api/v1/. Breaking API changes require a new version prefix; withinv1, changes are additive only (the API-level mirror of expand-contract). - Auth header:
Authorization: Bearer <token>with the credential issued by TOGO auth (token format per §2 — follows TOGO auth, currently an open question). - Error model (proposed shape, to finalize at implementation): structured JSON error envelope on every non-2xx response — a stable machine-readable
code(e.g.,hydration.cooldown_active,hydration.daily_cap_reached), an HTTP status, a human-readablemessage(server-localized via ICU Message Format, honoring the user's locale incl. Arabic — never string-concatenated), and adetailsobject for field-level issues. Protocol-rule rejections (cooldown, cap, GERD window, caffeine block) are first-class, well-known error codes, because clients must render them without local protocol knowledge. PENDING DESIGN — the exact set of user-facing rejection messages and how much explanation travels in the error vs. is composed client-side depends on the finalized UI copy. - Idempotency for logging endpoints: every state-mutating logging endpoint (hydration entries, and later food, medication, cycle, pomodoro events) accepts an
Idempotency-Keyheader. Same user + same key ⇒ the original result is replayed, no duplicate row. This is essential because sensor-collecting clients (wearables, extension, mobile on flaky networks) will retry. Key retention window: implementation decision, to be documented with the endpoint. - Time: clients report events; the server's clock and the server-stored user timezone decide all temporal protocol logic (cooldowns, windows, day boundaries). Clients never pre-compute protocol verdicts.
Phase 1 endpoint sketch (hydration + identity; other engines' surfaces planned in their own passes):
| Method | Path | Purpose | Notes |
|---|---|---|---|
| — | /api/v1/auth/... | sign-in/sign-up/session | delegated to / shaped by TOGO auth (§2) |
GET | /api/v1/me | current user profile | fields PENDING DESIGN |
POST | /api/v1/hydration/entries | log water (250ml units) | requires Idempotency-Key; enforces 30s cooldown + 5000ml cap server-side |
GET | /api/v1/hydration/today | day state: total ml, units, remaining to 5000ml cap, cooldown remaining | response shape PENDING DESIGN — must carry whatever the timer/progress UI (neon-glow active timer states) needs, finalized against the design |
GET | /api/v1/hydration/entries?date=... | history for a day | PENDING DESIGN — pagination/range shape depends on history UI |
GET | /healthz | liveness for ops | unauthenticated |
8. Engine Module Layout — One ToGO Plugin Per Protocol Engine
All 8 engines live in Phase 1 as backend state machines (they are the SSOT), and each ships as its own ToGO plugin on the Microkernel plugin registry (decided 2026-09-01) — independently registrable, manageable, and implementable. Their HTTP surfaces and schemas beyond hydration are planned in follow-up passes, but the plugin layout and purity rules are fixed now.
Each engine plugin directory contains: plugin.go (kernel registration — routes, hook subscriptions, config, migration manifest), core/ (the pure state machine, subject to the purity rules below), and the engine's sqlc query files. The kernel boots whichever engine plugins are registered; enabling or disabling an engine is a registry change, not a code deletion.
Purity rules (enforced by review + the import graph):
- Every engine package exposes pure state-machine functions: inputs are (current state, incoming event, current time), outputs are (new state, decision/effects, typed protocol error). Signature style, conceptually:
Decide(state, event, now) → (state', decision, err). - No I/O in engine packages. No database, no HTTP, no clock reads (
nowis always a parameter, supplied viainternal/clockat the call site), no logging side effects. This is what makes engines testable without infrastructure (§9). - Handlers are thin orchestrators: decode request → load state via
internal/store→ call engine → persist within a transaction → encode response. All protocol numbers (30s, 5000ml, 90min, 4h, 60min, 3 cycles) live in engine packages only — never in handlers, never in clients. - Engine decisions emit kernel hooks (e.g., entry accepted/rejected) so later concerns — smart alerts across devices, future doctor-prescribed Medical Restrictions dynamically altering engine parameters, future gamification — attach without touching engine code. The Medical Restrictions influence point is designed for now (engine parameters arrive as part of loaded state, not constants baked inline) but not implemented in Phase 1.
- Engines that consume wearable/IoT signals (kineticpomodoro's movement detection) receive those signals as events already pushed to the API by clients reading HealthKit / Health Connect on-device — the engine itself never talks to device SDKs.
9. Testing Strategy
Layer 1 — Engine unit tests as the executable spec.
- Each engine package carries table-driven tests that encode the protocol numbers literally, so the test file reads as the specification: hydration — entry at t+29s rejected, t+30s accepted, 20th unit (5000ml) accepted, 21st rejected, cap resets at day boundary; caffeineblock — caffeine at wake+89min rejected, wake+90min allowed; gerdwindow — inside the 4-hour pre-sleep window only water/chamomile/anise pass, everything else rejected; medicationgrace — logging inside the 60-minute window valid; cycle — no prediction until 3 consecutive logged cycles, and fail-safe (no prediction) on irregularity; contraceptive — daily pill vs monthly injection vs implant follow distinct schedules; triggerfamilies — classification is strictly categorical (Safe vs Trigger-bearing per family), with an explicit test asserting no quantity input affects the verdict.
- Pure functions + injected
now⇒ no mocks, no containers, sub-second suite; run on every commit. - A change to any protocol number is impossible without a visible test diff — the tests are the guardrail for the "categorical, not quantitative" and "no invented medical certainty" opinions at the engine level.
Layer 2 — API integration tests against real PostgreSQL.
- Run against a real Postgres instance from the ToGO postgres image (containerized per test run), never against mocks or SQLite — the strict NOT NULL/default rules, constraint behavior, and transaction semantics must be tested for real.
- Pipeline per run: fresh database → apply the full Atlas migration chain (this doubles as a migration test) → verify
sqlc generatecleanliness → run suite. - Priority scenarios: full auth round-trip through TOGO auth integration; hydration
POSThappy path; cooldown rejection with correct error code; daily-cap rejection; idempotency replay (same key twice ⇒ one row, same response); cooldown race (two concurrent posts ⇒ exactly one accepted, proving the §6.2 serialization invariant); localized error messages for a user with Arabic locale (ICU path, no concatenation). - Expand-contract rehearsal: for each breaking change, an integration test applies the expand migration against a database still receiving "old-code-shaped" writes to prove both code generations coexist (§5 step guarantees).
CI gates (summary): engine unit suite → atlas migrate lint → fresh-DB migration apply → sqlc generate diff check → integration suite. All green before merge.
Open Questions
- ToGO framework specifics: exact microkernel boot API, plugin interface, hook bus conventions, and recommended scaffold layout — to be confirmed against the ToGO framework before the tree in §1 is finalized.
- TOGO auth specifics: provisioned tables, token format (JWT vs. opaque), refresh/session semantics, and supported sign-in methods — the spec does not define them.
- Day-boundary policy: the 5000ml daily cap, the 90-minute post-wakeup caffeine block, and the 4-hour pre-sleep GERD window all need a server-side definition of the user's day/wake/sleep anchors (stored timezone? logged wake/sleep events? wearable-derived?). The spec fixes the durations but not the anchor sources.
- Hydration entry granularity: may one API entry carry multiple 250ml units, or strictly one unit per entry (with the 30s cooldown effectively rate-limiting intake logging)? Spec is silent; also PENDING DESIGN (depends on the logging control in the UI).
- BYOK AI proxy phase placement: Phase 1 is defined as backend + PostgreSQL + auth; whether the
aiproxymodule ships functional in Phase 1 or is only reserved (schema + plugin slot) is not specified. - Error envelope final shape and the catalog of protocol rejection codes/messages — proposal in §7 needs sign-off, and user-facing copy is PENDING DESIGN.
- Server-side vs. client-side theme/profile fields (
theme_preference, profile fields) — PENDING DESIGN, blocked on the finalized UI. - Schema sketches for the remaining 7 engines — deliberately deferred to per-engine planning passes before implementation; they will follow §4/§5 rules and the §8 layout.