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)
This commit is contained in:
87
docs/architecture/data-model.md
Normal file
87
docs/architecture/data-model.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user