tangled
alpha
login
or
join now
malpercio.dev
/
atbb
5
fork
atom
WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node
typescript
hono
htmx
atproto
5
fork
atom
overview
issues
pulls
pipelines
docs: add design doc for ATB-57 theme write API endpoints
malpercio.dev
1 week ago
1cc1e3a2
7274516a
+178
1 changed file
expand all
collapse all
unified
split
docs
plans
2026-03-02-theme-write-api-design.md
+178
docs/plans/2026-03-02-theme-write-api-design.md
···
1
1
+
# Theme Write API Endpoints — Design
2
2
+
3
3
+
**Linear:** ATB-57
4
4
+
**Date:** 2026-03-02
5
5
+
**Status:** Approved, ready for implementation
6
6
+
7
7
+
---
8
8
+
9
9
+
## Context
10
10
+
11
11
+
The AppView needs write endpoints so admins can create, update, and delete themes, and manage the theme policy. These follow the PDS-first pattern established by category and board management.
12
12
+
13
13
+
**Depends on:** ATB-51 (theme lexicons), ATB-55 (theme read endpoints + DB tables)
14
14
+
15
15
+
---
16
16
+
17
17
+
## Route Placement
18
18
+
19
19
+
All four endpoints are added to `apps/appview/src/routes/admin.ts`, alongside existing category/board write endpoints. The admin router is already mounted at `/admin` in `index.ts` — no routing changes needed.
20
20
+
21
21
+
---
22
22
+
23
23
+
## Endpoints
24
24
+
25
25
+
| Method | Path | Permission |
26
26
+
|--------|------|-----------|
27
27
+
| `POST` | `/api/admin/themes` | `space.atbb.permission.manageThemes` |
28
28
+
| `PUT` | `/api/admin/themes/:rkey` | `space.atbb.permission.manageThemes` |
29
29
+
| `DELETE` | `/api/admin/themes/:rkey` | `space.atbb.permission.manageThemes` |
30
30
+
| `PUT` | `/api/admin/theme-policy` | `space.atbb.permission.manageThemes` |
31
31
+
32
32
+
---
33
33
+
34
34
+
## Permission Changes
35
35
+
36
36
+
Add `space.atbb.permission.manageThemes` to `apps/appview/src/lib/seed-roles.ts`:
37
37
+
38
38
+
- **Owner**: already has `"*"` wildcard — no change
39
39
+
- **Admin**: add `manageThemes` to the permissions array
40
40
+
- **Moderator / Member**: no change
41
41
+
42
42
+
---
43
43
+
44
44
+
## Input Validation
45
45
+
46
46
+
### Theme (POST and PUT)
47
47
+
48
48
+
| Field | Rule |
49
49
+
|-------|------|
50
50
+
| `name` | Required string, non-empty, ≤ 100 graphemes |
51
51
+
| `colorScheme` | Required, must be `"light"` or `"dark"` |
52
52
+
| `tokens` | Required, must be a non-null object; values must be strings |
53
53
+
| `cssOverrides` | Optional string (do NOT render until ATB-62 CSS sanitization ships) |
54
54
+
| `fontUrls` | Optional array of strings; each must start with `"https://"` |
55
55
+
56
56
+
Token keys are **not** validated against a known list (lenient mode — allows custom/future tokens).
57
57
+
58
58
+
### Theme Policy (PUT)
59
59
+
60
60
+
| Field | Rule |
61
61
+
|-------|------|
62
62
+
| `availableThemes` | Required non-empty array of `{ uri: string, cid: string }` |
63
63
+
| `defaultLightThemeUri` | Required string; must be an AT-URI present in `availableThemes` |
64
64
+
| `defaultDarkThemeUri` | Required string; must be an AT-URI present in `availableThemes` |
65
65
+
| `allowUserChoice` | Optional boolean, defaults `true` |
66
66
+
67
67
+
---
68
68
+
69
69
+
## Endpoint Details
70
70
+
71
71
+
### `POST /api/admin/themes`
72
72
+
73
73
+
1. Parse and validate request body
74
74
+
2. Get ForumAgent (return 503 if unavailable)
75
75
+
3. Generate `rkey = TID.nextStr()`
76
76
+
4. `putRecord` on Forum DID's PDS with `collection: "space.atbb.forum.theme"`
77
77
+
5. Return `{ uri, cid }` with `201`
78
78
+
79
79
+
Does not wait for firehose indexing — the PDS write is the authoritative action.
80
80
+
81
81
+
### `PUT /api/admin/themes/:rkey`
82
82
+
83
83
+
1. Parse and validate request body
84
84
+
2. Look up existing theme by `rkey` + `forumDid` in DB (404 if missing)
85
85
+
3. Get ForumAgent
86
86
+
4. `putRecord` with same rkey, preserving `createdAt` from DB row
87
87
+
5. Optional fields (`cssOverrides`, `fontUrls`, `description`) fall back to existing DB values if not provided in request
88
88
+
6. Return `{ uri, cid }` with `200`
89
89
+
90
90
+
### `DELETE /api/admin/themes/:rkey`
91
91
+
92
92
+
1. Look up theme in DB (404 if missing)
93
93
+
2. Pre-flight conflict check: query `theme_policies` for rows where `default_light_theme_uri` OR `default_dark_theme_uri` = this theme's AT-URI
94
94
+
3. Return `409` if any match
95
95
+
4. Get ForumAgent
96
96
+
5. `deleteRecord` on Forum DID's PDS
97
97
+
6. Return `{ success: true }` with `200`
98
98
+
99
99
+
### `PUT /api/admin/theme-policy`
100
100
+
101
101
+
Upsert semantics (creates if no policy row exists yet, updates if one does).
102
102
+
103
103
+
1. Parse and validate request body
104
104
+
2. Validate `defaultLightThemeUri` is present in `availableThemes` (400 if not)
105
105
+
3. Validate `defaultDarkThemeUri` is present in `availableThemes` (400 if not)
106
106
+
4. Get ForumAgent
107
107
+
5. `putRecord` with `rkey: "self"`, `collection: "space.atbb.forum.themePolicy"`
108
108
+
6. PDS record structure follows the `themeRef` wrapper pattern from the lexicon: `{ theme: { uri, cid } }`
109
109
+
7. Return `{ uri, cid }` with `200`
110
110
+
111
111
+
---
112
112
+
113
113
+
## Error Codes
114
114
+
115
115
+
| Status | Condition |
116
116
+
|--------|-----------|
117
117
+
| 400 | Invalid/missing input field, invalid colorScheme, non-HTTPS fontUrl, default theme not in availableThemes |
118
118
+
| 401 | Not authenticated |
119
119
+
| 403 | Caller lacks `manageThemes` permission |
120
120
+
| 404 | Theme rkey not found (PUT/DELETE) |
121
121
+
| 409 | DELETE attempted on a theme that is the current policy default |
122
122
+
| 503 | DB or PDS connectivity error |
123
123
+
124
124
+
---
125
125
+
126
126
+
## Tests
127
127
+
128
128
+
### `POST /api/admin/themes`
129
129
+
- Happy path: returns 201 with uri and cid
130
130
+
- Missing `name` → 400
131
131
+
- Empty `name` → 400
132
132
+
- `name` too long (> 100 graphemes) → 400
133
133
+
- Invalid `colorScheme` (not light/dark) → 400
134
134
+
- Missing `colorScheme` → 400
135
135
+
- `tokens` not an object → 400
136
136
+
- Missing `tokens` → 400
137
137
+
- Non-HTTPS fontUrl → 400
138
138
+
- Permission denied (no manageThemes) → 403
139
139
+
- Unauthenticated → 401
140
140
+
- PDS/DB error → 503
141
141
+
142
142
+
### `PUT /api/admin/themes/:rkey`
143
143
+
- Happy path: updates theme, returns 200
144
144
+
- Partial update (no cssOverrides in body) preserves existing cssOverrides
145
145
+
- Unknown rkey → 404
146
146
+
- Same input validation failures as POST → 400
147
147
+
- Permission denied → 403
148
148
+
149
149
+
### `DELETE /api/admin/themes/:rkey`
150
150
+
- Happy path: deletes theme, returns 200
151
151
+
- Unknown rkey → 404
152
152
+
- Theme is defaultLightTheme in policy → 409
153
153
+
- Theme is defaultDarkTheme in policy → 409
154
154
+
- Permission denied → 403
155
155
+
156
156
+
### `PUT /api/admin/theme-policy`
157
157
+
- Happy path create (no existing policy): returns 200
158
158
+
- Happy path update (policy already exists): returns 200
159
159
+
- `defaultLightThemeUri` not in `availableThemes` → 400
160
160
+
- `defaultDarkThemeUri` not in `availableThemes` → 400
161
161
+
- Missing `availableThemes` → 400
162
162
+
- Empty `availableThemes` array → 400
163
163
+
- Missing `defaultLightThemeUri` → 400
164
164
+
- Missing `defaultDarkThemeUri` → 400
165
165
+
- Permission denied → 403
166
166
+
167
167
+
---
168
168
+
169
169
+
## Bruno Collection
170
170
+
171
171
+
New files in `bruno/AppView API/Admin Themes/`:
172
172
+
173
173
+
- `Create Theme.bru`
174
174
+
- `Update Theme.bru`
175
175
+
- `Delete Theme.bru`
176
176
+
- `Update Theme Policy.bru`
177
177
+
178
178
+
All use `{{appview_url}}` for the base URL and include error code documentation.