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.

docs: add ATB-55 theme read API design doc

Records approved design for themes table, theme_policies table,
theme_policy_available_themes join table, firehose indexer configs,
and GET /api/themes + GET /api/themes/:rkey + GET /api/theme-policy endpoints.

Malpercio 55f092d6 3ff848fb

+197
+197
docs/plans/2026-03-02-theme-api-design.md
··· 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 + 12 + Add database tables, firehose indexers, and read-only REST endpoints for theme and 13 + theme policy data. The web UI (and future mobile clients) will consume these to 14 + resolve which CSS tokens to render per request. 15 + 16 + --- 17 + 18 + ## Database Schema 19 + 20 + Three 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 + 40 + Indexes: `UNIQUE(did, rkey)` 41 + 42 + ### `theme_policies` 43 + 44 + Singleton 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 + 57 + Indexes: `UNIQUE(did, rkey)` 58 + 59 + ### `theme_policy_available_themes` 60 + 61 + Normalized join table for the `availableThemes` array from themePolicy records. 62 + Enables 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 + 70 + Primary key: `(policyId, themeUri)` 71 + 72 + --- 73 + 74 + ## Firehose Indexer 75 + 76 + Two new `CollectionConfig` entries in `apps/appview/src/lib/indexer.ts`, following 77 + the 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 + 95 + Both collections registered in `firehose.ts` `createHandlerRegistry()`. 96 + 97 + --- 98 + 99 + ## API Endpoints 100 + 101 + New file: `apps/appview/src/routes/themes.ts` 102 + Factory function: `createThemesRoutes(ctx: AppContext)` 103 + Registered in `routes/index.ts` as `.route("/themes", createThemesRoutes(ctx))` 104 + 105 + ### `GET /api/themes` 106 + 107 + Returns themes filtered to those in `themePolicy.availableThemes` via SQL join. 108 + Returns `{ themes: [] }` when no policy exists (no 404). 109 + 110 + Query: 111 + ```sql 112 + SELECT t.* FROM themes t 113 + INNER JOIN theme_policy_available_themes tpa 114 + ON tpa.theme_uri = ('at://' || t.did || '/space.atbb.forum.theme/' || t.rkey) 115 + INNER JOIN theme_policies tp ON tp.id = tpa.policy_id 116 + ``` 117 + 118 + Response: `{ themes: [{ id, uri, name, colorScheme, indexedAt }] }` 119 + (Token summary only — full tokens are in the single-theme endpoint.) 120 + 121 + Error codes: 500 with structured logging for DB errors. 122 + 123 + ### `GET /api/themes/:rkey` 124 + 125 + Returns full theme data for a single theme identified by its rkey. 126 + The Forum DID comes from `ctx.config.forumDid`. 127 + 128 + Validation: 400 for empty/missing rkey (rkeys are TIDs, not BigInts — use string 129 + validation, not `parseBigIntParam`). 130 + 131 + Response: `{ id, uri, name, colorScheme, tokens, cssOverrides, fontUrls, createdAt, indexedAt }` 132 + 133 + Error codes: 400 (invalid rkey), 404 (theme not found), 500 (DB error). 134 + 135 + ### `GET /api/theme-policy` 136 + 137 + Returns the forum's singleton themePolicy record with its `availableThemes` list. 138 + Returns 404 when no policy exists. 139 + 140 + Queries `theme_policies` then assembles `availableThemes` from 141 + `theme_policy_available_themes` join. 142 + 143 + Response: 144 + ```json 145 + { 146 + "defaultLightThemeUri": "at://...", 147 + "defaultDarkThemeUri": "at://...", 148 + "allowUserChoice": true, 149 + "availableThemes": [{ "uri": "at://...", "cid": "..." }] 150 + } 151 + ``` 152 + 153 + Error codes: 404 (no policy), 500 (DB error). 154 + 155 + --- 156 + 157 + ## Tests 158 + 159 + File: `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 + 179 + New 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 + 187 + Each file documents all HTTP status codes the endpoint can return and uses 188 + environment 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`