# Development Guide
Everything you need to work in this codebase: architecture, setup, workflows, conventions, and commands.
For the contribution process (issues, PRs, review), see [CONTRIBUTING.md](CONTRIBUTING.md). For test architecture and E2E suite details, see [TESTING.md](TESTING.md).
---
## Architecture Overview
BookOrbit is a pnpm monorepo with three workspaces: a Vue 3 single-page app, a NestJS API server, and a shared types package.
```mermaid
graph LR
Client["Client
Vue 3 SPA
localhost:5173"]
Server["Server
NestJS + Fastify
localhost:3000/api"]
DB["PostgreSQL
Drizzle ORM"]
WS["WebSocket
Socket.IO"]
Types["Shared Types
@bookorbit/types"]
Client -- "HTTP (fetch)" --> Server
Client <-. "real-time events" .-> WS
Server --> DB
Server --> WS
Types -.- Client
Types -.- Server
```
| Layer | Tech | Location |
| ------------ | ------------------------------- | ----------------- |
| Frontend | Vue 3, Tailwind CSS v4, Vite | `client/` |
| Backend | NestJS 11, Fastify, Drizzle ORM | `server/` |
| Database | PostgreSQL 16, pgvector | Docker container |
| Shared types | TypeScript | `packages/types/` |
| Real-time | Socket.IO | Server + Client |
---
## Project Structure
```text
bookorbit/
├── client/ # Vue 3 frontend
│ └── src/
│ ├── features/ # Feature modules (composables + components)
│ ├── components/ui/ # Shared UI components
│ ├── lib/ # API client, utilities
│ ├── router/ # Vue Router config
│ └── assets/ # Tailwind theme, static assets
├── server/ # NestJS backend
│ └── src/
│ ├── modules/ # Feature modules (controller + service + dto)
│ ├── db/
│ │ ├── schema/ # Drizzle table definitions (one file per domain)
│ │ └── migrations/ # Generated SQL migrations
│ ├── common/ # Guards, filters, decorators, pipes
│ └── config/ # Typed config (app, db, auth)
│ └── test/
│ ├── e2e/ # E2E test helpers and harness
│ └── *.e2e-spec.ts # E2E test suites
├── packages/
│ └── types/ # Shared TypeScript types
│ └── src/ # Exported via @bookorbit/types
├── scripts/ # Bootstrap, DB reset, E2E orchestration
├── docker/ # Postgres init scripts (extensions)
├── .github/ # CI workflows, PR/issue templates
└── .husky/ # Git hooks (pre-commit, pre-push)
```
> **Shared types** live in `packages/types/` and are imported as `@bookorbit/types`. Never duplicate types between client and server.
---
## Prerequisites
- **Node.js** >= 24 - [nodejs.org](https://nodejs.org)
- **pnpm** >= 9 - `npm install -g pnpm` (or see [pnpm.io](https://pnpm.io/installation))
- **Docker** - [docs.docker.com/get-started/get-docker](https://docs.docker.com/get-started/get-docker/)
---
## First-Time Setup
### Option A: One Command
```bash
pnpm setup
```
This runs all the steps below automatically. If it succeeds, skip to [Running the App](#running-the-app).
---
### Option B: Step by Step
#### Step 1 - Clone the repository
```bash
git clone https://github.com//bookorbit.git
cd bookorbit
```
If you are an external contributor, fork first and clone your fork. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
#### Step 2 - Create your environment file
```bash
cp server/.env.example server/.env
```
Only do this once. If `server/.env` already exists, skip this step. Copying again will overwrite any local changes you have made.
The defaults in `.env.example` work out of the box for local development. You only need to edit this file if you want to change ports, point to a different database, or configure optional features like email.
See the [Key Environment Variables](#key-environment-variables) section for what each variable does.
#### Step 3 - Install dependencies
```bash
pnpm install
```
This installs dependencies for all three workspaces (server, client, packages/types) in one shot.
#### Step 4 - Start PostgreSQL
```bash
docker compose -f docker-compose.dev.yml up -d --wait
```
This starts a PostgreSQL 16 container (with the `pgvector` extension) on port 5432. The `--wait` flag blocks until the database is healthy. Data is persisted in a named Docker volume across restarts.
> **Already have PostgreSQL running?** You can point `DATABASE_URL` in `server/.env` at your existing instance, but you will still need the `uuid-ossp`, `pg_trgm`, `unaccent`, and `vector` extensions installed. Using Docker is the easiest path.
#### Step 5 - Apply database migrations
```bash
pnpm run db:migrate
```
This creates all tables and indexes. Migrations are generated by Drizzle Kit and live in `server/src/db/migrations/`.
#### Step 6 - Seed baseline data
```bash
pnpm run db:seed
```
This populates the database with the data required for the app to function (default roles, permissions, initial settings). You only need to run this once.
#### Step 7 - Start the app
```bash
pnpm dev
```
If everything worked, you will see the server, client, and types watcher start up. The app is ready when you see output from all three processes.
---
### Verifying Your Setup
| Check | Expected |
| ---------------------------------------- | ------------------------------------------ |
| Open http://localhost:5173 | BookOrbit login page loads |
| Open http://localhost:3000/api/v1/health | Returns `{"status":"ok"}` |
| `docker ps` | Shows `bookorbit-dev-db` container running |
---
## Running the App
```bash
pnpm dev
```
This starts the server, client, and types watcher concurrently with hot reload.
| Service | URL |
| ------- | ------------------------- |
| Client | http://localhost:5173 |
| API | http://localhost:3000/api |
If PostgreSQL is not running, start it first with `pnpm run db:up`.
### API Docs
Generated OpenAPI docs are disabled by default. To enable them for local development, set `SWAGGER_ENABLED=true` in `server/.env` or prefix the dev command:
```bash
SWAGGER_ENABLED=true pnpm dev
```
When enabled, open:
| Docs | URL |
| ------------ | ----------------------------------- |
| Swagger UI | http://localhost:3000/api/docs |
| OpenAPI JSON | http://localhost:3000/api/docs-json |
Running workspaces separately
```bash
# Terminal 1: Server
cd server && pnpm start:dev
# Terminal 2: Client
cd client && pnpm dev
```
If you are editing shared types (`packages/types/`), also start the types watcher:
```bash
# Terminal 3: Types (only needed when editing packages/types/)
pnpm --filter @bookorbit/types dev
```
---
## Key Environment Variables
These are the variables you are most likely to need during development. See `server/.env.example` for the full list with comments.
| Variable | Purpose | Default (dev) |
| --------------------- | ---------------------------------- | --------------------------------------------------------- |
| `DATABASE_URL` | PostgreSQL connection string | `postgres://bookorbit:bookorbit@localhost:5432/bookorbit` |
| `PORT` | Server listen port | `3000` |
| `NODE_ENV` | Runtime mode | `development` |
| `JWT_SECRET` | Signing key for auth tokens | `change-me-in-production` |
| `APP_DATA_PATH` | Storage for covers, avatars, cache | `../local/data` |
| `LIBRARY_BROWSE_ROOT` | Library folder picker root | `/` |
| `APP_URL` | Base URL for email links | `http://localhost:5173` |
| `SWAGGER_ENABLED` | Serve Swagger UI and OpenAPI JSON | `false` |
---
## Database
### Schema Organization
Database tables are defined as Drizzle `pgTable()` calls in `server/src/db/schema/`, one file per domain (e.g. `books.ts`, `auth.ts`, `kobo.ts`). All files are re-exported from `server/src/db/schema/index.ts`.
Type inference uses `typeof table.$inferSelect` and `$inferInsert`. Never write manual type aliases for DB rows.
### Common Commands
| Command | What it does |
| -------------------------------------- | ----------------------------------------------- |
| `pnpm run db:up` | Start PostgreSQL via Docker |
| `pnpm run db:migrate` | Apply pending migrations |
| `cd server && pnpm db:generate ` | Generate a migration from schema changes |
| `pnpm run db:seed` | Seed baseline data |
| `pnpm run db:reset` | Drop everything, re-migrate, re-seed (dev only) |
| `cd server && pnpm db:studio` | Open Drizzle Studio (visual DB browser) |
### Making a Schema Change
1. Edit the relevant file in `server/src/db/schema/`.
2. Generate a migration: `cd server && pnpm db:generate describe-your-change`
3. Review the generated SQL in `server/src/db/migrations/`.
4. Apply it: `pnpm run db:migrate`
Never hand-write migration SQL. Always let Drizzle Kit generate it from schema diffs.
### Migration Strategy
**Before production launch (current phase):** We maintain a single baseline migration (`0000_*.sql`). When adding new tables or columns, wipe the migrations folder and regenerate:
```bash
rm -rf server/src/db/migrations
docker compose -f docker-compose.dev.yml down -v && docker compose -f docker-compose.dev.yml up -d --wait
cd server && pnpm db:generate baseline && pnpm run db:migrate
```
**After production launch:** Never wipe migrations. Every schema change gets its own incremental migration.
### Full Reset
When local migration state is broken or you need a clean slate:
```bash
pnpm run db:reset
```
If that is insufficient (e.g. corrupted Docker volumes), stop the container with `-v` to drop volumes, then start fresh:
```bash
docker compose -f docker-compose.dev.yml down -v
pnpm run db:up
pnpm run db:migrate
pnpm run db:seed
```
---
## Testing
| Command | What it runs |
| -------------------------------- | -------------------------------- |
| `pnpm run test` | Server + client unit tests |
| `pnpm run test:server` | Server unit tests only |
| `pnpm run test:client` | Client unit tests only |
| `cd server && pnpm test:watch` | Server tests in watch mode |
| `pnpm run e2e:run -- ` | Run a single E2E suite |
| `pnpm run e2e:all` | Run all E2E suites sequentially |
| `pnpm run e2e:list` | List available E2E suite IDs |
| `pnpm run coverage` | Unit tests with coverage reports |
Server unit tests use Vitest with `@nestjs/testing`. Client tests use Vitest with `@vue/test-utils` in a jsdom environment. Most E2E suites boot the full app against a dedicated test database; a few smoke-style suites run against a shared E2E database without a reset.
For test architecture, suite details, coverage thresholds, the E2E harness, and examples, see [TESTING.md](TESTING.md).
---
## Code Quality
### Quality Gates
| Command | What it checks | When to use |
| ------------------------ | -------------------------------------------- | ---------------------------- |
| `pnpm run verify:fast` | Lint + typecheck + tests | Quick check while developing |
| `pnpm run verify` | Lint + typecheck + tests (full) | Before opening a PR |
| `pnpm run verify:strict` | Format check + lint + full typecheck + tests | Strictest gate |
### Individual Checks
| Command | What it does |
| ----------------------- | -------------------------------------- |
| `pnpm run lint:check` | ESLint across server + client |
| `pnpm run lint:fix` | ESLint auto-fix across server + client |
| `pnpm run typecheck` | TypeScript type checking |
| `pnpm run format` | Prettier format across server + client |
| `pnpm run format:check` | Prettier check (no write) |
### Git Hooks
Two hooks run automatically via [Husky](https://typicode.github.io/husky/):
- **Pre-commit:** Runs `lint-staged` on staged files. This auto-fixes lint issues and formats code with Prettier. You do not need to format manually before committing.
- **Pre-push:** Runs `pnpm verify:fast`. Your push is blocked if lint, typecheck, or tests fail.
---
## Must-Know Conventions
These are the rules that most commonly cause PR rejections. Following them from the start will save you review cycles.
### Backend
- **One module per feature.** Each domain lives in `server/src/modules//` with its own controller, service, module, and `dto/` subfolder.
- **DTOs for all input.** Request bodies are validated via `class-validator` decorators on DTO classes. The global `ValidationPipe` rejects unknown fields.
- **Throw NestJS exceptions.** Use `NotFoundException`, `BadRequestException`, `ForbiddenException`, etc. Never throw raw `Error`.
- **Multi-user ownership.** User-owned data needs a `userId` foreign key. Services must check ownership and throw `ForbiddenException` for non-owners.
### Frontend
- **Composition API only.** Every component uses `