# ATProto Forum Software — Project Plan **atBB** — A BB-style forum, on the ATmosphere! Domain: `atbb.space` (owned) | License: AGPL-3.0 | Org: [atBB-Community](https://github.com/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](https://github.com/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 nested `forum.forum` namespace. - `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 `topic` type. A post without a `reply` ref acts as the topic/thread starter. This follows the same root/parent reply-chain pattern as Bluesky's `app.bsky.feed.post`. - Vendors `com.atproto.repo.strongRef` locally **Rust AppView** ([atBB-Community/atBB](https://github.com/atBB-Community/atBB)): - Axum 0.7, SQLx 0.8 (dual Postgres + SQLite), Tera templates, HTMX - `jetstream-oxide` for firehose subscription, `atrium-api` 0.24 for AT Proto SDK - DB schema: `forums` (id, rkey, cid, name, description) and `posts` (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, and `at-delegation` (the delegation pattern discussed in Future Roadmap) #### Lexicon Plan Existing lexicons to **keep as-is** (reuse directly): - `space.atbb.forum.forum` — forum metadata - `space.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.membership` records. 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 `crossPost` record 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) - [x] 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. - [x] 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. - [x] Define new lexicons in YAML: `forum.category`, `forum.role`, `membership`, `reaction`, `modAction` — **Result:** All 5 defined in `packages/lexicon/lexicons/`, with YAML→JSON→TypeScript build pipeline via `@atproto/lex-cli`. - [x] 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 in `apps/`, shared libraries in `packages/`. - [x] Create Forum Service Account (generate DID, set up PDS or use existing hosting) — **Complete:** Forum DID configured in `.env`, PDS credentials set (ATB-5) - [x] 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) - [x] Implement firehose subscription — connect to relay, filter for `space.atbb.*` records — **Complete:** Production-ready implementation in `apps/appview/src/lib/firehose.ts` with Jetstream WebSocket client, cursor persistence, circuit breaker, and exponential backoff reconnection logic (ATB-9) - [x] Build indexer: parse incoming records, write to Postgres — **Complete:** Full implementation in `apps/appview/src/lib/indexer.ts` handles all record types (posts, forums, categories, memberships, modActions) with transaction support. Reactions handlers stubbed pending schema table addition (ATB-10) - [x] Database schema: `forums`, `categories`, `users`, `memberships`, `posts` (unified — thread starters have no parent_uri), `mod_actions` — **Complete:** 7 tables defined in `packages/db/src/schema.ts` using Drizzle ORM (includes `firehose_cursor` for subscription state). Migrations in `packages/db/drizzle/` (ATB-7) - [x] 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'`) from `forums` table (`apps/appview/src/routes/forum.ts`) - `GET /api/categories` — lists all categories ordered by `sort_order` (`apps/appview/src/routes/categories.ts`) - `GET /api/topics/:id` — fetches topic post + all replies where `rootPostId = :id`, joins with users, filters deleted posts (`apps/appview/src/routes/topics.ts`) - Factory functions (`createForumRoutes`, `createCategoriesRoutes`, `createTopicsRoutes`) accept `AppContext` parameter 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) - [x] API endpoints (write path — proxy to user's PDS) — **DONE** (ATB-12): - `POST /api/topics` — create `space.atbb.post` record with `forumRef` but no `reply` ref. 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` — create `space.atbb.post` record with both `forumRef` and `reply` ref. 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/api` UnicodeString, 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. - [x] **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.board` lexicon (YAML + generated types) - Schema changes: `boards` table (bigserial id, did, rkey, cid, name, description, categoryUri, sortOrder, indexed_at), added `boardUri` and `boardId` columns to `posts` table with foreign key constraint, migrations `0002_sturdy_maestro.sql` (boards table) and `0003_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 categories - `GET /api/boards/:id/topics` — list topics for a specific board - `GET /api/categories/:id/boards` — list boards within a category - Updated `POST /api/topics` to require `boardUri` (validates board exists) - Indexer handles `space.atbb.forum.board` records 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) - [x] Implement AT Proto OAuth flow (user login via their PDS) — **Complete:** OAuth 2.1 implementation using `@atproto/oauth-client-node` library with PKCE flow, state validation, automatic token refresh, and DPoP. Supports any AT Protocol PDS (not limited to bsky.social). Routes in `apps/appview/src/routes/auth.ts` (ATB-14) - [x] On first login: create `membership` record on user's PDS — **Complete:** Fire-and-forget membership creation integrated into OAuth callback. Helper function `createMembershipForUser()` checks for duplicates, writes `space.atbb.membership` record to user's PDS. Login succeeds even if membership creation fails (graceful degradation). 9 tests (5 unit + 4 integration) verify architectural contract. Implementation in `apps/appview/src/lib/membership.ts` and `apps/appview/src/routes/auth.ts:163-188` (ATB-15, PR #27) - [x] Session management (JWT or similar, backed by DID verification) — **Complete:** Three-layer session architecture using `@atproto/oauth-client-node` library 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 in `apps/appview/src/middleware/auth.ts` (ATB-14) - [x] Forum DID authenticated agent for server-side PDS writes — **Complete:** `ForumAgent` service authenticates as Forum DID on startup with smart retry logic (network errors retry with exponential backoff, auth errors fail permanently). Integrated into `AppContext` with proactive session refresh every 30 minutes. Graceful degradation (server starts even if auth fails). Health endpoint (`GET /api/health`) exposes granular ForumAgent status. Implementation in `apps/appview/src/lib/forum-agent.ts`, health endpoint in `apps/appview/src/routes/health.ts` (ATB-18) - [x] 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) - [x] Middleware: permission checks on write endpoints — **Complete:** `requirePermission()` and `requireRole()` middleware integrated on all write endpoints (`POST /api/topics`, `POST /api/posts`). Future mod endpoints will use `canActOnUser()` for priority hierarchy enforcement. #### Phase 3: Moderation Basics (Week 6–7) - [x] **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/ban` and `DELETE /api/mod/ban/:did` — ban/unban users (requires `banUsers` permission) - `POST /api/mod/lock` and `DELETE /api/mod/lock/:topicId` — lock/unlock topics (requires `lockTopics` permission) - `POST /api/mod/hide` and `DELETE /api/mod/hide/:postId` — hide/unhide posts (requires `moderatePosts` permission) - All endpoints write `space.atbb.modAction` records to Forum DID's PDS via ForumAgent - Idempotent design: returns 200 with `alreadyActive: true` for 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) - [x] **ATB-24: Admin moderation UI in web app** — **Complete:** 2026-02-19 - `GET /api/admin/members/me` AppView endpoint returns current user's role + permissions - `getSessionWithPermissions()` + `canLockTopics()` / `canModeratePosts()` / `canBanUsers()` helpers in web session lib - `POST /mod/action` web proxy route dispatches lock/unlock/hide/unhide/ban/unban to AppView - Topic page renders lock button, per-post hide/ban buttons, and shared `` confirmation modal — all gated on permissions - 507 tests total across appview + web (added ~35 new tests) - [x] **ATB-20: Enforce mod actions in read/write-path API responses** — **Complete:** 2026-02-16 - All API read endpoints filter soft-deleted posts (`deleted = false` in all queries) - All API write endpoints (topic/post create) block banned users at request time - `requireNotBanned()` middleware checks `mod_actions` before allowing writes - Comprehensive test coverage for banned user scenarios across all write endpoints - [x] **ATB-21: Enforce mod actions in firehose indexer** — **Complete:** 2026-02-17 - New `BanEnforcer` class composed into `Indexer` (fail-closed: DB error → treat as banned) - `handlePostCreate`: checks `isBanned(event.did)` before indexing; banned users' posts are silently dropped (never inserted) - `handleModActionCreate`: after inserting a ban mod action, calls `applyBan(subjectDid)` to soft-delete all existing posts — retroactive enforcement - `handleModActionDelete`: read-before-delete in a single transaction; if deleted action was a ban, calls `liftBan(subjectDid)` to restore posts - Decision documented: skip (not soft-mark) for new posts; reuse `deleted` column (no new column); DB query with existing `mod_actions_subject_did_idx` index (no cache) - Race condition handled: post-before-ban path covered by eventual consistency — post inserts normally, `applyBan` soft-deletes it when ban arrives - 8 new integration tests in `indexer.test.ts`; 7 unit tests in `indexer-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) - [x] 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 - [x] **ATB-25: Separate ban enforcement from user-initiated deletes (Bug Fix)** — **Complete:** 2026-02-24 - `liftBan` was silently resurrecting user-deleted posts on unban because both deletion sources shared the `deleted` column - Added `bannedByMod` boolean column to `posts` table; `applyBan`/`liftBan` now toggle `bannedByMod` only, leaving `deleted` for user-initiated deletes - All API read queries filter `eq(posts.bannedByMod, false)` in addition to `eq(posts.deleted, false)` - Also adds `deleted_by_user` column 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) - [x] **ATB-35: Strip title from reply records at index time (Bug Fix)** — **Complete:** 2026-02-24 - Indexer was unconditionally storing `title: record.title ?? null` on all posts, including replies — violating the lexicon invariant that titles belong only to thread starters - Fixed: `toInsertValues`/`toUpdateValues` set `title: null` when `Post.isReplyRef(record.reply)` is true - File: `apps/appview/src/lib/indexer.ts` (ATB-35, PR #55) - [x] **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 - `BackfillManager` orchestrates full-repo sync via `com.atproto.sync.listRepos` + `com.atproto.repo.listRecords` for each member DID - Processes all `space.atbb.*` collections through the same `Indexer` handlers 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 run - `GET /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) and `backfill_errors` (per-DID failures) - Rate-limited with configurable concurrency (`backfillRateLimit`, `backfillConcurrency` in config) - 555 tests passing; 16 new tests in `admin-backfill.test.ts` covering 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) - [x] 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 Hono `serveStatic`; route stubs for all Phase 4 pages. PR #39 (2026-02-18) - [x] 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 in `home.test.tsx` - [x] 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; `timeAgo` relative date utility; `isNotFoundError` error helper; AppView: `GET /api/boards/:id`, `GET /api/categories/:id`, offset/limit pagination on `GET /api/boards/:id/topics`; 20 integration tests in `boards.test.tsx`; 8 AppView tests - [ ] Category view: paginated topic list, sorted by last reply — **Deferred:** boards hierarchy (ATB-23) replaced per-category pages; boards view serves this purpose - [x] Topic view: OP + flat replies, pagination - ATB-29 | `apps/web/src/routes/topics.tsx` — OP card (#1) + reply cards (#2, #3, …) with post numbers and `timeAgo` dates; breadcrumb (Home → Category → Board → Topic) with graceful degradation on breadcrumb fetch failures; locked-topic banner + reply-slot gating (unauthenticated/authenticated/locked); HTMX "Load More" with `hx-push-url` for bookmarkable offsets; `?offset=N` bookmark support renders all replies 0→N+pageSize inline; three-stage sequential fetch (topic fatal, board/category non-fatal); 35 integration tests in `topics.test.tsx` - [x] Server-side offset/limit pagination for topic replies - ATB-33 | `GET /api/topics/:id` now accepts `?offset=N&limit=N` query params; web `topics.tsx` passes these to AppView instead of fetching all replies and slicing client-side. PR #57 (2026-02-24) - [x] 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 - [x] 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 - [x] Admin panel: manage categories, view members, mod actions - ATB-24 | Topic view mod buttons (lock/hide/ban) gated on permissions; `` confirmation modal; `POST /mod/action` web proxy route; `getSessionWithPermissions()` for permission-aware rendering - [x] **ATB-42: Admin panel landing page and routing infrastructure** — **Complete:** 2026-02-28 - `GET /admin` landing page with permission-gated nav cards (Members, Structure, Mod Log) - `hasAnyAdminPermission()` gate redirects non-admins; 403 for authenticated users without any admin permission - `canManageMembers()`, `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` - [x] **ATB-43: Admin panel member management page (`/admin/members`)** — **Complete:** 2026-02-28 - `GET /admin/members` renders full member table (handle, role badge, joined date) with `manageMembers` gate - Role assignment controls (`