Blame

6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
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)