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)
- Admin UI: ban user, lock topic, hide post (ATB-24)
- 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
Phase 4: Web UI (Week 7–9)#
- Forum homepage: category list, recent topics
- Category view: paginated topic list, sorted by last reply
- Topic view: OP + flat replies, pagination
- Compose: new topic form, reply form
- Login/logout flow
- Admin panel: manage categories, view members, mod actions
- Basic responsive design
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: Writing records to a user's PDS on their behalf requires proper OAuth scopes. Verify the current state of the AT Proto OAuth spec — it's been evolving.
- Record deletion: If a user deletes a post from their PDS, the firehose emits a tombstone. Your indexer needs to handle this (soft-delete from index).
- Backfill: If your AppView goes down and comes back, you need to backfill missed records. AT Proto supports repo sync for this, but it adds complexity.
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.
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.
- 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