Appearance
Contributing
Prerequisites
- Node.js 24 — install via fnm or mise. The repo has a
.node-versionfile that both tools pick up automatically. - pnpm 10.x —
npm install -g pnpmor let mise/fnm handle it - Docker — for Postgres 17 + Redis 7 (recommended), or install them locally
Setup
bash
git clone <repo-url>
cd libris
pnpm install
cp .env.example .env # defaults match the Docker compose belowStart Postgres and Redis:
bash
docker compose -f docker-compose.dev.yml up -dData is persisted to ./tmp/postgres and ./tmp/redis (gitignored). To wipe state, stop the stack and delete those directories.
Start both servers:
bash
vp run -F @libris/api-hono dev # API on port 3000
vp run -F @libris/web dev # SPA on port 3100On first visit, the settings page prompts you to create an API key.
Project Structure
apps/web/ Vue 3 + Vite 8 SPA frontend
services/api-hono/ Hono API backend
tests/e2e/ Playwright E2E tests
docs/ VitePress documentation
scripts/ Build and test helpersSee architecture.md for how the pieces connect.
Development Workflow
Running checks
bash
vp run check # Format + lint + typecheck (all packages) + docs build
vp run test # Unit tests (root, recursive)
vp run -F @libris/api-hono test # API integration tests (PGlite, no external DB needed)
vp run -F @libris/e2e e2e # E2E tests (requires dev servers running)Always run vp run check before pushing. CI runs the same checks. The docs workspace validates via vitepress build, so broken links, missing pages, or invalid frontmatter fail the check.
Adding an API endpoint
- Define the route with
createRoutefrom@hono/zod-openapiinservices/api-hono/src/routes/api/ - Add Zod schemas for request/response in
services/api-hono/src/shared/schemas.ts - The
hcclient on the frontend automatically picks up the new route — no codegen step - Verify the OpenAPI docs at
http://localhost:3000/_docs/scalar
Working on the frontend
Pages use query composables built on useQuery from @pinia/colada:
ts
// composables/queries/useBookDetailQuery.ts
import { useQuery } from "@pinia/colada";
export function useBookDetailQuery(id: Ref<string>) {
const client = useApiClient();
return useQuery({
key: () => ["library", id.value],
query: async () => {
const res = await client.api.library[":id"].$get({ param: { id: id.value } });
if (!res.ok) throw new Error("Not found");
return res.json();
},
staleTime: 30_000,
});
}- Cover/download URLs are plain strings:
`${config.apiBaseUrl}/api/library/${id}/cover` - File uploads use
useUpload()(XHR with progress tracking) - Real-time events use
useServerEvents()(WebSocket-backed, one connection per tab)
Every interactive element needs a data-testid attribute for E2E test stability.
Database changes
bash
# Edit the schema
$EDITOR services/api-hono/src/db/schema.ts
# Generate a migration
cd services/api-hono && vp exec drizzle-kit generate
# Migrations auto-apply on API startup — just restart the dev serverNever edit applied migrations. See database.md for the full schema.
Changesets
Every code change needs a changeset before pushing:
bash
pnpm changeset
# Select affected packages, describe the change
# Commit the .changeset/*.md file with your codeE2E Tests
bash
# With dev servers running
vp run -F @libris/e2e e2e
# Self-contained (starts its own Postgres + Redis in Docker)
vp run test:e2e:dockerE2E tests use Playwright with Chromium. See testing.md for details.
Resetting local queue diagnostics
BullMQ history lives in Redis, not PostgreSQL. If you recreate the database or clear the inbox/library paths, the Home and Settings diagnostics can still show old queue completions or failed jobs until the BullMQ keys are cleared too.
For a truly fresh local reset, run:
bash
vp run -F @libris/api-hono reset:bullmqThis deletes only Libris BullMQ keys for the known queues; it does not flush the entire Redis database.
Multi-user test patterns
The test suite supports two authenticated sessions: an admin and a regular user.
Page fixtures (from tests/e2e/fixtures.ts):
authedPage/adminPage— admin session (loaded from.auth/user.json)userPage— non-admin session (loaded from.auth/regular-user.json, created in its own browser context)
API-level helpers (from tests/e2e/helpers/index.ts):
authHeaders()— Bearer headers for the admin keyuserAuthHeaders()— Bearer headers for the regular-user keygetAdminKeyId()— fetches the admin API key ID, useful for seedingcreated_byon books
Gotchas when seeding data:
reading_progress.api_key_idis NOT NULL. Always include it when inserting rows — usegetAdminKeyId()or the equivalent user key ID.- The unique constraint on
reading_progressis(api_key_id, document, device), not(document, device). ON CONFLICT clauses must target all three columns.
See tests/e2e/multi-user-auth.spec.ts for working examples of ownership checks, credential rotation, and cross-user 403 assertions.
Issue-First Workflow
Open an issue before starting work. This applies to features, bugs, and refactors — anything non-trivial. It prevents duplicate work, makes it easy to track what's in progress, and gives others a chance to weigh in before code is written.
Use the forge issue tracker. Keep the title specific, describe what you're changing and why.
AI Agents
Agents are encouraged and actively used in this project. That said, using an agent badly is worse than not using one — you still own the output.
What good agent use looks like:
- Open an issue first, then point the agent at it
- Read the diff before you commit it. If you wouldn't write it yourself, don't ship it
- Run
vp run checkand the relevant tests before opening a PR — the agent should do this, and you should verify it did - Extend or add tests when the agent adds functionality; don't let it skip them
- Review for security issues: injection, auth bypass, sensitive data in logs, overly permissive configs. Agents miss these
- One PR per logical change. If the agent opens a sprawling refactor alongside a bug fix, split it
What to avoid:
- Iterating blindly until CI goes green without understanding why it was red
- Accepting "it works" as a substitute for "it's correct"
- Letting the agent add abstractions, helpers, or config options the task doesn't require
- Skipping the changeset because the agent forgot it
The bar for a PR is the same regardless of whether a human or agent wrote the code.
Code Style
- TypeScript everywhere — strict mode enabled in all packages
- oxfmt for formatting, oxlint for linting (both run via
vp run check) - Zod for all validation — request params, response bodies, env vars
- Prefer simple code over abstractions. Three similar lines > a premature helper
- No hand-written API response types —
hcinfers them from Zod schemas
Docker
bash
# Build unified image (API + SPA)
docker build -t libris .See deployment.md for production configuration.