ATProto Forum Software — Project Plan#
atBB — A BB-style forum, on the ATmosphere!
Domain: atbb.space (owned) | License: AGPL-3.0 | Org: atBB-Community
Architecture Overview#
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Forum UI │────▶│ AppView │────▶│ Firehose / │
│ (Web App) │◀────│ (Node/TS) │◀────│ User PDS nodes │
└─────────────┘ └──────┬───────┘ └─────────────────┘
│
┌──────▼───────┐
│ Forum DID │
│ (Service │
│ Account) │
└──────────────┘
Core principle: User-generated content (topics, posts, reactions) lives on each user's PDS. Forum metadata (categories, permissions, branding, mod actions) lives on a dedicated Forum Service Account (its own DID/PDS).
High-Level Plan (Full Vision)#
1. Identity & Ownership#
- Each forum instance has a dedicated DID (service account) that owns forum-level records.
- Users authenticate via AT Proto OAuth. Their DID is their identity across all forums.
- A user's membership, posts, and reputation are portable — leave a forum, your content stays in your PDS (though the AppView can stop surfacing it).
2. Custom Lexicon Namespace#
Namespace: space.atbb.* (domain atbb.space is owned and under control)
Prior Art Audit#
Lexicon repo (atBB-Community/lexicon):
- Source of truth is YAML (JSON generated via
build_lexicons.sh+yq) - Two lexicons defined today:
space.atbb.forum.forum— key:literal:self, fields:name(required),description(optional). Note the nestedforum.forumnamespace.space.atbb.post— key:tid, fields:text(required),createdAt(required),forum(optional forumRef → strongRef),reply(optional replyRef with root + parent → strongRef)
- Design decision: No separate
topictype. A post without areplyref acts as the topic/thread starter. This follows the same root/parent reply-chain pattern as Bluesky'sapp.bsky.feed.post. - Vendors
com.atproto.repo.strongReflocally
Rust AppView (atBB-Community/atBB):
- Axum 0.7, SQLx 0.8 (dual Postgres + SQLite), Tera templates, HTMX
jetstream-oxidefor firehose subscription,atrium-api0.24 for AT Proto SDK- DB schema:
forums(id, rkey, cid, name, description) andposts(id, rkey, cid) — minimal columns, no user/thread indexing yet - Hardcoded test forum data in
server.rs; routes:/healthz/*,/v1/forums, web UI at/ - Docker build with
cargo-chef+sccache, CI publishes to GHCR - Rust toolchain pinned to 1.82.0
Monorepo (malpercio-dev/atbb-monorepo):
- Git submodules under
prior-art/: atBB, lexicon, andat-delegation(the delegation pattern discussed in Future Roadmap)
Lexicon Plan#
Existing lexicons to keep as-is (reuse directly):
space.atbb.forum.forum— forum metadataspace.atbb.post— unified post/reply record (no separate topic type)
New lexicons to define for MVP:
| Lexicon | Owner | Description |
|---|---|---|
space.atbb.forum.category |
Forum DID | Subforum / category definition |
space.atbb.forum.role |
Forum DID | Role definitions (admin, mod, member, custom) |
space.atbb.membership |
User DID | User's membership in a forum + assigned role |
space.atbb.reaction |
User DID | Upvote/like/emoji reaction on a post |
space.atbb.modAction |
Forum DID | Moderation action (ban, mute, pin, lock, delete) |
Deferred to post-MVP:
| Lexicon | Description |
|---|---|
space.atbb.crossPost |
Reference to content in another forum |
3. Permissions Model#
AT Proto has no native ACLs, so permissions are enforced at the AppView layer:
- Write side: Users can always write records to their PDS. The AppView decides whether to index/surface them.
- Read side: AppView checks role assignments before serving content from restricted categories.
- Mod actions: Stored as records on the Forum DID. The AppView treats these as authoritative (e.g., a ban record means "don't surface this user's content").
- Role hierarchy: Owner → Admin → Moderator → Member → Guest. Configurable per-category.
MVP shortcut — AppView as trusted intermediary: The AppView holds the Forum DID's signing keys directly. When an admin or moderator performs an action (create category, ban user, etc.), the AppView verifies their role via its own index, then writes the record to the Forum DID's PDS on their behalf. This is a single point of trust but avoids the need for a delegation layer at launch.
Future — AT Protocol Privilege Delegation: The at.delegation namespace and Delegation Gateway Server (DGS) pattern would replace this shortcut. Admins and moderators would be Permitted Actors with scoped write access to specific collections on the Forum DID's repo (e.g., a moderator gets delegation for space.atbb.modAction but not space.atbb.category). This removes the AppView as a key-holding bottleneck and enables multi-server moderation teams. See dedicated section in Future Roadmap.
4. Federation & Discovery#
- Shared identity: Since users have a single DID across forums, any AppView can query "which forums does DID X belong to" by reading their
space.atbb.membershiprecords. This enables "common forums" features like Discord's mutual servers. - Forum directory: A well-known registry (could be a shared record collection or a simple directory service) lets forums opt into discoverability.
- Cross-posting: A
crossPostrecord references content by AT-URI. The target forum's AppView can choose to index it (with attribution) or ignore it.
5. Self-Hosted Model#
- Ship as a Docker Compose stack: AppView + Postgres + Web UI + optional bundled PDS for the forum service account.
- Config file defines: forum DID, domain, branding, category structure, default roles.
- Operator runs their own instance, points DNS at it. No central authority.
Detailed MVP Plan#
MVP Goal#
A single self-hosted forum where users can authenticate with their AT Proto identity, browse categories, create topics, reply, and where admins can perform basic moderation.
MVP Scope (In / Out)#
| In Scope | Out of Scope |
|---|---|
| Single forum instance | Multi-forum hosting |
| AT Proto OAuth login | Cross-posting |
| Categories (admin-created) | Forum discovery / directory |
Create topic, reply (unified space.atbb.post — topics are posts without reply ref) |
Nested threading |
| Basic roles: admin, member | Custom roles, per-category permissions |
| Mod actions: ban user, lock topic, delete post (hide from index) | Rich moderation tools (warnings, temp bans, audit log) |
| Firehose subscription + indexing | Full-text search |
| Basic web UI | Rich UI (avatars, signatures, user profiles) |
| Docker Compose deployment | Helm charts, cloud-native deploy |
MVP Technical Stack#
- AppView: Node.js + TypeScript
- Framework: Express or Hono
- Database: PostgreSQL (indexed forum state)
- AT Proto libraries:
@atproto/api,@atproto/lexicon,@atproto/repo - Firehose consumer:
@atproto/sync(subscribe to repos) - Web UI: Lightweight — React or even server-rendered (your call, can defer)
- Deployment: Docker Compose
MVP Milestones#
Phase 0: Foundation (Week 1–2)#
- Audit existing
space.atbb.*lexicons — Result: 2 existing (forum.forum,post), 5 new needed for MVP. No separate topic type; unified post model with reply refs. - Review prior Rust AppView — Result: Axum/SQLx scaffold with jetstream-oxide firehose, minimal DB schema. Reference for route structure and Docker setup; MVP will be Node/TS rewrite.
- Define new lexicons in YAML:
forum.category,forum.role,membership,reaction,modAction— Result: All 5 defined inpackages/lexicon/lexicons/, with YAML→JSON→TypeScript build pipeline via@atproto/lex-cli. - Set up project scaffolding: monorepo with
packages/lexicon,apps/appview,apps/web— Result: Turborepo + pnpm workspaces, devenv for Nix toolchain. AppView (Hono JSON API, port 3000), Web (Hono JSX + HTMX, port 3001). Apps live inapps/, shared libraries inpackages/. - Create Forum Service Account (generate DID, set up PDS or use existing hosting) — Complete: Forum DID configured in
.env, PDS credentials set (ATB-5) - Spike: write a test record to a PDS, read it back via AT Proto API — Complete: Validated write/read/list/delete for all MVP record types (ATB-6)
Phase 1: AppView Core (Week 3–4)#
- Implement firehose subscription — connect to relay, filter for
space.atbb.*records — Complete: Production-ready implementation inapps/appview/src/lib/firehose.tswith Jetstream WebSocket client, cursor persistence, circuit breaker, and exponential backoff reconnection logic (ATB-9) - Build indexer: parse incoming records, write to Postgres — Complete: Full implementation in
apps/appview/src/lib/indexer.tshandles all record types (posts, forums, categories, memberships, modActions) with transaction support. Reactions handlers stubbed pending schema table addition (ATB-10) - Database schema:
forums,categories,users,memberships,posts(unified — thread starters have no parent_uri),mod_actions— Complete: 7 tables defined inpackages/db/src/schema.tsusing Drizzle ORM (includesfirehose_cursorfor subscription state). Migrations inpackages/db/drizzle/(ATB-7) - API endpoints (read path) — Complete: Implemented factory pattern with database queries via Drizzle ORM. Routes use dependency injection to access AppContext (ATB-11):
GET /api/forum— queries singleton forum record (rkey='self') fromforumstable (apps/appview/src/routes/forum.ts)GET /api/categories— lists all categories ordered bysort_order(apps/appview/src/routes/categories.ts)GET /api/topics/:id— fetches topic post + all replies whererootPostId = :id, joins with users, filters deleted posts (apps/appview/src/routes/topics.ts)- Factory functions (
createForumRoutes,createCategoriesRoutes,createTopicsRoutes) acceptAppContextparameter for database access - BigInt IDs serialized as strings for JSON compatibility
- Defensive parsing with try-catch returns 400 Bad Request for invalid IDs
- Comprehensive error handling with try-catch on all database queries
- Global error handler in create-app.ts for unhandled errors
- Helper functions for serialization (serializeBigInt, serializeDate, serializeAuthor, parseBigIntParam)
- API endpoints (write path — proxy to user's PDS) — DONE (ATB-12):
POST /api/topics— createspace.atbb.postrecord withforumRefbut noreplyref. Validates text (1-300 graphemes), writes to user's PDS via OAuth agent, returns {uri, cid, rkey} with 201 status. Fire-and-forget design (firehose indexes asynchronously). (apps/appview/src/routes/topics.ts:13-119)POST /api/posts— createspace.atbb.postrecord with bothforumRefandreplyref. Validates text, parses rootPostId/parentPostId, validates parent belongs to same thread, writes to user's PDS, returns {uri, cid, rkey}. (apps/appview/src/routes/posts.ts:13-119)- Helper functions for validation:
validatePostText()(1-300 graphemes using@atproto/apiUnicodeString, with type guard for non-string input),getForumByUri(),getPostsByIds()(bulk lookup with Map),validateReplyParent()(thread boundary validation). (apps/appview/src/routes/helpers.ts:65-190) - Error handling: Type guards prevent crashes, JSON parsing wrapped in try-catch (400 for malformed), catch blocks re-throw TypeError/ReferenceError (don't swallow programming bugs), network errors (503) vs server errors (500) properly classified. No silent data fabrication (returns null).
- Tests: 16 integration tests for POST /api/topics (includes 5 PDS error scenarios), 14 integration tests for POST /api/posts (includes 5 PDS error scenarios), 16 unit tests for helpers. 134 total appview tests passing (29 new tests for ATB-12). Three comprehensive review rounds completed.
- ATB-23: Boards Hierarchy (EXPANDED from "add categoryUri column") — Complete: 2026-02-14
- Implemented 3-level hierarchy: Forum → Categories → Boards → Topics
- Added
space.atbb.forum.boardlexicon (YAML + generated types) - Schema changes:
boardstable (bigserial id, did, rkey, cid, name, description, categoryUri, sortOrder, indexed_at), addedboardUriandboardIdcolumns topoststable with foreign key constraint, migrations0002_sturdy_maestro.sql(boards table) and0003_brief_mariko_yashida.sql(posts columns) - Posts now link to boards (primary) and forums (redundant for backward compatibility)
- New API endpoints with comprehensive tests:
GET /api/boards— list all boards across all categoriesGET /api/boards/:id/topics— list topics for a specific boardGET /api/categories/:id/boards— list boards within a category
- Updated
POST /api/topicsto requireboardUri(validates board exists) - Indexer handles
space.atbb.forum.boardrecords from firehose - Bruno collections updated with new board endpoints
- Files:
packages/lexicon/lexicons/space/atbb/forum/board.yaml,apps/appview/drizzle/0002_sturdy_maestro.sql,apps/appview/drizzle/0003_brief_mariko_yashida.sql,apps/appview/src/routes/boards.ts,apps/appview/src/routes/__tests__/boards.test.ts
Phase 2: Auth & Membership (Week 5–6)#
- Implement AT Proto OAuth flow (user login via their PDS) — Complete: OAuth 2.1 implementation using
@atproto/oauth-client-nodelibrary with PKCE flow, state validation, automatic token refresh, and DPoP. Supports any AT Protocol PDS (not limited to bsky.social). Routes inapps/appview/src/routes/auth.ts(ATB-14) - On first login: create
membershiprecord on user's PDS — Complete: Fire-and-forget membership creation integrated into OAuth callback. Helper functioncreateMembershipForUser()checks for duplicates, writesspace.atbb.membershiprecord to user's PDS. Login succeeds even if membership creation fails (graceful degradation). 9 tests (5 unit + 4 integration) verify architectural contract. Implementation inapps/appview/src/lib/membership.tsandapps/appview/src/routes/auth.ts:163-188(ATB-15, PR #27) - Session management (JWT or similar, backed by DID verification) — Complete: Three-layer session architecture using
@atproto/oauth-client-nodelibrary with OAuth session store (oauth-stores.ts), cookie-to-DID mapping (cookie-session-store.ts), and HTTP-only cookies. Sessions include DID, handle, PDS URL, access tokens with automatic refresh, expiry. Automatic cleanup every 5 minutes. Authentication middleware (requireAuth,optionalAuth) implemented inapps/appview/src/middleware/auth.ts(ATB-14) - Forum DID authenticated agent for server-side PDS writes — Complete:
ForumAgentservice authenticates as Forum DID on startup with smart retry logic (network errors retry with exponential backoff, auth errors fail permanently). Integrated intoAppContextwith proactive session refresh every 30 minutes. Graceful degradation (server starts even if auth fails). Health endpoint (GET /api/health) exposes granular ForumAgent status. Implementation inapps/appview/src/lib/forum-agent.ts, health endpoint inapps/appview/src/routes/health.ts(ATB-18) - Role assignment: admin can set roles via Forum DID records (ATB-17) — Complete: Full permission system implemented with 4 default roles, middleware enforcement, admin endpoints, and role seeding. Files:
apps/appview/src/middleware/permissions.ts,apps/appview/src/routes/admin.ts,apps/appview/src/lib/seed-roles.ts,packages/db/src/schema.ts:188-210(2026-02-14) - Middleware: permission checks on write endpoints — Complete:
requirePermission()andrequireRole()middleware integrated on all write endpoints (POST /api/topics,POST /api/posts). Future mod endpoints will usecanActOnUser()for priority hierarchy enforcement.
Phase 3: Moderation Basics (Week 6–7)#
- ATB-19: Moderation action write-path API endpoints — Complete: 2026-02-16
- Implemented 6 moderation endpoints with comprehensive error handling and test coverage
POST /api/mod/banandDELETE /api/mod/ban/:did— ban/unban users (requiresbanUserspermission)POST /api/mod/lockandDELETE /api/mod/lock/:topicId— lock/unlock topics (requireslockTopicspermission)POST /api/mod/hideandDELETE /api/mod/hide/:postId— hide/unhide posts (requiresmoderatePostspermission)- All endpoints write
space.atbb.modActionrecords to Forum DID's PDS via ForumAgent - Idempotent design: returns 200 with
alreadyActive: truefor duplicate actions - Error classification: 400 (validation), 404 (not found), 500 (server error), 503 (network/retry)
- Helper functions:
validateReason()(1-3000 chars),checkActiveAction()(query most recent action) - 421 tests total (added 78 new tests) — comprehensive coverage including auth, validation, business logic, infrastructure errors
- Files:
apps/appview/src/routes/mod.ts(~700 lines),apps/appview/src/routes/__tests__/mod.test.ts(~3414 lines),apps/appview/src/lib/errors.ts(error classification helpers) - Bruno API collection:
bruno/AppView API/Moderation/(6 .bru files documenting all endpoints)
- ATB-24: Admin moderation UI in web app — Complete: 2026-02-19
GET /api/admin/members/meAppView endpoint returns current user's role + permissionsgetSessionWithPermissions()+canLockTopics()/canModeratePosts()/canBanUsers()helpers in web session libPOST /mod/actionweb proxy route dispatches lock/unlock/hide/unhide/ban/unban to AppView- Topic page renders lock button, per-post hide/ban buttons, and shared
<dialog>confirmation modal — all gated on permissions - 507 tests total across appview + web (added ~35 new tests)
- ATB-20: Enforce mod actions in read/write-path API responses — Complete: 2026-02-16
- All API read endpoints filter soft-deleted posts (
deleted = falsein all queries) - All API write endpoints (topic/post create) block banned users at request time
requireNotBanned()middleware checksmod_actionsbefore allowing writes- Comprehensive test coverage for banned user scenarios across all write endpoints
- All API read endpoints filter soft-deleted posts (
- ATB-21: Enforce mod actions in firehose indexer — Complete: 2026-02-17
- New
BanEnforcerclass composed intoIndexer(fail-closed: DB error → treat as banned) handlePostCreate: checksisBanned(event.did)before indexing; banned users' posts are silently dropped (never inserted)handleModActionCreate: after inserting a ban mod action, callsapplyBan(subjectDid)to soft-delete all existing posts — retroactive enforcementhandleModActionDelete: read-before-delete in a single transaction; if deleted action was a ban, callsliftBan(subjectDid)to restore posts- Decision documented: skip (not soft-mark) for new posts; reuse
deletedcolumn (no new column); DB query with existingmod_actions_subject_did_idxindex (no cache) - Race condition handled: post-before-ban path covered by eventual consistency — post inserts normally,
applyBansoft-deletes it when ban arrives - 8 new integration tests in
indexer.test.ts; 7 unit tests inindexer-ban-enforcer.test.ts; 491 total tests passing - Files:
apps/appview/src/lib/ban-enforcer.ts(new),apps/appview/src/lib/indexer.ts(3 handler overrides),apps/appview/src/lib/__tests__/indexer-ban-enforcer.test.ts(new),apps/appview/src/lib/__tests__/indexer.test.ts(additions)
- New
- Document the trust model: operators must trust their AppView instance, which is acceptable for self-hosted single-server deployments
- ATB-22 |
docs/trust-model.md(new) — covers operator responsibilities, user data guarantees, security implications, and future delegation path; referenced from deployment guide
- ATB-22 |
- ATB-25: Separate ban enforcement from user-initiated deletes (Bug Fix) — Complete: 2026-02-24
liftBanwas silently resurrecting user-deleted posts on unban because both deletion sources shared thedeletedcolumn- Added
bannedByModboolean column topoststable;applyBan/liftBannow togglebannedByModonly, leavingdeletedfor user-initiated deletes - All API read queries filter
eq(posts.bannedByMod, false)in addition toeq(posts.deleted, false) - Also adds
deleted_by_usercolumn for future self-deletion endpoint (groundwork laid) - Files: new migration,
apps/appview/src/lib/ban-enforcer.ts,packages/db/src/schema.ts(ATB-25, PR #56)
- ATB-35: Strip title from reply records at index time (Bug Fix) — Complete: 2026-02-24
- Indexer was unconditionally storing
title: record.title ?? nullon all posts, including replies — violating the lexicon invariant that titles belong only to thread starters - Fixed:
toInsertValues/toUpdateValuessettitle: nullwhenPost.isReplyRef(record.reply)is true - File:
apps/appview/src/lib/indexer.ts(ATB-35, PR #55)
- Indexer was unconditionally storing
- ATB-13: Backfill & Repo Sync — Complete: 2026-02-23
- Automatic gap detection on startup: if cursor is stale (>48h) → CatchUp; no cursor → FullSync; healthy cursor → skip
BackfillManagerorchestrates full-repo sync viacom.atproto.sync.listRepos+com.atproto.repo.listRecordsfor each member DID- Processes all
space.atbb.*collections through the sameIndexerhandlers used by the live firehose - Interrupt recovery: in-progress backfills at shutdown are resumed on next startup via
status='interrupted'checkpoint - Admin API endpoints for manual triggering and monitoring:
POST /api/admin/backfill— triggers backfill (202 async, 200 if not needed, 409 if running, optional?force=catch_up|full_sync)GET /api/admin/backfill/:id— polls status, progress, and error count for a specific runGET /api/admin/backfill/:id/errors— lists per-DID errors (partial failures that didn't stop the run)
- DB tables:
backfill_progress(status, type, dids_total/processed, records_indexed, checkpoint) andbackfill_errors(per-DID failures) - Rate-limited with configurable concurrency (
backfillRateLimit,backfillConcurrencyin config) - 555 tests passing; 16 new tests in
admin-backfill.test.tscovering auth, progress, errors, and 409/503 edge cases - Files:
apps/appview/src/lib/backfill-manager.ts(new),apps/appview/src/routes/admin.ts(3 new routes),apps/appview/src/lib/firehose.ts(startup integration),packages/db/src/schema.ts(2 new tables) - Bruno collection:
bruno/AppView API/Admin/(3 .bru files)
Phase 4: Web UI (Week 7–9)#
- Web UI foundation: neobrutal design system, shared components, and route stubs
- ATB-26 | CSS architecture with custom properties (
reset.css,theme.css, neobrutal light token preset); shared components:Card,Button,PageHeader,ErrorDisplay,EmptyState; static file serving via HonoserveStatic; route stubs for all Phase 4 pages. PR #39 (2026-02-18)
- ATB-26 | CSS architecture with custom properties (
- Forum homepage: category list, recent topics
- ATB-27 |
apps/web/src/routes/home.tsx— server-renders forum name/description, categories as section headers, boards as cards with links; two-stage parallel fetch (forum+categories, then per-category boards); error display on network (503) or API (500) failures; 12 integration tests inhome.test.tsx
- ATB-27 |
- Board view: topic listing with pagination
- ATB-28 |
apps/web/src/routes/boards.tsx— breadcrumb navigation, topic list (truncated 80 chars), HTMX "Load More" pagination; auth-aware "New Topic" button;timeAgorelative date utility;isNotFoundErrorerror helper; AppView:GET /api/boards/:id,GET /api/categories/:id, offset/limit pagination onGET /api/boards/:id/topics; 20 integration tests inboards.test.tsx; 8 AppView tests
- ATB-28 |
- Category view: paginated topic list, sorted by last reply — Deferred: boards hierarchy (ATB-23) replaced per-category pages; boards view serves this purpose
- Topic view: OP + flat replies, pagination
- ATB-29 |
apps/web/src/routes/topics.tsx— OP card (#1) + reply cards (#2, #3, …) with post numbers andtimeAgodates; breadcrumb (Home → Category → Board → Topic) with graceful degradation on breadcrumb fetch failures; locked-topic banner + reply-slot gating (unauthenticated/authenticated/locked); HTMX "Load More" withhx-push-urlfor bookmarkable offsets;?offset=Nbookmark support renders all replies 0→N+pageSize inline; three-stage sequential fetch (topic fatal, board/category non-fatal); 35 integration tests intopics.test.tsx
- ATB-29 |
- Server-side offset/limit pagination for topic replies
- ATB-33 |
GET /api/topics/:idnow accepts?offset=N&limit=Nquery params; webtopics.tsxpasses these to AppView instead of fetching all replies and slicing client-side. PR #57 (2026-02-24)
- ATB-33 |
- Compose: new topic form, reply form
- ATB-31 |
apps/web/src/routes/new-topic.tsx— new topic form with board selector, character counter, validation;apps/web/src/routes/topics.tsx— inline reply form with HTMX submission; both forms write to AppView API which proxies to user's PDS
- ATB-31 |
- Login/logout flow
- ATB-30 |
apps/web/src/routes/login.tsx— handle input form;apps/web/src/routes/auth.tsx— OAuth callback + session management; BaseLayout shows login/logout based on auth state
- ATB-30 |
- Admin panel: manage categories, view members, mod actions
- ATB-24 | Topic view mod buttons (lock/hide/ban) gated on permissions;
<dialog>confirmation modal;POST /mod/actionweb proxy route;getSessionWithPermissions()for permission-aware rendering
- ATB-24 | Topic view mod buttons (lock/hide/ban) gated on permissions;
- ATB-42: Admin panel landing page and routing infrastructure — Complete: 2026-02-28
GET /adminlanding page with permission-gated nav cards (Members, Structure, Mod Log)hasAnyAdminPermission()gate redirects non-admins; 403 for authenticated users without any admin permissioncanManageMembers(),canManageCategories(),canViewModLog()helpers control which cards render- CSS:
.admin-nav-grid,.admin-nav-card,.admin-nav-card__icon/title/description; neobrutal card style - Files:
apps/web/src/routes/admin.tsx,apps/web/public/static/css/theme.css
- ATB-43: Admin panel member management page (
/admin/members) — Complete: 2026-02-28GET /admin/membersrenders full member table (handle, role badge, joined date) withmanageMembersgate- Role assignment controls (
<select>+ Assign button) shown only when session hasmanageRoles - Parallel fetch:
GET /api/admin/members+GET /api/admin/roles(roles skipped if nomanageRoles) - HTMX
outerHTMLrow swap:POST /admin/members/:did/roleproxy → AppView → updated<tr>fragment - Hidden form inputs carry reconstruction data (handle, joinedAt, currentRole, currentRoleUri, rolesJson) — no extra API call on re-render
- Web-layer permission gate on POST (auth +
manageRoles) returns<tr>error fragment (not redirect) for HTMX compatibility - AppView:
GET /api/admin/rolesnow includesurifield (at://${did}/space.atbb.forum.role/${rkey}) canManageRolessession helper added toapps/web/src/lib/session.ts- 31 integration tests: auth guards, table render, role form visibility, HTMX success/error paths, 503/500 error display
- Files:
apps/web/src/routes/admin.tsx,apps/web/src/lib/session.ts,apps/web/public/static/css/theme.css,apps/appview/src/routes/admin.ts
- Basic responsive design
- ATB-32 | Mobile-first responsive breakpoints (375px/768px/1024px), CSS-only hamburger nav via
<details>/<summary>, token overrides for mobile, accessibility improvements (skip link, focus-visible, ARIA attributes, semantic HTML), 404 page, visual polish (transitions, hover states), SVG favicon
- ATB-32 | Mobile-first responsive breakpoints (375px/768px/1024px), CSS-only hamburger nav via
- Show author handles in posts
- Design in
docs/plans/complete/2026-02-24-show-handles-in-posts-design.md. OAuth callback upsertsusers.handleaftergetProfile()resolves; posts display human-readable handles instead of raw DIDs. Users who have never logged in still show their DID (intentional MVP scope). No schema changes required.
- Design in
Phase 5: Packaging & Deployment (Week 9–10)#
- Dockerfiles for AppView and Web UI — Complete: Multi-stage Dockerfile with Node 22 Alpine, nginx reverse proxy, health checks (ATB-28)
- Docker Compose with Postgres, AppView, Web UI — Complete:
docker-compose.example.ymlwith service orchestration (ATB-28) - Config file: forum name, domain, admin DID, categories — Complete:
.env.examplewith all required variables documented - README: setup guide, architecture overview — Complete: README.md includes setup, architecture diagram, deployment instructions
- Seed script for initial forum + categories — Deferred: Automated wizard tracked in Future Roadmap
- Basic health check / status endpoint — Complete:
GET /api/healthzandGET /api/healthz/readyimplemented (ATB-9)
Key Risks & Open Questions#
Lexicon registration✅ RESOLVED —atbb.spacedomain is owned and controlled. Existing lexicon definitions inatBB-Community/lexiconrepo may be reusable.- Firehose filtering: The AT Proto firehose is all records from all users. Filtering for your Lexicon types at scale requires thought. For MVP (small user base), naive filtering is fine.
PDS write path✅ RESOLVED — OAuth 2.1 with PKCE +atproto transition:genericscope implemented (ATB-14). Write path validated in spike (ATB-6) and production (ATB-12). Token refresh and DPoP handled by@atproto/oauth-client-node.Record deletion✅ RESOLVED — Indexer handles PDS tombstones:handlePostDeletesetsdeleted = trueon the post row; all API read queries filterdeleted = false. User-delete and mod-hide are tracked with separate columns (deletedvsbannedByMod) since ATB-25.Backfill✅ RESOLVED — ATB-13 implemented full backfill & repo sync.BackfillManagerhandles gap detection, CatchUp vs FullSync, interrupt recovery, rate-limited repo crawl, and admin trigger API (POST /api/admin/backfill). See Phase 3 ATB-13 entry for details.
Known Issues / Active Backlog#
Open bugs and deferred improvements against the current implementation, by priority:
| Issue | Priority | Title | Notes |
|---|---|---|---|
| ATB-39 | High | upgradeBootstrapMembership writes PDS record without role field |
Forum Owner loses all permissions on first real login; role field must be read from existing DB row and included in the PDS write |
| ATB-38 | High | seedDefaultRoles partial failure should fail fast |
Server starts silently broken if Member role PDS write fails at startup; fix: throw after seeding if Member role absent in DB |
| ATB-41 | Medium | Missing $type discriminator on forumRef/boardRef objects written to PDS |
Inconsistent with replyRef fix (PR #61); safe today via optional chaining in indexer but fragile if typed guards are adopted |
| ATB-34 | Low | Axe-core automated accessibility testing | Deferred from ATB-32; add @axe-core/playwright + WCAG AA integration tests in CI pipeline |
Linear cleanup: ATB-39 duplicates ATB-37; ATB-40 duplicates ATB-38 (filed 4 minutes apart, same bugs). The later-filed pair (ATB-39/40) should be closed as duplicates.
Future Roadmap (Post-MVP)#
Privilege Delegation (at.delegation integration)#
This is the highest-impact architectural upgrade post-MVP. It replaces the "AppView holds Forum DID keys" shortcut with proper scoped delegation.
- Phase A — DGS Proxy: Deploy a Delegation Gateway Server that sits in front of the Forum DID's PDS. The AppView routes admin/mod writes through the DGS instead of signing directly. Delegation records (
at.delegation.*) define which DIDs can write to which collections. - Phase B — Multi-admin: Multiple admins can write forum metadata and mod actions from their own clients/contexts, not just through the AppView UI. The DGS validates delegation scopes and proxies authorized writes.
- Phase C — Federated moderation: Moderators on different AppView instances (or using third-party tools) can perform mod actions on the Forum DID via delegation, enabling distributed moderation teams.
- Phase D — Extract and propose: Package the delegation spec as a standalone AT Proto proposal with the forum as the reference use case. Contribute upstream.
Mobile Apps (iOS & Android)#
React Native + Expo cross-platform apps consuming the same /api/* endpoints as the web UI. Phased rollout: read-only browse → write/interact → push notifications → offline support & app store release. Full plan in docs/mobile-apps-plan.md.
SQLite Support ✅ Complete#
Design in docs/plans/complete/2026-02-24-sqlite-support-design.md. URL-prefix detection in createDb() selects the backend automatically (postgresql:// → postgres.js + Drizzle PG dialect; file: → @libsql/client + Drizzle LibSQL dialect). The role_permissions join table replaced the Postgres-specific text[] array column, improving both dialects. Separate schema files, Drizzle config files, and migration directories per dialect. TypeScript types are identical for both via Drizzle mode specifiers. Docker Compose variant (docker-compose.sqlite.yml) and NixOS module updated with database.type option.
Other Future Work#
- Setup wizard for first-time initialization — Interactive web-based wizard for administrators to initialize a new forum instance (create forum record on PDS, configure categories, set admin roles). Currently requires manual PDS API calls.
- User self-deletion endpoint — Allow users to delete their own posts with two modes: soft-delete (hide from forum index but keep on PDS) and hard-delete (actually remove from their PDS via deleteRecord). Complements moderation soft-delete which only hides content. Groundwork laid:
deleted_by_usercolumn added topoststable (ATB-25); indexer already handles post tombstones from the firehose. Endpoint implementation is the remaining work. - Nested/threaded replies
- Full-text search (maybe Meilisearch)
- User profiles & post history
- Forum directory / discovery protocol
- Cross-posting between forums
- "Common forums" feature (mutual server display)
- Rich text / markdown in posts
- File/image attachments via blob storage
- Custom themes & branding
- Plugin/extension system
- Email notifications
- Push notifications (mobile + web)
- RSS feeds per category/topic