Getting started #
Prerequisites: Bun. Everything runs under Bun — no Node, no native addons.
bun install
bun run dev # appview on :3000 (watch mode)
bun test # full test suite
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.
All commands #
bun run dev # appview server (watch mode)
bun run dev:firehose # firehose subscriber only
bun run dev:viz # watch-mode viz bundle
bun run dev:editor # watch-mode editor bundle
bun test # full test suite
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)
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.
Tests in tests/integration/ are scenario tests that synthesize firehose commit events and invoke handleCommitEvent directly — no real PDS, no Node subprocess. They cover multi-step flows (revision chaining, multi-user edits, membership lifecycle) on top of the per-event coverage in tests/firehose/handlers.test.ts.