Getting started#
Prerequisites: Bun and Node.js v22. Bun runs the appview and tests. Node is needed for the local ATProto PDS (@atproto/pds requires better-sqlite3 which only builds against Node 22).
Use mise to pin Node 22: mise use node@22. Or nvm: nvm use 22.
bun install
bun run dev # appview on :3000 (watch mode)
bun run test:unit # unit tests (no Node needed)
mise run test # full suite (unit + integration, needs Node 22)
bun run lint # check with Biome
bun run typecheck # tsgo --noEmit
The database auto-seeds with sample data on first run. To reset, delete lichen.db and restart.
Full local stack#
bun run dev:full starts a test PDS (Node), the appview, and the firehose subscriber. Two test accounts are created automatically. Log in via:
http://localhost:3000/dev/login/alice.testhttp://localhost:3000/dev/login/bob.test
All commands#
bun run dev # appview server (watch mode)
bun run dev:full # full local stack: PDS + appview + firehose
bun run dev:firehose # firehose subscriber only
bun run dev:viz # watch-mode viz bundle
bun run dev:editor # watch-mode editor bundle
bun run test:unit # unit tests only (no Node needed)
mise run test:integration # integration tests (Node 22 for PDS subprocess)
mise run test # full suite (unit + integration)
bun run lint:fix # auto-fix lint/format
bun run typecheck # type check
bun run build:css # build Tailwind CSS
bun run build:viz # bundle viz renderers
bun run build:editor # bundle editor
Project layout#
src/
server/ # Elysia routes, database (schema, queries/, seed)
atproto/ # OAuth, PDS write functions
firehose/ # standalone firehose subscriber
lib/ # shared logic (diff, markdown, slugs, viz, image, blob, i18n, urls...)
shared/ # shared type definitions
views/ # HTML templates
lexicons/ # ATProto lexicon schemas
public/ # static assets (editor, viz, CSS)
scripts/ # dev tooling
tests/ # mirrors src/ structure
Code style#
- TypeScript, strict mode
- Biome for formatting (tabs, double quotes), run
bun run lint:fixbefore committing - Named exports, no default exports
- Functions over classes unless state is needed
- Throw typed error classes (
WikiNotFoundError,AccessDeniedError,PdsWriteError), caught by a singleonErrorhook. Route handlers never catch errors themselves. - Route handlers stay thin: orchestrators in
src/lib/orchestrators/own the PDS-then-DB lifecycle. Routes parse input, call the orchestrator, redirect. - PDS write must succeed before DB write. If PDS fails, the request fails.
- No ORMs, raw SQL with prepared statements
- UI strings go through
src/lib/i18n/, no hardcoded English in views - Tailwind classes come from
src/views/theme.ts(THEME.*), no hardcoded class strings in views
Database#
SQLite via bun:sqlite. Schema in src/server/db/schema.ts, queries in src/server/db/queries/. All queries use prepared statements, writes are wrapped in transactions.
ATProto#
Lexicon namespace: wiki.lichen (domain: lichen.wiki). Schemas in lexicons/.
Record types: wiki.lichen.wiki, wiki.lichen.note, wiki.lichen.noteRevision, wiki.lichen.memberRequest, wiki.lichen.membership, wiki.lichen.bookmark
In dev mode (no OAuth configured), PDS writes are skipped and a mock DID is used.
Tests#
Tests mirror src/ under tests/. All test files share one in-memory DB singleton (tests/preload.ts sets DB_PATH=:memory:), so tests that create data must clean up in afterAll.
Integration tests in tests/integration/ write real ATProto records to a PDS subprocess and verify they flow through the firehose into the appview DB. The PDS uses better-sqlite3 (native addon), which requires Node 22. Run them via mise run test:integration to ensure Node 22 is on PATH.