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
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.