Files
oO/docs/architecture/data-model.md
alvis 7f173f88d3 refactor: architecture revision — modular monolith, auth-commit, event protobuf, privacy-from-day-0
- ADR-0003: modular monolith for Phase 0 with documented extraction triggers
- ADR-0004: Auth.js + OIDC-shaped boundary; dedicated provider when mobile ships
- ADR-0005: protobuf for events, OpenAPI for HTTP, schema-registry CI gate
- New architecture docs: data-model, metrics (magic proxies), privacy (Phase-0 feature)
- Prime directives updated: privacy-as-feature, modular-by-package-deployable-by-stage
- Roadmap revised: Apple OAuth deferred to M1; web push in M1; k3s intermediate; tip-kind-aware UI
- PLAN updated: Phase-0 deletion endpoint, metrics baseline, compose profiles, import-boundary lint
- License decision in README (ARR with OSS plan in Phase 5)
2026-04-13 14:36:11 +00:00

2.9 KiB

Data model

Durable entities across modules. Per-module databases/schemas own these; cross-module access is only via the module's API.

Core entities

User                 auth + profile
  id (uuid)
  created_at
  email                        (from IdP)
  preferred_name?
  deleted_at?                  soft-delete for 30-day recovery; hard-delete after

IdentityLink         auth
  user_id
  provider                     "google" | "apple"
  provider_sub                 subject from IdP
  created_at

Session              auth
  user_id
  sid (uuid)                   in JWT
  issued_at
  expires_at
  revoked_at?

Profile              profile
  user_id (pk)
  timezone
  quiet_hours                  jsonb: [{start,end,days}]
  contexts                     jsonb: [{name,predicate}]      introduced in Phase 2
  consents                     jsonb: {integration: {read,write,retain_days}}

Credential           integrations
  user_id
  provider                     "todoist" | "google_calendar" | ...
  ciphertext                   sealed-box over {access, refresh, scopes, expires_at}
  meta                         provider-specific (sync_token cursor for Todoist)
  created_at
  last_refreshed_at
  revoked_at?

Event                events
  event_id (ulid)
  user_id
  schema_version
  kind                         e.g. "signals.task.updated"
  occurred_at
  ingested_at
  payload                      protobuf bytes

TipInstance          recommender
  tip_id (ulid)
  user_id
  policy_name                  "random" | "bandit.linucb" | "remote:v3"
  policy_version
  candidate_source             "todoist" | "advice.library" | ...
  context_snapshot             jsonb: features seen at decision time
  tip                          jsonb: {kind,title,body,source,deep_link,meta}
  created_at
  shown_at?                    set when the client reports render
  reaction?                    "done" | "snooze" | "dismiss" | null
  reacted_at?
  delivery_id?                 fk if surfaced via notifier push

Delivery             notifier
  delivery_id
  user_id
  tip_id
  channel                      "webpush" | "apns" | "fcm" | "email"
  dispatched_at
  delivered_at?
  failure_reason?

Foreign-key discipline

There are no cross-module FKs. Each module owns its tables. References by id are soft; consistency is maintained by events (user-deleted → every module cascades its own cleanup).

Deletion

User.deleted_at set → a user.deletion_requested event goes out → each module soft-deletes its rows → after 30 days a scheduled job hard-deletes. Credentials are revoked at the provider (not just erased locally) on soft-delete. See privacy.md.

Replay and reproducibility

TipInstance.context_snapshot captures the exact features that produced the decision. This is what lets offline replay re-score historical tips against a new policy without touching the feature store.