Files
cesnimda 4ce2df0a2b
CI / backend (push) Successful in 52s
CI / frontend (push) Successful in 14s
Deploy Staging / deploy (push) Successful in 18s
CI / backend (pull_request) Successful in 52s
CI / frontend (pull_request) Successful in 15s
Security / secrets (push) Successful in 4s
Security / dependencies (push) Successful in 55s
Security / secrets (pull_request) Successful in 4s
Security / dependencies (pull_request) Successful in 54s
docs: discovery blueprint + multi-provider design (#8)
2026-07-01 19:59:53 +02:00

80 lines
6.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 03 — Database Design (Part 6)
Schema for a **small-team, self-hosted, one-org** platform: multi-provider accounts per
user, a normalised email store scaling to **millions of messages**, settings, feature
flags, and audit. PostgreSQL + EF Core (+ pgvector for the blueprint's semantic tier).
## Entity map
```
users ─┬─< accounts ─┬─< provider_tokens (1:1, encrypted)
│ ├─< email_threads ─< email_messages ─┬─< attachments
│ │ └─< message_labels >─ labels
│ └─ sync_state (cursor)
├─< user_settings (1:1)
└─< audit_logs (actor)
system_settings (singleton) feature_flags
```
## Tables
### Identity & access
- **`users`** — the app identity (from OAuth).
`id (uuid pk) · primary_email (citext, unique) · display_name · avatar_url · role (enum: Admin|Member) · status (enum: Active|Suspended) · created_at · last_login_at`
*First-ever user is bootstrapped as **Admin** (see [05](05-admin-system.md)).*
- **`accounts`** — a connected mailbox **and** a login identity (OAuth-is-login).
`id (uuid pk) · user_id (fk) · provider (enum: Google|Microsoft|Imap) · provider_account_id (text, the OAuth 'sub' — immutable) · email (citext) · display_name · is_login_identity (bool) · status (enum: Active|ReauthNeeded|Disabled) · scopes (text[]) · added_at · last_sync_at`
**Unique:** `(provider, provider_account_id)` → resolves an OAuth login to exactly one account→user; prevents the same mailbox linking twice.
- **`provider_tokens`** — 1:1 with `accounts`, **encrypted at rest** (Data Protection API).
`account_id (pk/fk) · access_token_enc (bytea) · refresh_token_enc (bytea) · expires_at_utc · token_type · rotated_at`
*Never logged; see [06](06-security-model.md).*
- **`sessions`** — server-side app sessions (opaque cookie).
`id · user_id · created_at · expires_at · ip · user_agent · revoked_at` (supports "sign out everywhere" + admin revoke).
### Email (normalised, provider-agnostic)
- **`email_threads`** — per account.
`id (uuid pk) · account_id (fk) · user_id (denorm) · provider_thread_id · subject · participants (jsonb) · message_count · last_message_at`
**Unique:** `(account_id, provider_thread_id)`.
- **`email_messages`** — the big table.
`id (uuid pk) · account_id (fk) · user_id (denorm) · thread_id (fk) · provider_message_id · sender_id (fk) · subject · snippet · body_text · sent_at_utc · received_at_utc · size_bytes · flags (unread/starred/important/in_inbox/trashed as bits or bools) · has_attachments · category (enum) · has_list_unsubscribe · supports_one_click_unsub · search_vector (tsvector, generated, weighted) · embedding (vector(768), nullable) · created_at`
**Unique:** `(account_id, provider_message_id)` (idempotent upsert).
- **`labels`** (`id · account_id · provider_label_id · name · type`) + **`message_labels`** (`message_id · label_id`, pk both).
- **`attachments`** (`id · message_id · filename · mime · size · provider_attachment_id · content_text nullable` for future OCR/search).
- **`senders`** / **`domains`** (existing) — kept, scoped per user (or global with per-user stats materialised in `sender_importance`).
### Settings, flags, audit
- **`user_settings`** — 1:1 with `users`.
`user_id (pk) · theme (enum: system|light|dark) · inbox_layout (jsonb) · notifications (jsonb) · ai_prefs (jsonb) · provider_prefs (jsonb) · updated_at`
- **`system_settings`** — singleton (org-wide, admin-managed).
`id (const) · maintenance_mode (bool) · default_theme · registration_open (bool) · updated_by · updated_at` (+ arbitrary `values jsonb` for growth).
- **`feature_flags`** — the flag system (drives AI gating).
`key (pk text) · enabled (bool) · scope (enum: SystemOnly|UserOverridable) · description · rollout (jsonb, e.g. per-role) · updated_by · updated_at`
Seeded flags: `ai.enabled`, `ai.semantic_search`, `ai.ask_inbox`, `provider.google`, `provider.microsoft`, `provider.imap`, `maintenance.readonly`.
- **`audit_logs`** — admin + security events.
`id (bigserial) · actor_user_id (fk, nullable for system) · action (text) · target_type · target_id · metadata (jsonb) · ip · created_at`
Append-only; indexed on `(created_at)`, `(actor_user_id)`, `(action)`.
## Indexing & performance
| Index | Column(s) | For |
|-------|-----------|-----|
| GIN | `email_messages.search_vector` | FTS |
| GIN `pg_trgm` | sender/subject | fuzzy (fixes non-sargable `.Contains`) |
| HNSW | `email_messages.embedding` | semantic k-NN |
| btree | `(user_id, sent_at_utc desc)`, `(account_id, sent_at_utc desc)`, `(thread_id)` | keyset pagination, scoping, threading |
| unique | `(account_id, provider_message_id)`, `(provider, provider_account_id)` | idempotency, login resolution |
## Scalability (millions of emails, multi-account, incremental)
- **Multi-account** = first-class via `accounts`; every email carries `account_id` + denormalised `user_id` (so cross-account per-user search is a single indexed scan).
- **Millions of rows:** keyset (cursor) pagination, top-N-by-score ranking, `pg_trgm`/GIN/HNSW indexes. If a single user exceeds ~12M messages, **list-partition `email_messages` by `account_id`** (or hash by `user_id`).
- **Incremental sync:** per-account `sync_state` cursor (`historyId` / `deltaLink` / `uidvalidity+modseq`) → only deltas fetched; idempotent upserts on the unique key.
- **Idempotency & resumability:** all sync writes keyed on `(account_id, provider_message_id)`; cursor advanced atomically with the batch.
## RBAC in the schema
Small-team model = a `role` column on `users` (`Admin|Member`) — no separate roles/permissions tables yet. **Extensible** to `roles`/`permissions`/`org_id` if this ever grows to multi-tenant, without reshaping the email tables.
## Migration from today (see full [Migration Guide](12-migration-guide.md))
1. Create `users` from existing OAuth identity; set first user = **Admin**.
2. Create one `Google` **`account`** per existing user; move current Gmail tokens → `provider_tokens`.
3. Backfill `email_messages.account_id`/`user_id`, rename/extend from the current `Email` table; widen `search_vector`; add nullable `embedding`.
4. Add `user_settings`, `system_settings`, `feature_flags` (seed `ai.enabled` from the current `Ai:Mode`), `audit_logs`.
5. All additive + backfill; no destructive step — safe to run behind a maintenance window.