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.

Address PR review comments for theming plan

Fix lexicon convention issues identified in code review:
- Use knownValues (not implicit enum) for colorScheme field
- Wrap theme references with strongRef for CID integrity checks
- Separate themePolicy into its own singleton to prevent forum record bloat
- Note that forum.theme must be added to CLAUDE.md ownership list

Add missing implementation details:
- Elevate CSS sanitization from open question to mandatory Phase 3 gate
- Document cache key must include resolved color scheme
- Constrain fontUrls to HTTPS with allowlist consideration
- Add database schema notes for themes and memberships tables
- Clarify AppView endpoints are REST, not XRPC

Updates theme resolution waterfall to show CID integrity checks via strongRef.

Malpercio 604bc03c f7a38315

+79 -54
+79 -54
docs/theming-plan.md
··· 21 21 ### How Themes Work 22 22 23 23 ``` 24 - Forum DID PDS Web Server (Hono) 25 - ┌──────────────────────────┐ ┌───────────────────────────────────┐ 26 - │ space.atbb.forum.theme │ │ │ 27 - │ (multiple records) │──cache─▶│ Theme resolution per request: │ 28 - │ │ │ 1. User pref (membership record) │ 29 - │ space.atbb.forum.forum │ │ 2. Color scheme (cookie/header) │ 30 - │ .themePolicy { │──cache─▶│ 3. Forum default (themePolicy) │ 31 - │ availableThemes │ │ 4. Hardcoded fallback │ 32 - │ defaultLightTheme │ │ │ │ 33 - │ defaultDarkTheme │ │ ▼ │ 34 - │ allowUserChoice │ │ <style>:root { --tokens }</style>│ 35 - │ } │ │ + /static/theme.css │ 36 - └──────────────────────────┘ └───────────────────────────────────┘ 24 + Forum DID PDS Web Server (Hono) 25 + ┌─────────────────────────────┐ ┌───────────────────────────────────┐ 26 + │ space.atbb.forum.theme │ │ │ 27 + │ (multiple records) │──cache─▶│ Theme resolution per request: │ 28 + │ │ │ 1. User pref (membership record) │ 29 + │ space.atbb.forum.themePolicy│ │ 2. Color scheme (cookie/header) │ 30 + │ (singleton) { │──cache─▶│ 3. Forum default (themePolicy) │ 31 + │ availableThemes │ │ 4. Hardcoded fallback │ 32 + │ defaultLightTheme │ │ │ │ 33 + │ defaultDarkTheme │ │ ▼ │ 34 + │ allowUserChoice │ │ <style>:root { --tokens }</style>│ 35 + │ } │ │ + /static/theme.css │ 36 + └─────────────────────────────┘ └───────────────────────────────────┘ 37 37 ``` 38 38 39 39 1. **Theme records** live on the Forum DID's PDS as `space.atbb.forum.theme` records. A forum can have many saved themes. 40 - 2. **Theme policy** on the `forum.forum` record controls which themes are available to users, which are the defaults for light/dark mode, and whether users can choose their own. 40 + 2. **Theme policy** is a separate singleton record (`space.atbb.forum.themePolicy`) that controls which themes are available to users, which are the defaults for light/dark mode, and whether users can choose their own. 41 41 3. **On each request**, the web server resolves which theme to render (see [Theme Resolution](#theme-resolution) below) and injects the winning theme's CSS custom properties into a `<style>` block in `<head>`. 42 42 4. **A single base stylesheet** (`theme.css`) references only custom properties — never hardcoded colors or sizes. Swapping property values completely changes the look. 43 43 5. **Optional per-theme CSS overrides** can extend the base for structural changes (e.g., sidebar layout vs. top-nav), stored as a `cssOverrides` string in the theme record. ··· 182 182 key: tid # Multiple themes per forum 183 183 fields: 184 184 name: string (required) # "Neobrutal Default", "Dark Mode", etc. 185 - colorScheme: string (required) # "light" | "dark" — which mode this theme targets 185 + colorScheme: 186 + type: string (required) 187 + knownValues: ["light", "dark"] # Which mode this theme targets (extensible) 186 188 tokens: map<string, string> # Design token key-value pairs 187 189 cssOverrides: string (optional)# Raw CSS for structural overrides 188 - fontUrls: array<string> (opt) # Google Fonts or self-hosted font URLs 190 + fontUrls: array<string> (opt) # HTTPS URLs for Google Fonts or self-hosted fonts 189 191 createdAt: datetime 190 192 updatedAt: datetime 191 193 ``` 192 194 193 - **Record ownership:** Forum DID (same as `forum.forum`, `forum.category`). 195 + **Record ownership:** Forum DID (same as `forum.forum`, `forum.category`). When implemented, `forum.theme` must be added to the canonical ownership list in CLAUDE.md. 194 196 195 197 **Why `tid` key?** Forums can have many saved themes (like phpBB's theme gallery). The admin curates which ones are available via the theme policy below. 196 198 197 - **Why `colorScheme` instead of `active`?** A single `active` boolean is too limiting. Forums need separate defaults for light and dark mode, plus a curated list of user-selectable themes. The `colorScheme` field tags each theme so the resolution logic knows which mode it serves. The `themePolicy` on the forum record handles the rest. 199 + **Why `colorScheme` instead of `active`?** A single `active` boolean is too limiting. Forums need separate defaults for light and dark mode, plus a curated list of user-selectable themes. The `colorScheme` field tags each theme so the resolution logic knows which mode it serves. The theme policy handles the rest. 198 200 199 - ### Extended: `space.atbb.forum.forum` 201 + ### New: `space.atbb.forum.themePolicy` 200 202 201 - Add a `themePolicy` object to the existing forum singleton record: 203 + A new singleton record on the Forum DID for theme configuration, separate from the main forum record to allow independent updates without invalidating `strongRef`s to the forum record. 202 204 203 205 ```yaml 204 - # New fields on forum.forum 205 - themePolicy: 206 - type: object 207 - properties: 208 - availableThemes: # AT-URIs of themes admins have enabled for users 209 - type: array 210 - items: string (at-uri) 211 - defaultLightTheme: # AT-URI of the default light-mode theme 212 - type: string (at-uri) 213 - defaultDarkTheme: # AT-URI of the default dark-mode theme 214 - type: string (at-uri) 215 - allowUserChoice: # Can users pick their own theme? 216 - type: boolean 217 - default: true 206 + lexiconId: space.atbb.forum.themePolicy 207 + key: literal:self # Singleton — one per forum 208 + 209 + # Named def for theme references 210 + defs: 211 + themeRef: 212 + type: object 213 + required: [theme] 214 + properties: 215 + theme: 216 + type: ref 217 + ref: com.atproto.repo.strongRef # CID integrity check for theme records 218 + 219 + fields: 220 + availableThemes: # Themes admins have enabled for users 221 + type: array 222 + items: 223 + type: ref 224 + ref: '#themeRef' 225 + defaultLightTheme: # Default light-mode theme 226 + type: ref 227 + ref: '#themeRef' 228 + defaultDarkTheme: # Default dark-mode theme 229 + type: ref 230 + ref: '#themeRef' 231 + allowUserChoice: # Can users pick their own theme? 232 + type: boolean 233 + default: true 234 + updatedAt: datetime 218 235 ``` 236 + 237 + **Record ownership:** Forum DID. 219 238 220 239 The admin's saved themes may outnumber the available list — `availableThemes` is the curated subset exposed to users. Both `defaultLightTheme` and `defaultDarkTheme` must be members of `availableThemes`. 221 240 ··· 226 245 ```yaml 227 246 # New optional field on membership 228 247 preferredTheme: 229 - type: string (at-uri, optional) # AT-URI of user's chosen theme, null = follow forum defaults 248 + type: ref (optional) 249 + ref: com.atproto.repo.strongRef # strongRef to space.atbb.forum.theme record 250 + # Null = follow forum defaults 230 251 ``` 231 252 232 - This lives on the **user's PDS** (they own their membership record), so theme preference is portable — leave a forum and rejoin, your preference is still there. 253 + This lives on the **user's PDS** (they own their membership record), so theme preference is portable — leave a forum and rejoin, your preference is still there. Uses `strongRef` for CID integrity — if the theme record is updated, stale preferences are detected. 233 254 234 255 --- 235 256 ··· 261 282 - The editor itself is an HTMX-driven form. Token changes POST to the server, which returns an updated `<style>` block for the preview panel via an `hx-swap`. 262 283 - No client-side JS framework needed — HTMX + server rendering is sufficient for the live preview workflow. 263 284 - Theme JSON import/export is just the `tokens` + `cssOverrides` + `fontUrls` + `colorScheme` fields serialized. 264 - - Theme policy changes (defaults, available list, allowUserChoice) write back to the `forum.forum` record on the Forum DID's PDS. 285 + - Theme policy changes (defaults, available list, allowUserChoice) write to the `space.atbb.forum.themePolicy` singleton on the Forum DID's PDS. 265 286 266 287 --- 267 288 268 289 ## AppView API Endpoints 269 290 270 - Theme data flows through the AppView like all other forum data. New endpoints: 291 + Theme data flows through the AppView like all other forum data. New endpoints (AppView REST, not XRPC — consistent with existing `/api/forum`, `/api/categories` patterns): 271 292 272 293 ### Read Endpoints 273 294 ··· 275 296 |----------|------|-------------| 276 297 | `GET /api/themes` | Public | List available themes (filtered by `themePolicy.availableThemes`). Returns name, colorScheme, and token summary for each. | 277 298 | `GET /api/themes/:rkey` | Public | Get a single theme's full token set, cssOverrides, and fontUrls. | 278 - | `GET /api/forum` (existing) | Public | Already returns forum metadata — now also includes `themePolicy` (defaults, available list, allowUserChoice). | 299 + | `GET /api/theme-policy` | Public | Get the forum's theme policy (available themes, defaults for light/dark, allowUserChoice). | 279 300 280 301 ### Write Endpoints 281 302 ··· 284 305 | `POST /api/themes` | Admin | Create a new theme record on Forum DID's PDS. | 285 306 | `PUT /api/themes/:rkey` | Admin | Update an existing theme's tokens, name, colorScheme, etc. | 286 307 | `DELETE /api/themes/:rkey` | Admin | Delete a theme. Fails if it's currently a default. | 287 - | `PUT /api/forum/theme-policy` | Admin | Update `themePolicy` on the forum record (available list, defaults, allowUserChoice). | 308 + | `PUT /api/theme-policy` | Admin | Update the `themePolicy` singleton (available list, defaults, allowUserChoice). | 288 309 | `PATCH /api/membership/theme` | User | Set `preferredTheme` on the caller's membership record (writes to their PDS). Pass `null` to clear. | 289 310 290 311 ### Caching 291 312 292 313 The web server caches resolved theme data aggressively since themes change rarely: 293 314 294 - - **Theme tokens:** Cached in-memory on the web server, keyed by AT-URI. Invalidated when the AppView receives a firehose event for `space.atbb.forum.theme` records. 315 + - **Theme tokens:** Cached in-memory on the web server, keyed by AT-URI **and resolved color scheme** (light/dark). Cache key must include color scheme to prevent serving a cached light response to dark-mode users. Use `Vary: Cookie` or equivalent for HTTP caching. 295 316 - **Theme policy:** Cached alongside forum metadata. Same invalidation path. 296 317 - **User preference:** Looked up from the AppView's indexed `membership` records (local DB query, not a PDS fetch per request). 297 318 ··· 425 446 Is the user logged in? 426 447 AND has a preferredTheme set on their membership record? 427 448 AND does the forum's themePolicy.allowUserChoice == true? 428 - AND is preferredTheme still in themePolicy.availableThemes? 449 + AND is preferredTheme.uri still in themePolicy.availableThemes? 450 + AND does preferredTheme.cid match current theme record (integrity check)? 429 451 → Use their preferred theme. 430 452 431 453 2. Color scheme default ··· 434 456 b. HTTP header: Sec-CH-Prefers-Color-Scheme (client hint) 435 457 c. Default: light 436 458 437 - → Use themePolicy.defaultDarkTheme or defaultLightTheme accordingly. 459 + → Use themePolicy.defaultDarkTheme or defaultLightTheme accordingly 460 + (with CID integrity check via strongRef). 438 461 439 462 3. Hardcoded fallback 440 463 If no theme policy exists or the resolved theme can't be loaded: ··· 487 510 - No admin editor, no dynamic themes — just ship a good-looking default 488 511 489 512 ### Theme Phase 2: Light/Dark + Token System 490 - - Define `space.atbb.forum.theme` lexicon (with `colorScheme` field) 491 - - Add `themePolicy` to `space.atbb.forum.forum` lexicon 513 + - Define `space.atbb.forum.theme` lexicon (with `colorScheme` field using `knownValues`) 514 + - Define `space.atbb.forum.themePolicy` lexicon as separate singleton (with `themeRef` strongRef wrapper) 492 515 - Build `tokensToCss()` utility 493 516 - Ship built-in preset JSON files (neobrutal light + dark, clean light + dark, classic) 494 517 - Load theme policy + resolved theme from Forum DID's PDS (with caching) 495 518 - Inject tokens dynamically in `BaseLayout` based on theme resolution waterfall 496 519 - Add light/dark toggle (cookie-based, vanilla JS, ~6 lines) 497 520 - Add `Sec-CH-Prefers-Color-Scheme` client hint support as fallback 498 - - AppView endpoints: `GET /api/themes`, `GET /api/themes/:rkey` 521 + - AppView endpoints: `GET /api/themes`, `GET /api/themes/:rkey`, `GET /api/theme-policy` (REST, not XRPC) 499 522 500 523 ### Theme Phase 3: Admin Theme Management 501 - - Admin endpoints: `POST/PUT/DELETE /api/themes`, `PUT /api/forum/theme-policy` 524 + - **CSS sanitization (mandatory gate):** Server-side sanitization for `cssOverrides` and freeform `tokens` map before rendering. Raw CSS in `<style>` tags is a real exfiltration vector via `url()`, `@import`, `@font-face`. Must sanitize before this phase ships. 525 + - Admin endpoints: `POST/PUT/DELETE /api/themes`, `PUT /api/forum/theme-policy` (AppView REST, not XRPC) 502 526 - Theme list management UI (create, duplicate, delete, availability toggles) 503 527 - Default light/dark assignment dropdowns 504 528 - `allowUserChoice` kill-switch toggle 505 529 - Token editor with grouped controls + live preview via HTMX 506 530 - Import/export (JSON download/upload) 531 + - Database additions: `themes` table following `(did, rkey, cid, indexed_at)` pattern 507 532 508 533 ### Theme Phase 4: User Choice 509 534 - Add `preferredTheme` field to `space.atbb.membership` lexicon 510 - - User endpoint: `PATCH /api/membership/theme` 535 + - User endpoint: `PATCH /api/membership/theme` (AppView REST, not XRPC) 511 536 - Theme picker UI for logged-in users (dropdown in settings or site header) 512 537 - "Auto (follow forum default)" option to clear preference 538 + - Database additions: `preferred_theme_uri` column on `memberships` table (nullable) 513 539 - AppView indexes `preferredTheme` from membership records for fast lookup 514 540 515 541 ### Theme Phase 5: Polish ··· 522 548 523 549 ## Open Questions 524 550 525 - 1. **Font loading strategy.** Google Fonts is easy but has privacy implications for self-hosters. Should we bundle a few default fonts, allow self-hosted uploads, or both? 526 - 2. **CSS override sandboxing.** Raw CSS in `cssOverrides` could break layouts or introduce XSS (via `url()`, `expression()`, etc.). Need a sanitization strategy — maybe a CSS parser that strips dangerous properties. 527 - 3. **Theme record size limits.** AT Proto records have size limits. If `cssOverrides` gets large, might need to store it as a blob reference instead of inline. 528 - 4. **Build-time vs. runtime tokens.** The plan above is fully runtime (no CSS build per theme). This is simpler but means we can't use tools like Tailwind for the base styles. Is that acceptable? 529 - 5. **Theme migration on updates.** When atBB ships new tokens in a release (e.g., a new component token), existing saved themes won't have values for them. Need a merge strategy — probably fall back to the preset's value for any missing token. 530 - 6. **Stale user preferences.** If an admin removes a theme from `availableThemes` while users have it as their `preferredTheme`, the resolution waterfall handles this gracefully (falls through to the color scheme default). But should we also notify affected users or silently degrade? 551 + 1. **Font loading strategy.** Google Fonts is easy but has privacy implications for self-hosters. Should we bundle a few default fonts, allow self-hosted uploads, or both? **Security note:** `fontUrls` must be constrained to HTTPS and consider an allowlist (Google Fonts, self-hosted paths) — arbitrary font URLs leak user IPs to third parties. 552 + 2. **Theme record size limits.** AT Proto records have size limits. If `cssOverrides` gets large, might need to store it as a blob reference instead of inline. 553 + 3. **Build-time vs. runtime tokens.** The plan above is fully runtime (no CSS build per theme). This is simpler but means we can't use tools like Tailwind for the base styles. Is that acceptable? 554 + 4. **Theme migration on updates.** When atBB ships new tokens in a release (e.g., a new component token), existing saved themes won't have values for them. Need a merge strategy — probably fall back to the preset's value for any missing token. 555 + 5. **Stale user preferences.** If an admin removes a theme from `availableThemes` while users have it as their `preferredTheme`, the resolution waterfall handles this gracefully (falls through to the color scheme default). But should we also notify affected users or silently degrade?