# ProjectX A book and library management app with Kobo device support. Multi-user, self-hostable. **Tech stack:** NestJS 11 (Fastify) + Vue 3 + PostgreSQL + Drizzle ORM --- ## Quick Start ### Prerequisites | Tool | Version | Install | |---------|---------|--------------------------------------------------------------| | Node.js | >= 24 | [nodejs.org](https://nodejs.org) | | pnpm | >= 9 | `corepack enable && corepack prepare pnpm@latest --activate` | | Docker | latest | [docker.com](https://www.docker.com/products/docker-desktop) | ### Setup ```bash git clone projectx cd projectx docker compose up -d --wait # start Postgres cp server/.env.example server/.env # create env config (edit if needed) pnpm install # install all dependencies cd server && pnpm db:migrate && cd .. # apply database migrations ``` Docker is only used for PostgreSQL. The server and client run directly on your machine. ### Start developing ```bash pnpm dev ``` This starts the NestJS server and Vue client concurrently: - **Client:** http://localhost:5173 - **API:** http://localhost:3000/api If you prefer separate terminals (cleaner logs): ```bash # Terminal 1 cd server && pnpm start:dev # Terminal 2 cd client && pnpm dev ``` Both must be running. The client proxies all `/api` and `/socket.io` requests to the server, so API calls will fail if only the client is running. ### First steps after setup 1. **Log in** - a default admin account is created automatically: - Username: `admin` - Password: `admin` 2. **Change password** - you'll be prompted to set a new password on first login. Use `Admin123!` for local dev. 3. **Create a library** - open Settings (gear icon) > Libraries > Create Library. Add a folder path pointing to some books on your machine, e.g. any directory containing `.epub`, `.pdf`, or `.cbz` files. 4. **Scan** - the library scans automatically after creation. You should see books appear on the home page once the scan completes. > **Where does data go?** `BOOKS_PATH` in `server/.env` defaults to `../local/data` (resolves to `/local/data`). This is the app's data directory where extracted cover images and thumbnails are stored (`local/data/covers/`). It is **not** where your book files live - those paths are configured per-library in the UI. The `local/` folder is gitignored. --- ## Project Structure ``` projectx/ ├── client/ Vue 3 frontend (Vite + Tailwind CSS v4) ├── server/ NestJS 11 backend (Fastify + Drizzle ORM) ├── packages/ │ └── types/ Shared TypeScript types (@projectx/types) ├── docker/ │ └── postgres/ Postgres init scripts (extensions) ├── local/ Local dev data (covers, docs) - gitignored ├── docker-compose.yml Dev: runs Postgres only (app runs natively) ├── Dockerfile Production multi-stage build └── Dockerfile.dev Runs entire stack in Docker (not used for normal dev) ``` ### Monorepo layout This is a **pnpm workspace**. The three packages are: - **`server/`** - NestJS API. Modules live in `src/modules/`, one folder per feature. Database schema is in `src/db/schema/`. - **`client/`** - Vue 3 SPA. Feature code lives in `src/features/`, shared components in `src/components/ui/`. - **`packages/types/`** - Shared types imported as `@projectx/types` by both server and client. Add any type that crosses the API boundary here. --- ## Common Commands All commands run from the **repo root** unless noted otherwise. ### Development | Command | Description | |-------------------------------|--------------------------------------| | `pnpm dev` | Start server + client (concurrently) | | `cd server && pnpm start:dev` | Server only (watch mode) | | `cd client && pnpm dev` | Client only (Vite dev server) | ### Testing | Command | Description | |----------------------------------------------------|---------------------------| | `cd server && pnpm test` | Server unit tests | | `cd server && pnpm test -- --testPathPattern=scan` | Run matching server tests | | `cd client && pnpm test:unit` | Client unit tests | | `cd client && pnpm test:unit -- BookCover` | Run matching client tests | | `cd server && pnpm test:e2e` | Server E2E tests | ### Database | Command | Description | |----------------------------------------|------------------------------------------| | `cd server && pnpm db:migrate` | Apply pending migrations | | `cd server && pnpm db:generate ` | Generate a migration from schema changes | | `cd server && pnpm db:studio` | Open Drizzle Studio (DB browser) | ### Linting & formatting | Command | Description | |---------------------------------------|--------------------| | `cd server && npx prettier --write .` | Format server code | | `cd client && npx prettier --write .` | Format client code | | `cd server && pnpm lint` | Lint server | | `cd client && pnpm lint` | Lint client | --- ## Development Workflow Once `pnpm dev` is running, here's how the feedback loop works for each type of change: ### Frontend (`client/`) Save a file → Vite HMR hot-swaps the module → browser updates instantly. No restart needed. ### Backend (`server/`) Save a file → NestJS watch mode (SWC) recompiles and restarts the server automatically. Takes about a second. ### Shared types (`packages/types/`) The types package points directly at `.ts` source files. Edit a type → both client and server pick it up on their next rebuild cycle. No build step needed. ### Database schema Schema changes are the one thing that isn't automatic. See [Database Workflow](#database-workflow) for the generate + migrate steps. ### Environment variables The `start:dev` script reads `server/.env` once at startup. If you change a value in `.env`, you must restart `pnpm dev` for it to take effect. ### After `git pull` If other people have pushed changes, you may need to sync dependencies and migrations: ```bash pnpm install # pick up lockfile changes cd server && pnpm db:migrate # apply any new migrations ``` ### Adding a dependency Always install into the correct workspace, not the root: ```bash cd server && pnpm add # backend dependency cd client && pnpm add # frontend dependency ``` ### Adding a new feature The fastest way to learn the patterns is to follow an existing module: - **Backend:** Look at `server/src/modules/bookmark/` for a minimal example (controller, service, repository, DTO, module). Copy it, rename, and adapt. - **Frontend:** Look at `client/src/features/collection/` for a full feature (composables for state/API, components for UI). - **Shared types:** If the feature adds API request/response shapes, add them in `packages/types/src/` and import via `@projectx/types`. --- ## Environment Variables Server environment is configured in `server/.env` (created from `.env.example` during setup). | Variable | Default | Description | |--------------------------|--------------------------------------------------------|------------------------------------------------------------| | `DATABASE_URL` | `postgres://projectx:projectx@localhost:5432/projectx` | PostgreSQL connection string | | `PORT` | `3000` | Server port | | `NODE_ENV` | `development` | Environment mode | | `JWT_SECRET` | `change-me-in-production` | JWT signing secret | | `JWT_EXPIRES_IN` | `15m` | Access token lifetime | | `JWT_REFRESH_EXPIRES_IN` | `7d` | Refresh token lifetime | | `BOOKS_PATH` | `../local/data` | App data directory for cover images (not where books live) | | `SMTP_*` | (empty) | Optional SMTP config for password-reset emails | | `APP_URL` | `http://localhost:5173` | Client URL (used in emails) | --- ## Database Workflow The database schema is defined in `server/src/db/schema/` using Drizzle ORM. Each domain has its own schema file (e.g. `books.ts`, `libraries.ts`, `auth.ts`). **Making schema changes:** 1. Edit the relevant file in `server/src/db/schema/` 2. Generate a migration: `cd server && pnpm db:generate describe_your_change` 3. Apply it: `cd server && pnpm db:migrate` Never hand-write migration SQL. Always generate from schema diffs. ### Resetting the database from scratch ```bash # 1. Drop and recreate the public schema docker exec $(docker ps --filter "name=postgres" --format "{{.Names}}" | head -1) \ psql -U projectx -d projectx -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" # 2. Clear Drizzle's migration tracking (it lives in a separate schema) docker exec $(docker ps --filter "name=postgres" --format "{{.Names}}" | head -1) \ psql -U projectx -d projectx -c "DELETE FROM drizzle.__drizzle_migrations;" # 3. Re-apply all migrations cd server && pnpm db:migrate # 4. (Optional) Wipe cover images rm -rf local/data/covers ``` Step 2 is necessary because Drizzle tracks applied migrations in `drizzle.__drizzle_migrations`, which lives outside the `public` schema and survives the drop. --- ## Architecture Overview ``` ┌──────────────┐ ┌──────────────────────┐ ┌────────────┐ │ Vue 3 SPA │──────>│ NestJS API (/api) │──────>│ PostgreSQL │ │ port 5173 │<──────│ port 3000 │<──────│ port 5432 │ └──────────────┘ └──────────────────────┘ └────────────┘ │ │ │ WebSocket │ Drizzle ORM │ (socket.io) │ (schema in src/db/schema/) └────────────────────────┘ ``` - **Backend:** NestJS 11 on Fastify. Global prefix `/api`. Auth via JWT (access + refresh tokens in httpOnly cookies). RBAC with permissions system. - **Frontend:** Vue 3 Composition API (`