Skip to content

feat: the forum — old-school boards, modernized#3

Open
tsvb wants to merge 41 commits into
mainfrom
feat/forum
Open

feat: the forum — old-school boards, modernized#3
tsvb wants to merge 41 commits into
mainfrom
feat/forum

Conversation

@tsvb

@tsvb tsvb commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Adds an old-school forum — modernized — to Goose Index: XenForo-style categories → boards → threads → flat replies, the site's first-ever user accounts, and a full moderation kit. Built in-repo (MIT) on the existing Vercel + Neon + Drizzle stack — no new hosting, no new npm dependencies (Resend is called with fetch).

Follows the spec + plan committed at the base of this branch:

  • docs/superpowers/specs/2026-07-15-forum-design.md
  • docs/superpowers/plans/2026-07-15-forum.md

What's in it

Identity (passwordless). Username + email, magic-link sign-in via Resend (dev prints the link to the console). Sessions and login tokens are stored only as sha256 hashes; sliding 90-day sessions; single-use 15-minute tokens. Deliberately site-level (users/sessions/login_tokens) so Phase 4 fan-tracking can reuse it.

The forum. Board index → board (paginated, pinned-first) → thread (20/page, canonical {id}-{slug} redirects) → member profiles. A hand-rolled BBCode parser ([b] [i] [u] [s] [url] [quote] [code], autolinked URLs) renders to a safe React AST — never dangerouslySetInnerHTML, with javascript:/data: schemes refused and an XSS corpus in the tests. Transactional denormalized counters, guarded by a verify check.

The XenForo feel. Like 👍 / Honk 🪿 reactions, per-member unread markers with jump-to-first-unread, deterministic goose-palette identicon avatars, signatures, quoting (nested-quote stripping), and a "who's online" line.

Moderation. Report queue + admin page, soft-delete tombstones (counters untouched), lock/pin/ban, posting throttles (30s/60s by account age, daily caps, link caps for new accounts), a signup honeypot (hidden field + HMAC time-trap), and magic-link issuance rate limits.

Everywhere. Every page renders in all three experience modes (3.0 / 2.0 / 1.0), and every mutation is a plain HTML form + server action — the whole forum works with JavaScript disabled (1.0 is fully operational). New routes are dynamic; /forum + boards join the sitemap.

How it was built

27 tasks across 4 phases (identity → core forum → trimmings → moderation), each TDD'd, individually reviewed, and gated. The suite went from 511 → 642 tests; typecheck clean; production build green; npm run verify passes including four new forum-counter integrity checks. A final whole-branch review caught one cross-task seam bug — banned users could still write a (publicly-displayed) signature — now fixed and verified live.

Before this can be announced (see docs/DEPLOY.md)

  1. Set RESEND_API_KEY, AUTH_EMAIL_FROM, AUTH_SECRET (openssl rand -hex 32) in Vercel.
  2. Verify the sending domain in Resend.
  3. Run the migrations, then npm run make-admin -- <your-email> against prod.
  4. Board taxonomy (The Music: Tour Talk / Setlists & Stats / Tapes & Media · Community: Introductions / Off Topic / Site Feedback) is seeded but easy to edit — final wording is yours.

🤖 Generated with Claude Code

Tim and others added 30 commits July 15, 2026 12:10
XenForo-style categories/boards/threads built in-repo (MIT), magic-link
identity (first accounts on the site), BBCode subset, reactions, unread
markers, generated avatars, standard moderation kit. Server-first: plain
forms + server actions, all three experience modes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Identity (magic links, sessions), core forum (schema, BBCode, pages),
trimmings (reactions, unread, avatars, quoting), moderation kit
(reports, admin, throttles, honeypot). TDD steps with complete code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… emails)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…name-taken)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on timing

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nsert

Add safeBack() to app/forum/actions.ts so the "back" hidden field can only
redirect to same-origin forum paths, closing an open-redirect vector in
reactAction (and giving later back-field consumers a shared, safe helper).

Make toggleReaction's add branch use onConflictDoNothing on the
(post_id, user_id) PK so a raced double-submit is a silent no-op instead
of an unhandled unique-violation throw, matching the action's low-stakes
fall-through intent. Adds a regression test proving the duplicate insert
no longer throws.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keep aria-label in original username case per accessibility spec, while
using lowercased hash for deterministic color + cell computation.
Update determinism test to compare only visual output (fill + rects),
ignoring aria-label, since case variants now differ in the label text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tim and others added 11 commits July 15, 2026 16:23
Add regression tests for quoteBBCode to capture expected behavior:
- Sibling quote blocks: text between/after blocks is preserved
- Unclosed quote tag: doesn't match innermost regex, passes through
- Triple-nested quotes: all innermost pairs stripped, leaving final content

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tes 500'd)

A prior hardening commit exported a synchronous safeBack from app/forum/actions.ts,
which carries "use server" — Next requires every export there to be an async server
action, so the non-async export made every /forum/* route 500 at runtime (vitest and
tsc both passed, masking it). Moved safeBack + its test to lib/forum/safe-back.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ion)

requestLogin's RateLimitError handler returned {status:"error"}, which
loginAction rendered as a distinct visible state from {sent:true}. Since
unknown emails always return "silent" and only real emails accrue enough
issueToken calls to hit the 3/hr cap, submitting the login form 4x let an
attacker distinguish real from unknown emails. Rate-limited real emails
now return "silent" too — indistinguishable from unknown, no token issued
either way, limit still enforced.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ail-change race

A banned user retained a valid session and could still update their
signature (shown forum-wide under all posts) or change their account
email via /forum/settings, since bannedError() only gated the content
mutations, not the settings/email-change actions. Gate both actions at
the requireUser layer using the same bannedError() check, now exported
from lib/forum/mutations.

Also hardens the email-change confirm path: two users racing to claim
the same new address would 500 the second confirmation instead of
failing gracefully as an invalid link.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gooseindex Ready Ready Preview, Comment Jul 15, 2026 10:50pm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant