Blame

f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
1
---
2
category: reference
3
tags: [design, prd, architecture, atproto, vps]
4
last_updated: 2026-03-14
5
confidence: medium
6
---
7
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
8
# VPS Architecture (ATProto + robot.wtf)
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
9
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
10
**Status:** Active — this is the current plan
11
**Supersedes:** [[Design/Platform_Overview]], [[Design/Auth]], [[Design/Operations]], [[Design/Data_Model]] (infrastructure and billing sections), [[Design/Implementation_Phases]] (phase structure and premium tiers)
12
**Preserves:** ACL model, permission headers, MCP tools, Otterwiki multi-tenancy middleware, URL structure, semantic search logic, wiki bootstrap template, REST API surface
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
13
14
---
15
16
## Why this exists
17
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
18
The AWS serverless architecture described in [[Design/Platform_Overview]] works, but it optimizes for a problem we don't have: elastic scale and zero cost at rest. The tradeoff is complexity — VPC endpoints, Mangum adapters, DynamoDB Streams to avoid SQS endpoint costs, Lambda cold starts, EFS mount latency. All of that machinery exists to make Lambda work, not to make the wiki work.
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
19
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
20
robot.wtf is a free, volunteer-run wiki service for the ATProto community. No premium tier, no billing, no Stripe. The hosting is a Debian 12 VM on a Proxmox hypervisor with a static IP and generous RAM and disk. The deployment is conventional: persistent processes, local disk, SQLite, Caddy. The application logic — multi-tenant Otterwiki, MCP tools, semantic search, ACL enforcement — ports over from the Lambda implementation with minimal changes. The middleware we already built for Lambda is WSGI middleware with a Mangum wrapper; removing the wrapper gives us back the WSGI middleware.
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
21
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
22
The ATProto identity system replaces WorkOS as the auth provider. Users sign in with their Bluesky handle (or any ATProto PDS account). Identity is a DID — portable, user-owned, and philosophically aligned with "your wiki is a git repo you can clone." The target audience (developers and researchers using AI agents) overlaps heavily with the ATProto early-adopter community.
23
24
---
25
26
## Service model
27
28
robot.wtf is a free tool, not a business. There is no premium tier and no billing infrastructure.
29
30
Every user gets:
31
32
- 1 wiki
33
- 500 pages
34
- 3 collaborators
35
- Full-text search + semantic search
36
- MCP access (Claude.ai OAuth + Claude Code bearer token)
37
- Read-only git clone
38
- Public wiki toggle
39
40
These are resource management limits, not a paywall. If someone needs more, they clone their repo and self-host — which is the whole point of git-backed storage.
41
42
If paid tiers ever make sense, the architecture supports them — the ACL model and schema have room for tier fields. But the billing infrastructure (Stripe, webhooks, lapse enforcement, upgrade/downgrade flows) doesn't get built until someone is actually asking to pay. That decision and all the commercial design work is preserved in the archived design docs ([[Design/Implementation_Phases]], [[Design/Operations]]).
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
43
44
---
45
46
## Infrastructure
47
48
### Server
49
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
50
Debian 12 VM running on a Proxmox hypervisor. Static IP, generous RAM and disk allocation. If the VM ever needs to move, the deployment is portable to any Linux box (Hetzner, DigitalOcean, Fly.io, bare metal, or back to AWS on an EC2 instance) — nothing is host-specific.
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
51
52
### Process model
53
54
Four persistent processes, managed by systemd or Docker Compose:
55
56
```
57
┌─────────────────────────────────────────────────────────────────┐
58
│ Caddy (reverse proxy, TLS) │
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
59
│ *.robot.wtf + robot.wtf │
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
60
│ │
61
│ Routes: │
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
62
│ {slug}.robot.wtf/mcp → MCP sidecar (port 8001) │
63
│ {slug}.robot.wtf/api/v1/* → REST API (port 8002) │
64
│ {slug}.robot.wtf/repo.git/* → Git smart HTTP (port 8002) │
65
│ {slug}.robot.wtf/* → Otterwiki WSGI (port 8000) │
66
│ robot.wtf/auth/* → Auth service (port 8003) │
67
│ robot.wtf/api/* → Management API (port 8002) │
68
│ robot.wtf/app/* → Static files (SPA) │
69
│ robot.wtf → Static files (landing page) │
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
70
└────────┬──────────┬──────────┬──────────┬───────────────────────┘
71
│ │ │ │
72
┌────▼───┐ ┌────▼───┐ ┌───▼────┐ ┌──▼─────┐
73
│Otterwiki│ │ MCP │ │Platform│ │ Auth │
74
│ WSGI │ │sidecar │ │ API │ │service │
75
│Gunicorn│ │FastMCP │ │ Flask │ │ Flask │
76
│:8000 │ │:8001 │ │:8002 │ │:8003 │
77
└────┬───┘ └───┬────┘ └───┬────┘ └───┬────┘
78
│ │ │ │
79
┌────▼─────────▼──────────▼───────────▼───┐
80
│ Shared resources │
81
│ /srv/wikis/{slug}/repo.git (git) │
82
│ /srv/wikis/{slug}/index.faiss (vectors) │
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
83
│ /srv/data/robot.db (SQLite) │
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
84
│ /srv/data/embeddings/ (model) │
85
└─────────────────────────────────────────┘
86
```
87
88
### Caddy
89
90
Caddy handles TLS termination, automatic Let's Encrypt certificates (including wildcard via DNS challenge), and reverse proxy routing. It replaces API Gateway + CloudFront + ACM.
91
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
92
Wildcard TLS requires a DNS challenge. Caddy supports this natively with plugins for common DNS providers (Cloudflare, Route 53, OVHcloud). The DNS zone for robot.wtf needs API credentials configured in Caddy.
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
93
94
Caddy's routing is order-sensitive and matcher-based. The Caddyfile structure:
95
96
```
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
97
robot.wtf {
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
98
handle /auth/* {
99
reverse_proxy localhost:8003
100
}
101
handle /api/* {
102
reverse_proxy localhost:8002
103
}
104
handle /app/* {
105
root * /srv/static/app
106
try_files {path} /app/index.html
107
file_server
108
}
109
handle {
110
root * /srv/static/landing
111
file_server
112
}
113
}
114
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
115
*.robot.wtf {
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
116
@mcp path /mcp /mcp/*
117
handle @mcp {
118
reverse_proxy localhost:8001
119
}
120
121
@api path /api/v1/*
122
handle @api {
123
reverse_proxy localhost:8002
124
}
125
126
@git path /repo.git/*
127
handle @git {
128
reverse_proxy localhost:8002
129
}
130
131
handle {
132
reverse_proxy localhost:8000
133
}
134
}
135
```
136
137
The `{slug}` is extracted from the `Host` header by the downstream services, not by Caddy. Caddy just routes to the right backend; the backend resolves the tenant.
138
139
### Why not Nginx
140
141
Caddy's automatic TLS (including wildcard via DNS challenge) eliminates certbot, cron renewal, and manual certificate management. For a single-operator deployment where the admin might not be around to fix a cert renewal failure, this matters. Nginx is more configurable but requires more maintenance. If we needed fine-grained caching rules or complex rewrite logic, Nginx would be worth the tradeoff. We don't.
142
143
---
144
145
## Authentication
146
147
### Identity model
148
149
User identity is an ATProto DID (Decentralized Identifier). A DID is a persistent, portable identifier that survives handle changes and PDS migrations. When a user logs in, we resolve their handle to a DID and store the DID as the primary key.
150
151
```
152
User {
153
did: string, // e.g. "did:plc:abc123..." — primary identifier
154
handle: string, // e.g. "sderle.bsky.social" — display name, may change
155
display_name: string, // from ATProto profile
156
avatar_url?: string, // from ATProto profile
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
157
username: string, // platform username (URL slug)
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
158
created_at: ISO8601,
159
wiki_count: number,
160
}
161
```
162
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
163
The `did` is the stable identity. The `handle` is refreshed from the PDS on each login (handles can change). The `username` is the platform-local slug used in URLs — immutable after signup.
164
165
### Username defaulting
166
167
When a new user signs up, the platform username defaults to the local part of their ATProto handle. For a handle like `sderle.bsky.social`, the default is `sderle`. For a user with a custom domain handle like `schuyler.robot.wtf`, the default is `schuyler` (the domain prefix). The user can override this at signup if they want something different, but the default should be right most of the time.
168
169
Validation rules are unchanged from the original design: lowercase alphanumeric + hyphens, 3–30 characters, no leading/trailing hyphens, checked against the reserved name blocklist and existing usernames/wiki slugs.
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
170
171
### ATProto OAuth (browser login)
172
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
173
robot.wtf is an ATProto OAuth **confidential client**. The flow:
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
174
175
```
176
1. User enters their handle (e.g. "sderle.bsky.social") on the login page
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
177
2. robot.wtf resolves the handle to a DID, then resolves the DID to a PDS URL
178
3. robot.wtf fetches the PDS's Authorization Server metadata
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
179
(GET {pds}/.well-known/oauth-authorization-server)
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
180
4. robot.wtf sends a Pushed Authorization Request (PAR) to the PDS's AS,
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
181
including PKCE code_challenge and DPoP proof
182
5. User is redirected to their PDS's authorization interface
183
6. User approves the authorization request
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
184
7. PDS redirects back to robot.wtf/auth/callback with an authorization code
185
8. robot.wtf exchanges the code for tokens (access_token + refresh_token)
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
186
with DPoP binding and client authentication (signed JWT)
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
187
9. robot.wtf uses the access token to fetch the user's profile (DID, handle,
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
188
display name) from their PDS
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
189
10. robot.wtf mints a platform JWT, sets it as an HttpOnly cookie on .robot.wtf
190
11. Redirect to robot.wtf/app/
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
191
```
192
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
193
The platform JWT is signed with our own RS256 key (stored on disk). After step 10, the PDS is not in the runtime path — the platform JWT is self-contained and validated locally. ATProto tokens are stored in the session database for potential future use (e.g., posting to Bluesky on behalf of the user), but they're not needed for wiki operations.
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
194
195
### Reference implementation
196
829633 Claude (MCP) 2026-03-15 01:48:47
[mcp] Fix reference implementation description: authlib.jose, not joserfc
197
Bluesky maintains a Python Flask OAuth demo in `bluesky-social/cookbook/python-oauth-web-app` (CC-0 licensed). It implements the full ATProto OAuth flow as a confidential client using `authlib` for PKCE, DPoP, JWK/JWT, and code challenge. This is the starting point for our auth service. It handles the hard parts: handle-to-DID resolution, PDS Authorization Server discovery, PAR, DPoP nonce management, and token refresh. See [[Dev/V3_V5_Risk_Research]] for detailed assessment.
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
198
199
Key libraries from the reference implementation:
200
829633 Claude (MCP) 2026-03-15 01:48:47
[mcp] Fix reference implementation description: authlib.jose, not joserfc
201
- `authlib>=1.3` — PKCE, JWK/JWT, DPoP proof creation, code challenge
202
- `dnspython>=2.6` — DNS TXT lookups for handle resolution
203
- `requests>=2.32` + `requests-hardened>=1.0.0b3` — HTTP client with SSRF mitigations
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
204
205
### MCP OAuth (Claude.ai)
206
207
This is the most architecturally significant auth flow. Claude.ai's MCP client implements standard OAuth 2.1 with Dynamic Client Registration (DCR). It discovers the Authorization Server by fetching `/.well-known/oauth-protected-resource` from the MCP endpoint. The AS must support DCR, PKCE, and standard token endpoints.
208
209
ATProto's OAuth profile is not directly compatible with this — ATProto uses per-user Authorization Servers (each user's PDS), whereas Claude.ai expects a single AS URL from the resource metadata endpoint.
210
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
211
**Solution: robot.wtf runs its own OAuth 2.1 Authorization Server for MCP.**
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
212
213
```
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
214
1. Claude.ai connects to https://{slug}.robot.wtf/mcp
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
215
2. Gets 401, fetches /.well-known/oauth-protected-resource
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
216
3. Discovers robot.wtf's AS at https://robot.wtf/auth/oauth
217
4. Performs Dynamic Client Registration at robot.wtf/auth/oauth/register
218
5. Redirects user to robot.wtf/auth/oauth/authorize
219
6. User sees robot.wtf's consent page:
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
220
- If already logged in (platform JWT cookie): "Authorize Claude to access {wiki}?"
221
- If not logged in: "Sign in with Bluesky" → ATProto OAuth flow → then consent
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
222
7. User approves, robot.wtf issues authorization code
223
8. Claude.ai exchanges code for access token at robot.wtf/auth/oauth/token
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
224
9. Claude.ai uses access token to make MCP requests
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
225
10. MCP sidecar validates token against robot.wtf's JWKS
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
226
```
227
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
228
robot.wtf's MCP OAuth AS is a thin layer. It delegates authentication to ATProto (step 6) and handles authorization itself (does this user have access to this wiki?). The token it issues is a JWT containing the user's DID and the authorized wiki slug, signed with our RS256 key.
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
229
230
Required OAuth 2.1 AS endpoints:
231
232
| Endpoint | Purpose |
233
|----------|---------|
234
| `/.well-known/oauth-authorization-server` | AS metadata (issuer, endpoints, supported grants) |
235
| `/auth/oauth/register` | Dynamic Client Registration (RFC 7591) |
236
| `/auth/oauth/authorize` | Authorization endpoint (consent page) |
237
| `/auth/oauth/token` | Token endpoint (code exchange, refresh) |
238
| `/.well-known/jwks.json` | Public key for token validation |
239
240
These can be implemented with `authlib`'s server components or hand-rolled (the spec surface is small — DCR, authorization code grant with PKCE, token issuance, JWKS).
241
242
### MCP protected resource metadata
243
244
Each wiki's MCP endpoint serves its own resource metadata:
245
246
```json
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
247
// GET https://{slug}.robot.wtf/.well-known/oauth-protected-resource
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
248
{
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
249
"resource": "https://{slug}.robot.wtf/mcp",
250
"authorization_servers": ["https://robot.wtf/auth/oauth"],
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
251
"scopes_supported": ["wiki:read", "wiki:write"]
252
}
253
```
254
255
All wikis point to the same AS. The AS knows which wiki is being authorized because the `redirect_uri` and `resource` parameter identify the wiki.
256
257
### Bearer tokens (Claude Code / API)
258
259
Unchanged from the current design. Each wiki gets a bearer token at creation time, stored as a bcrypt hash in the database. The user sees the token once. Claude Code usage:
260
261
```bash
262
claude mcp add {slug} \
263
--transport http \
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
264
--url https://{slug}.robot.wtf/mcp \
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
265
--header "Authorization: Bearer YOUR_TOKEN"
266
```
267
268
### Cross-subdomain auth
269
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
270
Same approach as [[Design/Frontend]]: platform JWT stored as an `HttpOnly`, `Secure`, `SameSite=Lax` cookie on `.robot.wtf`. Every request to any subdomain includes the cookie. The Otterwiki middleware and MCP sidecar both validate JWTs using the same public key.
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
271
272
### Auth convergence
273
274
All three paths converge on the same identity and the same ACL check:
275
276
```
277
Browser → ATProto OAuth → platform JWT (cookie) → resolve DID → ACL check
278
Claude.ai → MCP OAuth 2.1 → MCP access token (JWT) → resolve DID → ACL check
279
Claude Code → Bearer token → hash lookup in DB → resolve user → ACL check
280
281
All paths → middleware → sets Otterwiki proxy headers (or authorizes MCP/API request)
282
```
283
284
### Migration off ATProto
285
286
We store the DID as the primary user identifier, not the handle or PDS URL. If ATProto auth needs to be replaced, the migration path is:
287
288
- Add alternative OAuth providers (Google, GitHub) alongside ATProto
289
- Link new provider identities to existing DIDs via an `identity_links` table
290
- Existing users continue to work; new users can sign up with either method
291
292
This is simpler than the WorkOS migration path in the original design because we already own the JWT-issuing layer — we're not migrating off a third-party token issuer.
293
294
---
295
296
## Data Model
297
298
### SQLite replaces DynamoDB
299
300
The dataset is small even at 1000 users. SQLite on local disk is simpler, faster, and free. The application layer uses SQLAlchemy (or raw `sqlite3` — the schema is simple enough). If the deployment ever needs Postgres, the migration is straightforward.
301
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
302
The SQLite database lives at `/srv/data/robot.db`. Write concurrency is handled by SQLite's WAL mode, which supports concurrent reads with serialized writes. For a wiki platform where writes are infrequent relative to reads, this is more than adequate.
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
303
304
### Tables
305
306
```sql
307
CREATE TABLE users (
308
did TEXT PRIMARY KEY, -- ATProto DID
309
handle TEXT NOT NULL, -- ATProto handle (may change)
310
display_name TEXT,
311
avatar_url TEXT,
312
username TEXT UNIQUE NOT NULL, -- platform slug, immutable
313
created_at TEXT NOT NULL, -- ISO8601
314
wiki_count INTEGER DEFAULT 0
315
);
316
317
CREATE TABLE wikis (
318
slug TEXT PRIMARY KEY, -- globally unique, URL slug
319
owner_did TEXT NOT NULL REFERENCES users(did),
320
display_name TEXT NOT NULL,
321
repo_path TEXT NOT NULL, -- /srv/wikis/{slug}/repo.git
322
mcp_token_hash TEXT NOT NULL, -- bcrypt hash
323
is_public INTEGER DEFAULT 0,
324
created_at TEXT NOT NULL,
325
last_accessed TEXT NOT NULL,
326
page_count INTEGER DEFAULT 0
327
);
328
329
CREATE TABLE acls (
330
wiki_slug TEXT NOT NULL REFERENCES wikis(slug),
331
grantee_did TEXT NOT NULL REFERENCES users(did),
332
role TEXT NOT NULL, -- 'owner' | 'editor' | 'viewer'
333
granted_by TEXT NOT NULL,
334
granted_at TEXT NOT NULL,
335
PRIMARY KEY (wiki_slug, grantee_did)
336
);
337
338
CREATE TABLE oauth_sessions (
339
id TEXT PRIMARY KEY, -- session ID
340
user_did TEXT NOT NULL REFERENCES users(did),
341
dpop_private_jwk TEXT NOT NULL, -- DPoP key (encrypted at rest)
342
access_token TEXT,
343
refresh_token TEXT,
344
token_expires_at TEXT,
345
created_at TEXT NOT NULL
346
);
347
348
CREATE TABLE mcp_oauth_clients (
349
client_id TEXT PRIMARY KEY, -- DCR-issued client ID
350
client_name TEXT,
351
redirect_uris TEXT NOT NULL, -- JSON array
352
client_secret_hash TEXT, -- for confidential clients
353
created_at TEXT NOT NULL
354
);
355
356
CREATE TABLE reindex_queue (
357
wiki_slug TEXT NOT NULL,
358
page_path TEXT NOT NULL,
359
action TEXT NOT NULL, -- 'upsert' | 'delete'
360
queued_at TEXT NOT NULL,
361
PRIMARY KEY (wiki_slug, page_path)
362
);
363
```
364
365
### Storage layout
366
367
```
368
/srv/
369
wikis/
370
{slug}/
371
repo.git/ # bare git repo
372
index.faiss # FAISS vector index
373
embeddings.json # page_path → vector mapping
374
data/
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
375
robot.db # SQLite database
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
376
signing_key.pem # RS256 private key for JWT signing
377
signing_key.pub # RS256 public key
378
client_jwk.json # ATProto OAuth confidential client JWK (private)
379
client_jwk_pub.json # ATProto OAuth client JWK (public, served at client_id URL)
380
static/
381
landing/ # landing page HTML/CSS/JS
382
app/ # management SPA
383
embeddings/
384
model/ # all-MiniLM-L6-v2 model files
385
backups/ # local backup staging
386
```
387
388
---
389
390
## Compute
391
392
### Otterwiki (WSGI)
393
394
Otterwiki runs as a persistent Gunicorn process. The multi-tenant middleware we built for Lambda ports back to WSGI by removing the Mangum wrapper. The middleware:
395
396
1. Extracts the wiki slug from the `Host` header
397
2. Looks up the wiki in SQLite
398
3. Resolves the user from the platform JWT (cookie) or bearer token
399
4. Checks ACL permissions
400
5. Sets Otterwiki proxy headers (`x-otterwiki-email`, `x-otterwiki-name`, `x-otterwiki-permissions`)
401
6. Swaps Otterwiki's config to point at the correct repo path
402
7. Delegates to Otterwiki's Flask app
403
404
The config-swapping is the multi-tenancy mechanism we already built. In Lambda, it happened per-invocation; in WSGI, it happens per-request. The difference is negligible — the config is a handful of in-memory variables, not file I/O.
405
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
406
Gunicorn runs with multiple workers. The Proxmox VM has generous RAM, so worker count is limited by CPU cores, not memory. Git write operations are serialized per-repo by git's own lock file.
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
407
408
### MCP sidecar (FastMCP)
409
410
FastMCP runs as a separate process serving Streamable HTTP on port 8001. It reads git repos directly from `/srv/wikis/{slug}/repo.git` — same code as the current MCP server, same tools, same return formats.
411
412
The sidecar validates MCP OAuth tokens (JWTs signed by our AS) and bearer tokens (bcrypt hash lookup in SQLite). Token validation is the same logic as the Otterwiki middleware, factored into a shared library.
413
414
Why a separate process: Otterwiki is a Flask app designed around page rendering. The MCP server is an async protocol handler. Mixing them in one process would require either making Otterwiki async (large refactor) or running FastMCP synchronously (defeats the purpose). Separate processes, same database, same git repos.
415
416
### Platform API (Flask)
417
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
418
A lightweight Flask app handling the management API (wiki CRUD, ACL management, token generation) and the Git smart HTTP protocol. This is the same API surface described in the archived [[Design/Implementation_Phases]], with SQLite queries instead of DynamoDB calls.
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
419
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
420
The Git smart HTTP endpoints (`/repo.git/info/refs`, `/repo.git/git-upload-pack`) use dulwich to serve the bare repos on disk. Read-only (upload-pack only) — users can clone and pull their wikis at any time.
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
421
422
### Auth service (Flask)
423
424
Handles both ATProto OAuth (browser login) and the MCP OAuth 2.1 AS. Runs as its own process because the OAuth flows involve redirects and state management that are cleaner in isolation.
425
426
This could be merged into the platform API process. Separating it keeps the auth code (which is security-critical and relatively complex) isolated from the CRUD endpoints. If the separation proves to be operationally annoying, merge them — they're both Flask apps talking to the same SQLite database.
427
428
---
429
430
## Semantic Search
431
432
The embedding pipeline simplifies dramatically on a VPS. No DynamoDB Streams, no event source mappings, no separate embedding Lambda. MiniLM loads once at process startup and stays in memory.
433
434
### Write path
435
436
```
437
Page write (Otterwiki or MCP)
438
→ Middleware writes {wiki_slug, page_path, action} to reindex_queue table in SQLite
439
→ Background worker (in-process thread or separate process) polls the queue:
440
1. Read page content from git repo on disk
441
2. Chunk page
442
3. Embed chunks using MiniLM (already loaded in memory)
443
4. Update FAISS index on disk
444
5. Delete queue entry
445
```
446
447
The background worker can be a simple thread in the Otterwiki process (using Python's `threading` or `concurrent.futures`), a separate `huey` or `rq` worker, or even a cron job that runs every 30 seconds. The latency requirement is loose — research wikis are written by AI agents and searched minutes later.
448
449
For simplicity, start with an in-process thread pool. If it causes issues (GIL contention under load, memory pressure from MiniLM in every Gunicorn worker), move to a dedicated worker process that loads MiniLM once and processes the queue.
450
451
### Search path
452
453
Synchronous, handled by the MCP sidecar or REST API:
454
455
1. MiniLM is loaded at process startup (the MCP sidecar and API processes both load it)
456
2. Embed the query
457
3. Load FAISS index from disk (cached in memory after first load)
458
4. Search, deduplicate, return results
459
460
On a VPS, loading the FAISS index is a local disk read (<1ms for a typical wiki). No EFS mount latency, no Lambda cold start loading the model.
461
462
### Model loading strategy
463
464
MiniLM (~80MB) loads in ~500ms. On a VPS with persistent processes, this happens once at startup. In the Lambda architecture, it happened on every cold start. This is one of the clearest wins of the VPS approach.
465
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
466
The Proxmox VM has plenty of RAM, so loading MiniLM in both the MCP sidecar and a dedicated embedding worker is fine. The Otterwiki process and platform API don't need it — they just write to the reindex queue.
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
467
468
---
469
470
## Backup and Disaster Recovery
471
472
### What we're protecting
473
474
| Data | Location | Severity of loss |
475
|------|----------|-----------------|
476
| Git repos | `/srv/wikis/*/repo.git` | **Critical** — user data |
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
477
| SQLite database | `/srv/data/robot.db` | **High** — reconstructable from repos but painful |
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
478
| FAISS indexes | `/srv/wikis/*/index.faiss` | **Low** — rebuildable from repo content |
479
| Signing keys | `/srv/data/*.pem`, `/srv/data/*.json` | **High** — loss invalidates all active sessions |
480
481
### Backup strategy
482
483
**Git repos:** `rsync` to offsite storage (a second VPS, an S3 bucket, or a Backblaze B2 bucket). Daily, with a cron job. Repos are bare git — rsync handles them efficiently. Also: users can `git clone` their own repos at any time, which is distributed backup by design.
484
485
**SQLite:** `.backup` command (online backup, doesn't block writes in WAL mode) to a local snapshot file, then rsync offsite with the git repos. Daily.
486
487
**Signing keys:** Backed up once at creation time, stored separately from the data backups (e.g., in a password manager or encrypted at rest on a different system). These rarely change.
488
489
**FAISS indexes:** Not backed up. Rebuildable from repo content. Loss triggers a one-time re-embedding — seconds per wiki.
490
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
491
**Proxmox snapshots:** The Proxmox hypervisor can take VM-level snapshots. These are a useful complement to application-level backups — a snapshot captures the entire VM state for rapid rollback after a bad deploy. Not a substitute for offsite backups (snapshots live on the same hardware).
492
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
493
### Recovery
494
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
495
If the VM dies completely, recovery is:
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
496
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
497
1. Provision a new VM (on Proxmox or any other host)
498
2. Install Debian 12, install dependencies, deploy application code
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
499
3. Restore signing keys
500
4. Restore SQLite database from backup
501
5. Restore git repos from backup (or users re-push from their clones)
502
6. Re-embed all wikis (automated script, runs in minutes)
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
503
7. Update DNS to point to new IP (if it changed)
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
504
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
505
RTO: hours (mostly limited by repo restore transfer time). RPO: 24 hours (daily backup cycle). This is acceptable for a free community service. If tighter RPO is needed, increase backup frequency or add streaming replication to a standby.
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
506
507
---
508
509
## Deployment
510
511
### Application deployment
512
513
Code lives in a Git repo. Deployment is `git pull` + restart services. No Pulumi, no CloudFormation, no CI/CD pipeline required (though one can be added).
514
515
```bash
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
516
ssh vm
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
517
cd /srv/app
518
git pull
519
pip install -r requirements.txt --break-system-packages
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
520
sudo systemctl restart robot-otterwiki
521
sudo systemctl restart robot-mcp
522
sudo systemctl restart robot-api
523
sudo systemctl restart robot-auth
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
524
# Caddy doesn't need restart for app deploys
525
```
526
527
Or with Docker Compose:
528
529
```bash
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
530
ssh vm
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
531
cd /srv/app
532
git pull
533
docker compose build
534
docker compose up -d
535
```
536
537
### Initial setup
538
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
539
1. Provision Debian 12 VM on Proxmox, assign static IP
540
2. Install OS packages: Python 3.11+, git, build-essential
541
3. Install Caddy (with DNS challenge plugin for the DNS provider)
542
4. Configure DNS: `robot.wtf` and `*.robot.wtf` → VM's static IP
543
5. Configure Caddy with DNS challenge credentials for wildcard TLS
544
6. Generate RS256 signing keypair (`/srv/data/signing_key.pem`)
545
7. Generate ATProto OAuth client JWK (`/srv/data/client_jwk.json`)
546
8. Publish client metadata at `https://robot.wtf/auth/client-metadata.json`
547
9. Initialize SQLite database (run migration script)
548
10. Download MiniLM model to `/srv/embeddings/model/`
549
11. Start services, verify health checks
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
550
551
### Monitoring
552
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
553
For a volunteer-run service, keep monitoring simple:
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
554
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
555
- **Health checks:** Each service exposes a `/health` endpoint. An external monitor (UptimeRobot, free tier) pings them.
556
- **Logs:** systemd journal or Docker logs. `journalctl -u robot-otterwiki --since "1 hour ago"` is sufficient at this scale.
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
557
- **Disk space:** A cron job that alerts (email or Bluesky DM) when disk usage exceeds 80%.
558
- **Backups:** The backup cron job logs success/failure. Alert on failure.
559
560
If the service grows, add Prometheus + Grafana. Not before.
561
562
---
563
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
564
## URL Structure
565
566
Every wiki gets a subdomain: `{slug}.robot.wtf`. The slug is the wiki's globally unique identifier.
567
568
```
569
sderle.robot.wtf/ → wiki web UI (Otterwiki)
570
sderle.robot.wtf/api/v1/ → wiki REST API
571
sderle.robot.wtf/mcp → wiki MCP endpoint
572
sderle.robot.wtf/repo.git/* → git smart HTTP (read-only clone)
573
```
574
575
For the single-wiki-per-user model, the wiki slug is the username. You sign up as `sderle`, your wiki lives at `sderle.robot.wtf`.
576
577
The management app and auth live on the root domain:
578
579
```
580
robot.wtf/ → landing page
581
robot.wtf/app/ → management SPA (dashboard)
582
robot.wtf/app/settings → wiki settings
583
robot.wtf/app/collaborators → collaborator management
584
robot.wtf/app/connect → MCP connection instructions
585
robot.wtf/app/account → account settings
586
robot.wtf/auth/* → OAuth flows (ATProto + MCP AS)
587
robot.wtf/api/* → management API
588
```
589
590
### Namespace rules
591
592
Slugs and usernames are the same thing (each user gets one wiki, the slug IS the username). Reserved names blocked for signup: `api`, `auth`, `app`, `www`, `admin`, `mcp`, `docs`, `status`, `blog`, `help`, `support`, `static`, `assets`, `null`, `undefined`, `wiki`, `robot`.
593
594
---
595
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
596
## What changes vs. what stays the same
597
598
### Stays the same
599
600
- ACL model (owner/editor/viewer roles, same permission matrix)
601
- Otterwiki proxy header mechanism (`x-otterwiki-email`, `x-otterwiki-name`, `x-otterwiki-permissions`)
602
- Multi-tenant middleware logic (resolve slug → look up wiki → check ACL → set headers → delegate)
603
- MCP tools (read_note, write_note, search, semantic_search, list_notes, etc.)
604
- REST API surface (same endpoints, same request/response shapes)
605
- Wiki bootstrap template
606
- FAISS + MiniLM semantic search
607
- Otterwiki admin panel disposition (same sections hidden/shown)
608
609
### Changes
610
611
| Component | AWS architecture | VPS architecture |
612
|-----------|-----------------|-----------------|
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
613
| Domain | wikibot.io | robot.wtf |
614
| Business model | Freemium SaaS | Free volunteer project |
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
615
| Hosting | Lambda + EFS + API Gateway | Gunicorn + local disk + Caddy |
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
616
| Host environment | AWS (managed) | Debian 12 VM on Proxmox |
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
617
| Database | DynamoDB (on-demand) | SQLite (WAL mode) |
618
| Auth provider | WorkOS AuthKit | ATProto OAuth (self-hosted) |
619
| MCP OAuth AS | WorkOS (standalone connect) | Self-hosted OAuth 2.1 AS |
620
| Identity | OAuth provider sub (Google/GitHub/etc.) | ATProto DID |
621
| TLS | ACM + CloudFront | Caddy + Let's Encrypt |
622
| Embedding trigger | DynamoDB Streams → Lambda | SQLite queue → background worker |
623
| Static hosting | S3 + CloudFront | Caddy file_server |
624
| IaC | Pulumi | systemd units or Docker Compose |
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
625
| Secrets | Secrets Manager | Files on disk |
626
| Backups | AWS Backup + DynamoDB PITR | rsync + SQLite .backup + Proxmox snapshots |
627
| Billing | Stripe (planned) | None |
628
| Cost | ~$13–18/mo at launch | $0 |
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
629
630
### What can be reused from existing implementation
631
632
- **Multi-tenant middleware** — remove Mangum wrapper, the WSGI middleware is underneath
633
- **MCP server tools** — identical, just change the repo path prefix
634
- **REST API handlers** — swap DynamoDB calls for SQLite queries
635
- **Otterwiki fork** — identical, same proxy header auth mode
636
- **Semantic search plugin** — identical
637
- **FAISS indexing code** — identical
638
- **Frontend SPA** — identical (change `VITE_API_BASE_URL`, remove WorkOS client ID)
639
- **Wiki bootstrap template** — identical
640
- **ACL checking logic** — swap DynamoDB reads for SQLite reads
641
642
---
643
644
## Open Questions
645
4f59a2 Claude (MCP) 2026-03-15 01:48:32
[mcp] Update open questions with research findings, mark resolved items
646
1. ~~**ATProto Python OAuth library maturity.**~~ **RESOLVED.** The Bluesky Flask demo uses `authlib` (not `joserfc` — earlier research was wrong). Dependencies are `authlib>=1.3`, `dnspython`, `requests`, `requests-hardened`, `regex`. All mature. The demo is ~600 lines, well-factored, and directly adaptable. See [[Dev/V3_V5_Risk_Research]].
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
647
4f59a2 Claude (MCP) 2026-03-15 01:48:32
[mcp] Update open questions with research findings, mark resolved items
648
2. ~~**MCP OAuth AS scope.**~~ **RESOLVED.** `authlib` provides `AuthorizationServer`, `AuthorizationCodeGrant` (with PKCE), and `ClientRegistrationEndpoint` (RFC 7591). The Flask OAuth 2.0 server components handle the heavy lifting. We implement model callbacks (save_client, save_token, query_client) against SQLite. See [[Dev/V3_V5_Risk_Research]] for implementation sketch.
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
649
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
650
3. **Caddy DNS challenge provider.** Wildcard TLS requires DNS API access. Which DNS provider hosts the robot.wtf zone? Cloudflare, Route 53, and OVHcloud are all supported by Caddy. The DNS provider choice should be made before deployment.
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
651
4f59a2 Claude (MCP) 2026-03-15 01:48:32
[mcp] Update open questions with research findings, mark resolved items
652
4. **Claude.ai MCP OAuth compatibility.** The self-hosted OAuth 2.1 AS approach should work — Claude.ai's MCP client follows standard OAuth 2.1 discovery. Key finding: Claude.ai uses `client_secret_post` auth method and does NOT require DPoP. The risk is in underdocumented client quirks. Mitigation: build a minimal stub AS early and test against Claude.ai before building the full thing. See [[Dev/V3_V5_Risk_Research]].
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
653
4f59a2 Claude (MCP) 2026-03-15 01:48:32
[mcp] Update open questions with research findings, mark resolved items
654
5. ~~**ATProto scopes.**~~ **RESOLVED.** The ATProto spec explicitly says: "A client may include only the `atproto` scope if they only need account authentication." The `sub` field in the token response contains the DID. We request scope `"atproto"` and nothing else. See [[Dev/V3_V5_Risk_Research]].
f139f8 Claude (MCP) 2026-03-15 00:28:30
[mcp] Add VPS architecture design doc: ATProto auth, Caddy, SQLite, OVHcloud
655
9595ef Claude (MCP) 2026-03-15 01:18:19
[mcp] Rewrite VPS architecture: robot.wtf, no premium tier, Debian 12/Proxmox, resolve open questions
656
6. **Docker Compose vs. systemd.** Both work. Docker Compose gives you reproducible builds, isolation, and easier migration between hosts. Systemd is lighter, native to Debian, and avoids Docker's overhead. For a Proxmox VM where we control the environment completely, systemd is probably sufficient. Docker adds value if we expect to move the deployment frequently.