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

docs: add design doc for ATB-57 theme write API endpoints

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