# 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? User (extended) profile ADR-0014 + tone 'direct' | 'gentle' | 'motivational' + tip_kinds_json jsonb: allowed tip kinds (stable globals) UserPreference profile ADR-0014 user_id, scope, key (pk) scope 'orchestrator' | 'agent:' value_json open-ended; agent validates against its pref_schema on read source 'user' | 'inferred' (inferred never overwrites user) updated_at UserConsent profile ADR-0014 user_id, consent_key (pk) consent_key 'data:todoist' | 'data:calendar' | 'agent:focus-area' | ... granted_at revoked_at? null = currently active UserContext profile ADR-0014 user_id, name (pk) 'work' | 'home' | 'vacation' | user-named active manual toggle in M2; auto-inference per agent in #112-#116 schedule_json? optional: when this context is active created_at AgentOutput recommender ADR-0013 id (pk) user_id agent_id e.g. 'overdue-task' (matches a manifest) prompt_text snippet for the orchestrator prompt signals_snapshot jsonb: inputs the agent consumed computed_at, expires_at computed_at + manifest.ttl_sec agent_version bump to invalidate cached outputs on logic changes 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 "v4-orchestrator" (ADR-0013) | legacy bandit names retained for history policy_version candidate_source "todoist" | "advice.library" | "agent-orchestrator" | ... context_snapshot jsonb: features + agent snippets 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.