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]
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
4
last_updated: 2026-03-17
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
5
confidence: high
6
---
7
8
# Per-Wiki Database Design
9
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
10
## Decision (2026-03-17)
11
12
**Implementing Option A (per-request DB swap via monkey-patching) now.** A cleaner upstream refactor (contextvars-based dependency injection) is sketched below and will be proposed to redimp as a separate PR. Option A is throwaway work if the upstream refactor lands, but unblocks per-wiki permissions immediately.
13
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
14
## Current State
15
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
16
Otterwiki uses a single shared SQLite DB (`/tmp/otterwiki_default.db`) for all tenants. The `Preferences`, `User`, `Drafts`, and `Cache` tables are global. The `TenantResolver` swaps `GitStorage` (the git repo path) per-request but never swaps the SQLAlchemy DB binding.
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
17
18
This hasn't caused problems because:
19
- `ProxyHeaderAuth.has_permission()` ignores the Preferences table entirely — permissions come from proxy headers
20
- User Management and Permissions admin panels are disabled via `PLATFORM_MODE`
21
- The `User` table is effectively dead code under `ProxyHeaderAuth`
22
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
23
### What the resolver swaps today
24
25
| Component | Swapped? | How |
26
|-----------|----------|-----|
27
| GitStorage | Yes | Monkey-patches `storage` across 7 modules |
28
| GitHttpServer | Yes | Recreated per-request |
29
| `app.config["REPOSITORY"]` | Yes | Direct assignment |
30
| SQLAlchemy `db` | **No** | All wikis share one DB |
31
| `app_renderer` | No | Created once at import time |
32
| Flask-Mail | No | Never swapped |
33
| `app.config` (other keys) | No | SITE_NAME, permissions, etc. are global |
34
35
## Option A: Per-Request DB Swap (implementing now)
36
37
### Storage layout
38
39
```
40
/srv/data/
41
├── robot.db (platform: users, acls, wikis, oauth)
42
└── wikis/{slug}/
43
├── repo/ (git repo — already exists)
44
└── wiki.db (otterwiki: preferences, drafts, user, cache)
45
```
46
47
### Mechanism
48
49
1. Resolver calls `_swap_database(wiki_slug)` after `_swap_storage(repo_path)`
50
2. Updates `app.config["SQLALCHEMY_DATABASE_URI"]` to `sqlite:////srv/data/wikis/{slug}/wiki.db`
51
3. Calls `db.engine.dispose()` to close connections to previous DB
52
4. Lazy-creates `wiki.db` with schema if it doesn't exist (first request)
53
5. Calls `update_app_config()` to reload Preferences into `app.config`
54
55
### Schema initialization
56
57
Raw SQL in management API (avoids importing Otterwiki's server.py). Creates 4 tables matching `otterwiki/models.py`: `preferences`, `drafts`, `user`, `cache`. Seeds `SITE_NAME`.
58
59
### Permission model
60
61
**Platform ACLs remain authoritative.** ProxyHeaderAuth trusts proxy headers, ignores READ_ACCESS/WRITE_ACCESS. No change to auth flow. Per-wiki Preferences used only for wiki appearance/behavior settings.
62
63
### DID-as-email
64
65
Already handled — resolver injects `@handle` in the email header field. No User table rows needed under ProxyHeaderAuth.
66
67
### Migration
68
69
Lazy: if `wiki.db` doesn't exist when resolver hits it, create and seed on the spot. No data migration from shared default DB.
70
71
### Risks
72
73
- **Flask-SQLAlchemy engine caching**: `dispose()` + config update should work with FSA 3.x lazy engine creation. Fallback: access `db._engines` internals.
74
- **Worker model**: Assumes gunicorn sync workers. Async/threaded workers would race on global state.
75
- **`update_app_config()` side effects**: Recreates Flask-Mail per-call. Harmless.
76
- **Renderer**: Holds reference to `app.config` dict, so Preferences updates propagate. But `RENDERER_HTML_WHITELIST` parsed at init time won't update per-wiki.
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
77
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
78
## Option B: Upstream Contextvars Refactor (future PR to redimp)
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
79
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
80
### Core idea
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
81
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
82
New `otterwiki/context.py` provides getter functions backed by `contextvars`:
83
- `get_storage()`, `get_db()`, `get_renderer()`, `get_mail()`, `get_githttpserver()`
84
- Each falls back to the current module-level singleton when no context override is set
85
- `wiki_context()` context manager sets overrides for a block
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
86
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
87
### What stays the same
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
88
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
89
- `app` (Flask) stays as module-level singleton — Flask has its own `current_app`
90
- `db` stays importable for model definitions (`db.Model`, `db.Column`)
91
- All `@app.route` decorators unchanged
92
- `models.py` unchanged
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
93
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
94
### Migration path
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
95
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
96
~280 mechanical substitutions across 14 files:
97
- `storage``get_storage()`
98
- `app.config[...]``current_app.config[...]`
99
- `db.session.X()``get_db().session.X()`
100
- One lazy-init fix for `_serializer` in `helper.py`
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
101
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
102
### Phasing
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
103
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
104
**PR 1** (small): Add `context.py`, `init_defaults()` call, migrate 2-3 leaf modules. Purely additive.
105
**PR 2** (wide, mechanical): Migrate remaining modules.
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
106
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
107
### Upstream pitch
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
108
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
109
- Testability: inject mocks via `wiki_context()` without real git repos or SQLite
110
- Flask best practice: `current_app` over direct `app` import
111
- Plugin flexibility: temporary renderer/storage overrides
112
- Backward compatible: getters fall back to singletons
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
113
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
114
### Constraint
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
115
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
116
`models.py` uses `db.Model` at class definition time — `db` can never fully move behind a getter. The contextvar is for session operations only.
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
117
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
118
## SQLite Multi-Tenant: Why It's Fine
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
119
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
120
Per-tenant SQLite is a well-established pattern (37signals, Turso, Laravel Tenancy, Rails Shardines). For our use case:
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
121
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
122
- Small per-tenant data (prefs, drafts, accounts)
123
- Read-heavy workload
124
- Single VPS, tens-to-hundreds of tenants
125
- Already using SQLite with per-wiki git repos
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
126
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
127
Postgres would add a daemon, configuration, and maintenance for no benefit. The pattern breaks down at tens of thousands of tenants — not a concern here.
6d8811 Claude (MCP) 2026-03-16 05:00:01
[mcp] Add per-wiki database design note
128
5f971f Claude (MCP) 2026-03-17 02:26:22
[mcp] Update per-wiki DB design with Option A implementation decision and upstream refactor sketch
129
Operational concern is **schema migrations** — lazy migration on connection open (`PRAGMA user_version`) is the simplest approach.