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

Configure Feed

Select the types of activity you want to include in your feed.

Theme Write API Endpoints — Design#

Linear: ATB-57 Date: 2026-03-02 Status: Approved, ready for implementation


Context#

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.

Depends on: ATB-51 (theme lexicons), ATB-55 (theme read endpoints + DB tables)


Route Placement#

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.


Endpoints#

Method Path Permission
POST /api/admin/themes space.atbb.permission.manageThemes
PUT /api/admin/themes/:rkey space.atbb.permission.manageThemes
DELETE /api/admin/themes/:rkey space.atbb.permission.manageThemes
PUT /api/admin/theme-policy space.atbb.permission.manageThemes

Permission Changes#

Add space.atbb.permission.manageThemes to apps/appview/src/lib/seed-roles.ts:

  • Owner: already has "*" wildcard — no change
  • Admin: add manageThemes to the permissions array
  • Moderator / Member: no change

Input Validation#

Theme (POST and PUT)#

Field Rule
name Required string, non-empty, ≤ 100 graphemes
colorScheme Required, must be "light" or "dark"
tokens Required, must be a non-null object; values must be strings
cssOverrides Optional string (do NOT render until ATB-62 CSS sanitization ships)
fontUrls Optional array of strings; each must start with "https://"

Token keys are not validated against a known list (lenient mode — allows custom/future tokens).

Theme Policy (PUT)#

Field Rule
availableThemes Required non-empty array of { uri: string, cid: string }
defaultLightThemeUri Required string; must be an AT-URI present in availableThemes
defaultDarkThemeUri Required string; must be an AT-URI present in availableThemes
allowUserChoice Optional boolean, defaults true

Endpoint Details#

POST /api/admin/themes#

  1. Parse and validate request body
  2. Get ForumAgent (return 503 if unavailable)
  3. Generate rkey = TID.nextStr()
  4. putRecord on Forum DID's PDS with collection: "space.atbb.forum.theme"
  5. Return { uri, cid } with 201

Does not wait for firehose indexing — the PDS write is the authoritative action.

PUT /api/admin/themes/:rkey#

  1. Parse and validate request body
  2. Look up existing theme by rkey + forumDid in DB (404 if missing)
  3. Get ForumAgent
  4. putRecord with same rkey, preserving createdAt from DB row
  5. Optional fields (cssOverrides, fontUrls, description) fall back to existing DB values if not provided in request
  6. Return { uri, cid } with 200

DELETE /api/admin/themes/:rkey#

  1. Look up theme in DB (404 if missing)
  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
  3. Return 409 if any match
  4. Get ForumAgent
  5. deleteRecord on Forum DID's PDS
  6. Return { success: true } with 200

PUT /api/admin/theme-policy#

Upsert semantics (creates if no policy row exists yet, updates if one does).

  1. Parse and validate request body
  2. Validate defaultLightThemeUri is present in availableThemes (400 if not)
  3. Validate defaultDarkThemeUri is present in availableThemes (400 if not)
  4. Get ForumAgent
  5. putRecord with rkey: "self", collection: "space.atbb.forum.themePolicy"
  6. PDS record structure follows the themeRef wrapper pattern from the lexicon: { theme: { uri, cid } }
  7. Return { uri, cid } with 200

Error Codes#

Status Condition
400 Invalid/missing input field, invalid colorScheme, non-HTTPS fontUrl, default theme not in availableThemes
401 Not authenticated
403 Caller lacks manageThemes permission
404 Theme rkey not found (PUT/DELETE)
409 DELETE attempted on a theme that is the current policy default
503 DB or PDS connectivity error

Tests#

POST /api/admin/themes#

  • Happy path: returns 201 with uri and cid
  • Missing name → 400
  • Empty name → 400
  • name too long (> 100 graphemes) → 400
  • Invalid colorScheme (not light/dark) → 400
  • Missing colorScheme → 400
  • tokens not an object → 400
  • Missing tokens → 400
  • Non-HTTPS fontUrl → 400
  • Permission denied (no manageThemes) → 403
  • Unauthenticated → 401
  • PDS/DB error → 503

PUT /api/admin/themes/:rkey#

  • Happy path: updates theme, returns 200
  • Partial update (no cssOverrides in body) preserves existing cssOverrides
  • Unknown rkey → 404
  • Same input validation failures as POST → 400
  • Permission denied → 403

DELETE /api/admin/themes/:rkey#

  • Happy path: deletes theme, returns 200
  • Unknown rkey → 404
  • Theme is defaultLightTheme in policy → 409
  • Theme is defaultDarkTheme in policy → 409
  • Permission denied → 403

PUT /api/admin/theme-policy#

  • Happy path create (no existing policy): returns 200
  • Happy path update (policy already exists): returns 200
  • defaultLightThemeUri not in availableThemes → 400
  • defaultDarkThemeUri not in availableThemes → 400
  • Missing availableThemes → 400
  • Empty availableThemes array → 400
  • Missing defaultLightThemeUri → 400
  • Missing defaultDarkThemeUri → 400
  • Permission denied → 403

Bruno Collection#

New files in bruno/AppView API/Admin Themes/:

  • Create Theme.bru
  • Update Theme.bru
  • Delete Theme.bru
  • Update Theme Policy.bru

All use {{appview_url}} for the base URL and include error code documentation.