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: address code review feedback on PR #3

Critical fixes:
- Add note confirming Jetstream wildcard syntax is supported

Important fixes:
- Correct post grapheme limit (300, not 3000 chars)
- Acknowledge that limit may need revisiting for forum posts
- Fix categoryId schema mismatch (posts use forumUri)
- Update event routing to use forumUri with implementation note
- Fix Hono JSX rendering API (use JSX directly in streamSSE, not renderToString)
- Clarify "zero custom client-side JS" (HTMX itself is ~14KB)
- Update AT Proto record description (supports updates via putRecord)
- Fix empty <tr> sentinel by moving SSE attributes to <tbody>

All changes address feedback from PR review to ensure technical
accuracy before merge.

+27 -20
+3 -3
docs/research/phpbb-research.md
··· 85 85 86 86 | Feature | Details | atBB Status | 87 87 |---------|---------|-------------| 88 - | **BBCode formatting** | Bold, italic, lists, links, images, etc. | Missing — plain text only (3000 char limit) | 88 + | **BBCode formatting** | Bold, italic, lists, links, images, etc. | Missing — plain text only (300 grapheme limit) | 89 89 | **Rich text / Markdown** | Formatted text in posts | Missing (noted as future work) | 90 90 | **Smilies / Emoticons** | Inline emoji/emoticon insertion | Missing | 91 91 | **Post preview** | Preview post before submitting | Missing | ··· 94 94 | **Polls** | Create polls with multiple options, voting, time limits | Missing | 95 95 | **Topic icons** | Visual icon next to topic title | Missing | 96 96 | **Drafts** | Save unfinished posts for later | Missing | 97 - | **Post editing** | Edit your own posts after submission | Missing (AT Proto records are immutable-ish) | 97 + | **Post editing** | Edit your own posts after submission | Architecturally feasible (AT Proto supports updates via `putRecord` — CID changes, firehose emits update event) | 98 98 | **Post edit history** | Track edit reasons and history | Missing | 99 99 | **Word censoring** | Auto-replace banned words | Missing | 100 100 | **Custom BBCodes** | Admin-defined formatting codes | Missing | ··· 340 340 #### P0 — Essential for MVP Parity 341 341 These are features without which the forum would feel fundamentally broken: 342 342 343 - 1. **Rich text / Markdown in posts** — Plain text only is not viable. Users expect at minimum bold, italic, links, code blocks, and lists. Markdown is the modern standard. 343 + 1. **Rich text / Markdown in posts** — Plain text only is not viable. Users expect at minimum bold, italic, links, code blocks, and lists. Markdown is the modern standard. Note: The current `maxGraphemes: 300` limit in `post.yaml` is closer to a tweet than a forum post — this constraint may need revisiting before implementing rich text (phpBB posts typically support thousands of characters). 344 344 345 345 2. **Unread tracking** — phpBB tracks read/unread state per-user across sessions. Without this, users can't efficiently follow conversations. This is the #1 quality-of-life feature for forum users. 346 346
+24 -17
docs/research/realtime-architecture.md
··· 139 139 events.emit(`topic:${post.rootPostId}`, { type: "newReply", post }); 140 140 } else { 141 141 // It's a new topic — broadcast to the category channel 142 - events.emit(`category:${post.categoryId}`, { type: "newTopic", post }); 142 + // NOTE: Current schema has `forumUri` not `categoryId`. To route 143 + // "new topic in category X" events, need to either: (a) resolve 144 + // category from forum metadata, or (b) add categoryUri to posts table. 145 + // For now, broadcast to forum-level channel: 146 + events.emit(`forum:${post.forumUri}`, { type: "newTopic", post }); 143 147 } 144 148 145 149 // Always broadcast to global (for notification badges, etc.) ··· 186 190 ``` 187 191 188 192 This catches `space.atbb.post`, `space.atbb.forum.forum`, `space.atbb.forum.category`, `space.atbb.membership`, `space.atbb.reaction`, `space.atbb.modAction` — everything in the atBB namespace. 193 + 194 + **Note:** Wildcard syntax is supported as long as the prefix (`space.atbb`) passes NSID validation, which it does. Jetstream allows up to 100 collection filters per connection. If wildcard filtering proves problematic in practice, the seven collections can be enumerated explicitly. 189 195 190 196 ### Cursor Management 191 197 ··· 245 251 246 252 // Subscribe to topic events 247 253 const handler = async (event: ForumEvent) => { 248 - const html = renderEventHtml(event); // JSX → HTML string 254 + // Hono's streamSSE accepts JSX directly in the data field 249 255 await stream.writeSSE({ 250 256 event: event.type, // "newReply", "newReaction", "postDeleted" 251 - data: html, 257 + data: renderEventComponent(event), // Returns JSX element 252 258 }); 253 259 }; 254 260 ··· 272 278 }, 30_000); 273 279 274 280 const handler = async (event: ForumEvent) => { 275 - const html = renderEventHtml(event); 276 - await stream.writeSSE({ event: event.type, data: html }); 281 + await stream.writeSSE({ 282 + event: event.type, 283 + data: renderEventComponent(event) 284 + }); 277 285 }; 278 286 279 287 firehoseConsumer.events.on(`category:${categoryId}`, handler); ··· 296 304 297 305 ### HTML Fragment Rendering 298 306 299 - The key insight: SSE events carry **pre-rendered HTML fragments**, not JSON. This is what makes HTMX SSE zero-JS on the client. 307 + The key insight: SSE events carry **pre-rendered HTML fragments**, not JSON. This is what makes HTMX SSE zero custom client-side JS. 300 308 301 309 ```typescript 302 310 // packages/web/src/components/ReplyCard.tsx ··· 327 335 </article> 328 336 ); 329 337 330 - // Render to string for SSE 331 - function renderReplyHtml(post: IndexedPost): string { 332 - // Hono JSX can render to string server-side 333 - return renderToString(<ReplyCard 338 + // Render JSX component for SSE 339 + function renderReplyComponent(post: IndexedPost) { 340 + // Hono's streamSSE accepts JSX directly — no manual string conversion needed 341 + return <ReplyCard 334 342 author={post.authorHandle} 335 343 authorDid={post.authorDid} 336 344 text={post.text} 337 345 createdAt={post.createdAt} 338 - />); 346 + />; 347 + // Alternative if string needed: component.toString() 339 348 } 340 349 ``` 341 350 ··· 418 427 <th>Last Post</th> 419 428 </tr> 420 429 </thead> 421 - <tbody id="topic-list-body"> 430 + <tbody id="topic-list-body" 431 + sse-swap="newTopic" 432 + hx-swap="afterbegin"> 422 433 {topics.map((topic) => <TopicRow {...topic} />)} 423 - 424 - {/* New topics appear at the top */} 425 - <tr sse-swap="newTopic" hx-swap="afterbegin" hx-target="#topic-list-body"> 426 - </tr> 427 434 </tbody> 428 435 </table> 429 436 ··· 591 598 | **Discourse** | Custom WebSocket implementation via MessageBus gem. Bolt-on architecture, requires Redis, sticky sessions. | 592 599 | **Flarum** | Pusher.com integration (third-party SaaS). Adds external dependency and cost. | 593 600 | **NodeBB** | Socket.io (WebSocket). Heavy client-side JS framework. | 594 - | **atBB** | Protocol-native firehose → in-process event bus → SSE → HTMX declarative swap. Zero client-side JS. The real-time stream is architecturally intrinsic, not bolted on. | 601 + | **atBB** | Protocol-native firehose → in-process event bus → SSE → HTMX declarative swap. Zero *custom* client-side JS (HTMX itself is ~14KB). The real-time stream is architecturally intrinsic, not bolted on. | 595 602 596 603 The AT Protocol firehose means atBB doesn't *add* real-time — it *is* real-time. The firehose consumer that indexes posts is the same component that powers live updates. There's no separate infrastructure, no Redis, no WebSocket server, no client-side framework. Just HTML attributes. 597 604