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
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

1# atBB Monorepo 2 3**atBB** is a decentralized BB-style forum built on the AT Protocol. Users own their posts on their own PDS; the forum's AppView indexes and serves them. Lexicon namespace: `space.atbb.*` (domain `atbb.space` is owned). License: AGPL-3.0. 4 5The master project plan with MVP phases and progress tracking lives at `docs/atproto-forum-plan.md`. 6 7## Apps & Packages 8 9### Apps (`apps/`) 10 11Servers and applications that are deployed or run as services. 12 13| App | Description | Port | 14|-----|-------------|------| 15| `@atbb/appview` | Hono JSON API server — indexes forum data, serves API | 3000 | 16| `@atbb/web` | Hono JSX + HTMX server-rendered web UI — calls appview API | 3001 | 17 18### Packages (`packages/`) 19 20Shared libraries, tools, and utilities consumed by apps or used standalone. 21 22| Package | Description | 23|---------|-------------| 24| `@atbb/db` | Drizzle ORM schema and connection factory for PostgreSQL | 25| `@atbb/lexicon` | AT Proto lexicon definitions (YAML) + generated TypeScript types | 26| `@atbb/spike` | PDS read/write test script for validating AT Proto operations | 27 28**Dependency chain:** `@atbb/lexicon` and `@atbb/db` build first, then `@atbb/appview` and `@atbb/web` build in parallel. Turbo handles this via `^build`. 29 30## Development 31 32### Setup 33 34```sh 35devenv shell # enter Nix dev shell (Node.js, pnpm, turbo) 36pnpm install # install all workspace dependencies 37cp .env.example .env # configure environment variables 38``` 39 40### Commands 41 42```sh 43pnpm build # build all packages (lexicon → appview + web) 44pnpm dev # start all dev servers with hot reload 45pnpm test # run all tests across all packages 46pnpm clean # remove all dist/ directories 47devenv up # start appview + web servers via process manager 48pnpm --filter @atbb/appview dev # run a single package 49pnpm --filter @atbb/appview test # run tests for a single package 50pnpm --filter @atbb/spike spike # run the PDS spike script 51``` 52 53### Environment Variables 54 55See `.env.example`. Key variables: 56 57- `PORT` — server port (appview: 3000, web: 3001) 58- `FORUM_DID` — the forum's AT Proto DID 59- `PDS_URL` — URL of the forum's PDS 60- `APPVIEW_URL` — URL the web package uses to reach the appview API 61- `FORUM_HANDLE`, `FORUM_PASSWORD` — forum service account credentials (for spike/writes) 62 63## Pre-Commit Checks 64 65Every commit automatically runs three checks in parallel via lefthook: 66 671. **Lint** — oxlint scans staged TypeScript/JavaScript files for code quality issues 682. **Typecheck**`pnpm turbo lint` runs type checking on affected packages 693. **Test** — Vitest runs tests in packages with staged changes 70 71### Auto-Fixing Lint Issues 72 73Before committing, auto-fix safe lint violations: 74 75```sh 76# Fix all packages 77pnpm turbo lint:fix 78 79# Fix specific package 80pnpm --filter @atbb/appview lint:fix 81``` 82 83### Bypassing Hooks (Emergency Only) 84 85In urgent situations, bypass hooks with: 86 87```sh 88git commit --no-verify -m "emergency: your message" 89``` 90 91Use sparingly — hooks catch issues that would fail in CI. 92 93### How Hooks Work 94 95- **Lefthook** manages git hooks (`.lefthook.yml`) 96- **Oxlint** provides fast linting (`.oxlintrc.json`) 97- **Turbo** filters checks to affected packages only 98- Hooks auto-install after `pnpm install` via `prepare` script 99 100### Known Issues 101 102**Baseline TypeScript errors:** The codebase currently has 32 TypeScript errors that will cause the typecheck hook to block commits: 103- 23 errors in generated lexicon code (`packages/lexicon/dist/types/**/*.ts`) 104- 9 errors in source/test code (test context types, OAuth types) 105 106These are pre-existing issues that need to be resolved. Until fixed, use `--no-verify` when committing or temporarily disable the typecheck command in `.lefthook.yml`. 107 108## Testing Standards 109 110**CRITICAL: Always run tests before committing code or requesting code review.** 111 112### Running Tests 113 114```sh 115# Run all tests 116pnpm test 117 118# Run tests for a specific package 119pnpm --filter @atbb/appview test 120 121# Run tests in watch mode during development 122pnpm --filter @atbb/appview test --watch 123 124# Run a specific test file 125pnpm --filter @atbb/appview test src/lib/__tests__/config.test.ts 126``` 127 128### When to Run Tests 129 130**Before every commit:** 131```sh 132pnpm test # Verify all tests pass 133git add . 134git commit -m "feat: your changes" 135``` 136 137**Before requesting code review:** 138```sh 139pnpm build # Ensure clean build 140pnpm test # Verify all tests pass 141# Only then push and request review 142``` 143 144**After fixing review feedback:** 145```sh 146# Make fixes 147pnpm test # Verify tests still pass 148# Push updates 149``` 150 151### Test Requirements 152 153**All new features must include tests:** 154- API endpoints: Test success cases, error cases, edge cases 155- Business logic: Test all code paths and error conditions 156- Error handling: Test that errors are caught and logged appropriately 157- Security features: Test authentication, authorization, input validation 158 159**Test quality standards:** 160- Tests must be independent (no shared state between tests) 161- Use descriptive test names that explain what is being tested 162- Mock external dependencies (databases, APIs, network calls) 163- Test error paths, not just happy paths 164- Verify logging and error messages are correct 165 166**Red flags (do not commit):** 167- Skipped tests (`test.skip`, `it.skip`) without Linear issue tracking why 168- Tests that pass locally but fail in CI 169- Tests that require manual setup or specific data 170- Tests with hardcoded timing (`setTimeout`, `sleep`) - use proper mocks 171 172### Example Test Structure 173 174```typescript 175describe("createForumRoutes", () => { 176 it("returns forum metadata when forum exists", async () => { 177 // Arrange: Set up test context with mock data 178 const ctx = await createTestContext(); 179 180 // Act: Call the endpoint 181 const res = await app.request("/api/forum"); 182 183 // Assert: Verify response 184 expect(res.status).toBe(200); 185 const data = await res.json(); 186 expect(data.name).toBe("Test Forum"); 187 }); 188 189 it("returns 404 when forum does not exist", async () => { 190 // Test error case 191 const ctx = await createTestContext({ emptyDb: true }); 192 const res = await app.request("/api/forum"); 193 expect(res.status).toBe(404); 194 }); 195}); 196``` 197 198### Test Coverage Expectations 199 200While we don't enforce strict coverage percentages, aim for: 201- **Critical paths:** 100% coverage (authentication, authorization, data integrity) 202- **Error handling:** All catch blocks should be tested 203- **API endpoints:** All routes should have tests 204- **Business logic:** All functions with branching logic should be tested 205 206**Do not:** 207- Skip writing tests to "move faster" - untested code breaks in production 208- Write tests after requesting review - tests inform implementation 209- Rely on manual testing alone - automated tests catch regressions 210 211### Before Requesting Code Review 212 213**CRITICAL: Run this checklist before requesting review to catch issues early:** 214 215```sh 216# 1. Verify all tests pass 217pnpm test 218 219# 2. Check runtime dependencies are correctly placed 220# (Runtime imports must be in dependencies, not devDependencies) 221grep -r "from 'drizzle-orm'" apps/*/src # If found, verify in dependencies 222grep -r "from 'postgres'" apps/*/src # If found, verify in dependencies 223 224# 3. Verify error test coverage is comprehensive 225# For API endpoints, ensure you have tests for: 226# - Input validation (missing fields, wrong types, malformed JSON) 227# - Error classification (network→503, server→500) 228# - Error message clarity (user-friendly, no stack traces) 229``` 230 231**Common mistake:** Adding error tests AFTER review feedback instead of DURING implementation. Write error tests immediately after implementing the happy path — they often reveal bugs in error classification and input validation that are better caught before review. 232 233## Lexicon Conventions 234 235- **Source of truth is YAML** in `packages/lexicon/lexicons/`. Never edit generated JSON or TypeScript. 236- **Build pipeline:** YAML → JSON (`scripts/build.ts`) → TypeScript (`@atproto/lex-cli gen-api`). 237- **Adding a new lexicon:** Create a `.yaml` file under `lexicons/space/atbb/`, run `pnpm --filter @atbb/lexicon build`. 238- **Record keys:** Use `key: tid` for collections (multiple records per repo). Use `key: literal:self` for singletons. 239- **References:** Use `com.atproto.repo.strongRef` wrapped in named defs (e.g., `forumRef`, `subjectRef`). 240- **Extensible fields:** Use `knownValues` (not `enum`) for strings that may grow (permissions, reaction types, mod actions). 241- **Record ownership:** 242 - Forum DID owns: `forum.forum`, `forum.category`, `forum.role`, `modAction` 243 - User DID owns: `post`, `membership`, `reaction` 244 245## AT Protocol Conventions 246 247- **Unified post model:** There is no separate "topic" type. A `space.atbb.post` without a `reply` ref is a topic starter; one with a `reply` ref is a reply. 248- **Reply chains:** `replyRef` has both `root` (thread starter) and `parent` (direct parent) — same pattern as `app.bsky.feed.post`. 249- **MVP trust model:** The AppView holds the Forum DID's signing keys directly and writes forum-level records on behalf of admins/mods after verifying their role. This will be replaced by AT Protocol privilege delegation post-MVP. 250 251## TypeScript / Hono Gotchas 252 253- **`@types/node` is required** as a devDependency in every package that uses `process.env` or other Node APIs. `tsx` doesn't need it at runtime, but `tsc` builds will fail without it. 254- **Hono JSX `children`:** Use `PropsWithChildren<T>` from `hono/jsx` for components that accept children. Unlike React, Hono's `FC<T>` does not include `children` implicitly. 255- **HTMX attributes in JSX:** The `typed-htmx` package provides types for `hx-*` attributes. See `apps/web/src/global.d.ts` for the augmentation. 256- **Glob expansion in npm scripts:** `@atproto/lex-cli` needs file paths, not globs. Use `bash -c 'shopt -s globstar && ...'` to expand `**/*.json` in npm scripts. 257- **`.env` loading:** Dev and spike scripts use Node's `--env-file=../../.env` flag to load the root `.env` file. No `dotenv` dependency needed. 258- **API endpoint parameter type guards:** Never trust TypeScript types for user input. Change handler parameter types from `string` to `unknown` and add explicit `typeof` checks. TypeScript types are erased at runtime — a request missing the `text` field will pass type checking but crash with `TypeError: text.trim is not a function`. 259 ```typescript 260 // ❌ BAD: Assumes text is always a string at runtime 261 export function validatePostText(text: string): { valid: boolean } { 262 const trimmed = text.trim(); // Crashes if text is undefined! 263 // ... 264 } 265 266 // ✅ GOOD: Type guard protects against runtime type mismatches 267 export function validatePostText(text: unknown): { valid: boolean } { 268 if (typeof text !== "string") { 269 return { valid: false, error: "Text is required and must be a string" }; 270 } 271 const trimmed = text.trim(); // Safe - text is proven to be a string 272 // ... 273 } 274 ``` 275- **Hono JSON parsing safety:** `await c.req.json()` throws `SyntaxError` for malformed JSON. Always wrap in try-catch and return 400 for client errors: 276 ```typescript 277 let body: any; 278 try { 279 body = await c.req.json(); 280 } catch { 281 return c.json({ error: "Invalid JSON in request body" }, 400); 282 } 283 ``` 284 285## Error Handling Standards 286 287Follow these patterns for robust, debuggable production code: 288 289### API Route Handlers 290 291**Required for all database-backed endpoints:** 2921. Validate input parameters before database queries (return 400 for invalid input) 2932. Wrap database queries in try-catch with structured logging 2943. Check resource existence explicitly (return 404 for missing resources) 2954. Return proper HTTP status codes (400/404/500, not always 500) 296 297**Example pattern:** 298```typescript 299export function createForumRoutes(ctx: AppContext) { 300 return new Hono().get("/", async (c) => { 301 try { 302 const [forum] = await ctx.db 303 .select() 304 .from(forums) 305 .where(eq(forums.rkey, "self")) 306 .limit(1); 307 308 if (!forum) { 309 return c.json({ error: "Forum not found" }, 404); 310 } 311 312 return c.json({ /* success response */ }); 313 } catch (error) { 314 console.error("Failed to query forum metadata", { 315 operation: "GET /api/forum", 316 error: error instanceof Error ? error.message : String(error), 317 }); 318 return c.json( 319 { error: "Failed to retrieve forum metadata. Please try again later." }, 320 500 321 ); 322 } 323 }); 324} 325``` 326 327### Catch Block Guidelines 328 329**DO:** 330- Catch specific error types when possible (`instanceof RangeError`, `instanceof SyntaxError`) 331- Re-throw unexpected errors (don't swallow programming bugs like `TypeError`) 332- Log with structured context: operation name, relevant IDs, error message 333- Return user-friendly messages (no stack traces in production) 334 335**DON'T:** 336- Use bare `catch` blocks that hide all error types 337- Return generic "try again later" for client errors (400) vs server errors (500) 338- Fabricate data in catch blocks (return null or fail explicitly) 339- Use empty catch blocks or catch without logging 340 341### Helper Functions 342 343**Validation helpers should:** 344- Return `null` for invalid input (not throw) 345- Re-throw unexpected errors 346- Use specific error type checking 347 348**Example:** 349```typescript 350export function parseBigIntParam(value: string): bigint | null { 351 try { 352 return BigInt(value); 353 } catch (error) { 354 if (error instanceof RangeError || error instanceof SyntaxError) { 355 return null; // Expected error for invalid input 356 } 357 throw error; // Unexpected error - let it bubble up 358 } 359} 360``` 361 362**Serialization helpers should:** 363- Avoid silent fallbacks (log warnings if fabricating data) 364- Prefer returning `null` over fake values (`"0"`, `new Date()`) 365- Document fallback behavior in JSDoc if unavoidable 366 367### Defensive Programming 368 369**All list queries must have defensive limits:** 370```typescript 371.from(categories) 372.orderBy(categories.sortOrder) 373.limit(1000); // Prevent memory exhaustion on unbounded queries 374``` 375 376**Filter deleted/soft-deleted records:** 377```typescript 378.where(and( 379 eq(posts.rootPostId, topicId), 380 eq(posts.deleted, false) // Never show deleted content to users 381)) 382``` 383 384**Use ordering for consistent results:** 385```typescript 386.orderBy(asc(posts.createdAt)) // Chronological order for replies 387``` 388 389### Global Error Handler 390 391The Hono app must have a global error handler as a safety net: 392```typescript 393app.onError((err, c) => { 394 console.error("Unhandled error in route handler", { 395 path: c.req.path, 396 method: c.req.method, 397 error: err.message, 398 stack: err.stack, 399 }); 400 return c.json( 401 { 402 error: "An internal error occurred. Please try again later.", 403 ...(process.env.NODE_ENV !== "production" && { 404 details: err.message, 405 }), 406 }, 407 500 408 ); 409}); 410``` 411 412### Testing Error Handling 413 414**Test error classification, not just error catching.** Users need actionable feedback: "retry later" (503) vs "report this bug" (500). 415 416```typescript 417// ✅ Test network errors return 503 (retry later) 418it("returns 503 when PDS connection fails", async () => { 419 mockPutRecord.mockRejectedValueOnce(new Error("fetch failed")); 420 const res = await app.request("/api/topics", { 421 method: "POST", 422 body: JSON.stringify({ text: "Test" }) 423 }); 424 expect(res.status).toBe(503); // Not 500! 425 const data = await res.json(); 426 expect(data.error).toContain("Unable to reach your PDS"); 427}); 428 429// ✅ Test server errors return 500 (bug report) 430it("returns 500 for unexpected database errors", async () => { 431 mockPutRecord.mockRejectedValueOnce(new Error("Database connection lost")); 432 const res = await app.request("/api/topics", { 433 method: "POST", 434 body: JSON.stringify({ text: "Test" }) 435 }); 436 expect(res.status).toBe(500); // Not 503! 437 const data = await res.json(); 438 expect(data.error).not.toContain("PDS"); // Generic message for server errors 439}); 440 441// ✅ Test input validation returns 400 442it("returns 400 for malformed JSON", async () => { 443 const res = await app.request("/api/topics", { 444 method: "POST", 445 headers: { "Content-Type": "application/json" }, 446 body: "{ invalid json }" 447 }); 448 expect(res.status).toBe(400); 449 const data = await res.json(); 450 expect(data.error).toContain("Invalid JSON"); 451}); 452``` 453 454**Error classification patterns to test:** 455- **400 (Bad Request):** Invalid input, missing required fields, malformed JSON 456- **404 (Not Found):** Resource doesn't exist (forum, post, user) 457- **503 (Service Unavailable):** Network errors, PDS connection failures, timeouts — user should retry 458- **500 (Internal Server Error):** Unexpected errors, database errors — needs bug investigation 459 460## Documentation & Project Tracking 461 462**Keep these synchronized when completing work:** 463 4641. **`docs/atproto-forum-plan.md`** — Master project plan with phase checklist 465 - Mark items complete `[x]` when implementation is done and tested 466 - Add brief status notes with file references and Linear issue IDs 467 - Update immediately after completing milestones 468 4692. **Linear issues** — Task tracker at https://linear.app/atbb 470 - Update status: Backlog → In Progress → Done 471 - Add comments documenting implementation details when marking Done 472 - Keep status in sync with actual codebase state, not planning estimates 473 4743. **Workflow:** When finishing a task: 475 ```sh 476 # 1. Run tests to verify implementation is correct 477 pnpm test 478 479 # 2. If tests pass, commit your changes 480 git add . 481 git commit -m "feat: your changes" 482 483 # 3. Update plan document: mark [x] and add completion note 484 # 4. Update Linear: change status to Done, add implementation comment 485 # 5. Push and request code review 486 # 6. After review approval: include "docs:" prefix when committing plan updates 487 ``` 488 489**Why this matters:** The plan document and Linear can drift from reality as code evolves. Regular synchronization prevents rediscovering completed work and ensures accurate project status. 490 491## Git Conventions 492 493- Do not include `Co-Authored-By` lines in commit messages. 494- `prior-art/` contains git submodules (Rust AppView, original lexicons, delegation spec) — reference material only, not used at build time. 495- Worktrees with submodules need `submodule deinit --all -f` then `worktree remove --force` to clean up.