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
4
fork

Configure Feed

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

at main 197 lines 6.7 kB view raw view rendered
1# ATB-55: Theme Read API Endpoints — Design 2 3**Date:** 2026-03-02 4**Status:** Approved 5**Linear:** ATB-55 6**Depends on:** ATB-51 (theme + themePolicy lexicons — already shipped) 7 8--- 9 10## Summary 11 12Add database tables, firehose indexers, and read-only REST endpoints for theme and 13theme policy data. The web UI (and future mobile clients) will consume these to 14resolve which CSS tokens to render per request. 15 16--- 17 18## Database Schema 19 20Three new tables added to both `packages/db/src/schema.ts` (Postgres) and 21`packages/db/src/schema.sqlite.ts` (SQLite). All follow the existing 22`(did, rkey, cid, indexed_at)` pattern. 23 24### `themes` 25 26| Column | Postgres | SQLite | Notes | 27|---|---|---|---| 28| `id` | `bigserial PK` | `integer PK autoIncrement` | | 29| `did` | `text NOT NULL` | `text NOT NULL` | Forum DID | 30| `rkey` | `text NOT NULL` | `text NOT NULL` | TID key | 31| `cid` | `text NOT NULL` | `text NOT NULL` | | 32| `name` | `text NOT NULL` | `text NOT NULL` | | 33| `colorScheme` | `text NOT NULL` | `text NOT NULL` | `"light"` or `"dark"` | 34| `tokens` | `jsonb NOT NULL` | `text NOT NULL` | SQLite: JSON string | 35| `cssOverrides` | `text` | `text` | Optional raw CSS | 36| `fontUrls` | `text[] ` | `text` | SQLite: JSON string array | 37| `createdAt` | `timestamp` | `integer (timestamp)` | | 38| `indexedAt` | `timestamp` | `integer (timestamp)` | | 39 40Indexes: `UNIQUE(did, rkey)` 41 42### `theme_policies` 43 44Singleton per forum (rkey is always `"self"`). 45 46| Column | Postgres | SQLite | Notes | 47|---|---|---|---| 48| `id` | `bigserial PK` | `integer PK autoIncrement` | | 49| `did` | `text NOT NULL` | `text NOT NULL` | Forum DID | 50| `rkey` | `text NOT NULL` | `text NOT NULL` | Always `"self"` | 51| `cid` | `text NOT NULL` | `text NOT NULL` | | 52| `defaultLightThemeUri` | `text NOT NULL` | `text NOT NULL` | AT-URI | 53| `defaultDarkThemeUri` | `text NOT NULL` | `text NOT NULL` | AT-URI | 54| `allowUserChoice` | `boolean NOT NULL` | `integer (boolean)` | | 55| `indexedAt` | `timestamp` | `integer (timestamp)` | | 56 57Indexes: `UNIQUE(did, rkey)` 58 59### `theme_policy_available_themes` 60 61Normalized join table for the `availableThemes` array from themePolicy records. 62Enables SQL-level filtering in `GET /api/themes` without application-layer iteration. 63 64| Column | Postgres | SQLite | Notes | 65|---|---|---|---| 66| `policyId` | `bigint FK → theme_policies.id ON DELETE CASCADE` | `integer FK` | | 67| `themeUri` | `text NOT NULL` | `text NOT NULL` | AT-URI of the theme | 68| `themeCid` | `text NOT NULL` | `text NOT NULL` | CID for integrity | 69 70Primary key: `(policyId, themeUri)` 71 72--- 73 74## Firehose Indexer 75 76Two new `CollectionConfig` entries in `apps/appview/src/lib/indexer.ts`, following 77the pattern established by `categoryConfig`, `roleConfig`, etc. 78 79### `space.atbb.forum.theme` 80 81- `toInsertValues`: maps `name`, `colorScheme`, `tokens` (JSON.stringify for SQLite, 82 raw object for Postgres), `cssOverrides`, `fontUrls` (array for Postgres, 83 JSON.stringify for SQLite), `createdAt`, `indexedAt` 84- `toUpdateValues`: same fields minus `did` / `rkey` / `createdAt` 85- No `afterUpsert` needed 86 87### `space.atbb.forum.themePolicy` 88 89- `toInsertValues`: maps `defaultLightTheme.theme.uri`, `defaultDarkTheme.theme.uri`, 90 `allowUserChoice`, `indexedAt` 91- `afterUpsert`: atomically replaces `theme_policy_available_themes` rows for this 92 policy — DELETE existing rows by `policyId`, then INSERT one row per entry in 93 `record.availableThemes`. Same pattern as `roleConfig.afterUpsert` for permissions. 94 95Both collections registered in `firehose.ts` `createHandlerRegistry()`. 96 97--- 98 99## API Endpoints 100 101New file: `apps/appview/src/routes/themes.ts` 102Factory function: `createThemesRoutes(ctx: AppContext)` 103Registered in `routes/index.ts` as `.route("/themes", createThemesRoutes(ctx))` 104 105### `GET /api/themes` 106 107Returns themes filtered to those in `themePolicy.availableThemes` via SQL join. 108Returns `{ themes: [] }` when no policy exists (no 404). 109 110Query: 111```sql 112SELECT t.* FROM themes t 113INNER JOIN theme_policy_available_themes tpa 114 ON tpa.theme_uri = ('at://' || t.did || '/space.atbb.forum.theme/' || t.rkey) 115INNER JOIN theme_policies tp ON tp.id = tpa.policy_id 116``` 117 118Response: `{ themes: [{ id, uri, name, colorScheme, indexedAt }] }` 119(Token summary only — full tokens are in the single-theme endpoint.) 120 121Error codes: 500 with structured logging for DB errors. 122 123### `GET /api/themes/:rkey` 124 125Returns full theme data for a single theme identified by its rkey. 126The Forum DID comes from `ctx.config.forumDid`. 127 128Validation: 400 for empty/missing rkey (rkeys are TIDs, not BigInts — use string 129validation, not `parseBigIntParam`). 130 131Response: `{ id, uri, name, colorScheme, tokens, cssOverrides, fontUrls, createdAt, indexedAt }` 132 133Error codes: 400 (invalid rkey), 404 (theme not found), 500 (DB error). 134 135### `GET /api/theme-policy` 136 137Returns the forum's singleton themePolicy record with its `availableThemes` list. 138Returns 404 when no policy exists. 139 140Queries `theme_policies` then assembles `availableThemes` from 141`theme_policy_available_themes` join. 142 143Response: 144```json 145{ 146 "defaultLightThemeUri": "at://...", 147 "defaultDarkThemeUri": "at://...", 148 "allowUserChoice": true, 149 "availableThemes": [{ "uri": "at://...", "cid": "..." }] 150} 151``` 152 153Error codes: 404 (no policy), 500 (DB error). 154 155--- 156 157## Tests 158 159File: `apps/appview/src/routes/__tests__/themes.test.ts` 160 161### Happy path 162 163- `GET /api/themes` returns only themes listed in `availableThemes` (not all themes in DB) 164- `GET /api/themes/:rkey` returns full token set for a known theme 165- `GET /api/theme-policy` returns correct `allowUserChoice` and `availableThemes` 166 167### Error / edge cases 168 169- `GET /api/themes` returns `{ themes: [] }` when no policy exists 170- `GET /api/themes/:rkey` returns 404 for unknown rkey 171- `GET /api/themes/:rkey` returns 400 for empty rkey 172- `GET /api/theme-policy` returns 404 when no policy exists 173- `GET /api/themes` does **not** include a theme that exists in DB but is absent from `availableThemes` 174 175--- 176 177## Bruno Collection 178 179New folder: `bruno/AppView API/Themes/` 180 181| File | Method | URL | 182|---|---|---| 183| `List Available Themes.bru` | GET | `{{appview_url}}/api/themes` | 184| `Get Theme.bru` | GET | `{{appview_url}}/api/themes/{{theme_rkey}}` | 185| `Get Theme Policy.bru` | GET | `{{appview_url}}/api/theme-policy` | 186 187Each file documents all HTTP status codes the endpoint can return and uses 188environment variables for all URLs and test data. 189 190--- 191 192## Out of Scope (ATB-55) 193 194- Write endpoints (`POST /api/themes`, `PUT /api/theme-policy`, etc.) — separate ticket 195- User theme preference (`PATCH /api/membership/theme`) — separate ticket 196- CSS sanitization for `cssOverrides` — required before admin write endpoints ship 197- Web UI theme resolution and injection into `BaseLayout`