Blame
|
1 | --- |
||||||
| 2 | category: reference |
|||||||
| 3 | tags: [architecture, database, multi-tenancy] |
|||||||
| 4 | last_updated: 2026-03-16 |
|||||||
| 5 | confidence: high |
|||||||
| 6 | --- |
|||||||
| 7 | ||||||||
| 8 | # Per-Wiki Database Design |
|||||||
| 9 | ||||||||
| 10 | ## Current State |
|||||||
| 11 | ||||||||
| 12 | Otterwiki uses a single shared SQLite DB (`/tmp/otterwiki_default.db`) for all tenants. The `Preferences`, `User`, and `Drafts` tables are global. The `TenantResolver` swaps `GitStorage` (the git repo path) per-request but never swaps the SQLAlchemy DB binding. |
|||||||
| 13 | ||||||||
| 14 | This hasn't caused problems because: |
|||||||
| 15 | - `ProxyHeaderAuth.has_permission()` ignores the Preferences table entirely — permissions come from proxy headers |
|||||||
| 16 | - User Management and Permissions admin panels are disabled via `PLATFORM_MODE` |
|||||||
| 17 | - The `User` table is effectively dead code under `ProxyHeaderAuth` |
|||||||
| 18 | ||||||||
| 19 | ## Why Change |
|||||||
| 20 | ||||||||
| 21 | The user wants wiki owners to manage permissions through otterwiki's existing admin UI (READ_ACCESS, WRITE_ACCESS, etc.). This requires per-wiki Preferences at minimum. Without per-wiki DBs, one wiki admin's Preferences changes affect all wikis. |
|||||||
| 22 | ||||||||
| 23 | ## Approaches |
|||||||
| 24 | ||||||||
| 25 | ### Option 1: Per-request DB swap |
|||||||
| 26 | ||||||||
| 27 | Each wiki gets its own SQLite file at e.g. `/srv/data/wikis/{slug}/wiki.db`. The resolver swaps `SQLALCHEMY_DATABASE_URI` per-request in addition to swapping `GitStorage`. |
|||||||
| 28 | ||||||||
| 29 | - **Pro:** Otterwiki's admin panel writes land in the correct per-wiki DB naturally |
|||||||
| 30 | - **Con:** SQLAlchemy engine re-binding per-request is heavy; Flask-SQLAlchemy uses a singleton `db` object initialized at import time |
|||||||
| 31 | ||||||||
| 32 | ### Option 2: Side-channel read |
|||||||
| 33 | ||||||||
| 34 | The resolver opens the per-wiki DB directly via `sqlite3.connect()`, reads Preferences, uses the values to compute permissions, then closes. Otterwiki itself never touches the per-wiki DB. |
|||||||
| 35 | ||||||||
| 36 | - **Pro:** Simpler, no SQLAlchemy changes needed |
|||||||
| 37 | - **Con:** Otterwiki's admin panel still writes to the shared DB, not the per-wiki one. Writes would need to be intercepted or redirected. |
|||||||
| 38 | ||||||||
| 39 | ### Option 3: Hybrid (recommended) |
|||||||
| 40 | ||||||||
| 41 | - Swap both `GitStorage` AND `SQLALCHEMY_DATABASE_URI` per-request (Option 1) |
|||||||
| 42 | - Accept the complexity of engine re-binding as a one-time cost |
|||||||
| 43 | - Each wiki bootstraps with its own SQLite DB at creation time (`_init_wiki_repo` in management routes) |
|||||||
| 44 | - The resolver uses the per-wiki DB for both reads (permission computation) and writes (admin panel saves) |
|||||||
| 45 | ||||||||
| 46 | ## DID-for-Email Question |
|||||||
| 47 | ||||||||
| 48 | Otterwiki's `User` model uses email as the primary identifier. Robot.wtf uses ATProtocol DID handles (e.g. `@alice.bsky.social`), not emails. |
|||||||
| 49 | ||||||||
| 50 | ### Option A: Store DID handle in email field |
|||||||
| 51 | - Quick, minimal code changes |
|||||||
| 52 | - The email field becomes a general "identity" field |
|||||||
| 53 | - Display will look odd ("Email: @alice.bsky.social") |
|||||||
| 54 | - Password fields become irrelevant (ATProto auth is external) |
|||||||
| 55 | ||||||||
| 56 | ### Option B: Modify User model for DID-native identity |
|||||||
| 57 | - Add `did` and `handle` columns, deprecate email as primary key |
|||||||
| 58 | - Override the User Management template for DID-based display |
|||||||
| 59 | - Cleaner but more fork divergence from upstream otterwiki |
|||||||
| 60 | ||||||||
| 61 | ### Option C: Bridge via ProxyHeaderAuth |
|||||||
| 62 | - Keep the proxy header architecture but have the resolver read per-wiki ACL settings from the per-wiki Preferences DB |
|||||||
| 63 | - The `User` table remains unused; user identity comes from ATProto |
|||||||
| 64 | - Permission levels (ANONYMOUS, REGISTERED, APPROVED) are set per-wiki via the Permissions admin panel |
|||||||
| 65 | - The resolver maps these levels to proxy header permissions based on the authenticated user's status |
|||||||
| 66 | ||||||||
| 67 | Option C avoids the DID-for-email problem entirely by keeping user management at the platform layer (ACL) while delegating access *policy* to otterwiki's Preferences. |
|||||||
| 68 | ||||||||
| 69 | ## Prerequisites |
|||||||
| 70 | ||||||||
| 71 | - Per-wiki SQLite DB creation during wiki bootstrap |
|||||||
| 72 | - SQLAlchemy DB swap per-request (or side-channel read) |
|||||||
| 73 | - Remove `@platform_mode_disabled` from Permissions panel (and possibly User Management) |
|||||||
| 74 | - Ensure `ProxyHeaderAuth.has_permission()` reads from `app.config` (or is replaced) |
|||||||