A personal watch vault for collectors — track your collection, gain insights, and export every detail for a single watch or the whole vault.
Languages: English (default) and German — switch under Settings → Language. Preference is stored per user (/api/users/me/locale) and in localStorage when logged out.
watchvault/
.env Single env file for both backend and frontend
.env.example Template — copy to .env
docker-compose.yml
frontend/ Next.js 16 — own package.json; npm install here
backend/ NestJS 11 — own package.json; npm install here
deploy/ Production Docker + Caddy (see deploy/README.md)
There is no npm project at the repo root — only shared config (.env, Docker).
cp .env.example .envA single .env at the project root is used by both backend and frontend. It contains sensible defaults for local development. Fill in S3/CDN values if you want image uploads and catalog images.
docker compose up -dStarts PostgreSQL on port 5432. Credentials: watchvault / watchvault_dev.
docker compose ps # verify it's runningcd backend
npm install # runs prisma generate via postinstall
npx prisma migrate dev # apply migrations
npm run start:dev # http://localhost:3001Swagger docs at http://localhost:3001/api/docs.
cd frontend
npm install
npm run dev # http://localhost:3000| Area | Description |
|---|---|
| Auth | Vault-style login/register with three-dial combination codes |
| Collection | CRUD, search, photos, box/papers, service history, catalog autocomplete |
| Insights | Collection overview — holding periods, service due, sold performance |
| Export | Printable export for a single watch or the full owned collection |
| Settings | Language (EN/DE), theme, vault combination |
| Admin | Root-only /admin — catalog maintenance, S3 image sync |
Translation files: frontend/src/i18n/locales/en.json and de.json.
Both projects read from the root .env file:
- Backend (NestJS):
ConfigModuleresolves../.envrelative tobackend/. - Backend (npm scripts):
dotenv-cliinbackend/package.jsoninjects the root.envviadotenv -e ../.env --. - Backend (Prisma):
prisma.config.tsloads../.envviadotenv.postinstallrunsprisma generate. - Frontend (Next.js): Reads
NEXT_PUBLIC_* vars from the root.envautomatically.
Do not create a separate backend/.env — it is not needed and would cause confusion.
Never commit .env or .env.production. Use the *.example templates only.
| Command | Description |
|---|---|
docker compose up -d |
Start PostgreSQL |
docker compose down |
Stop PostgreSQL |
docker compose down -v |
Stop + delete data |
docker compose logs postgres |
Show DB logs |
| Command | Description |
|---|---|
npm run start:dev |
Dev server with hot reload |
npm run start:debug |
Dev server with debugger |
npm run build |
Production build |
npm run start:prod |
Production server |
npm run lint |
Run ESLint |
npm test |
Unit tests |
npm run test:watch |
Unit tests (watch mode) |
npm run test:cov |
Unit tests with coverage |
npm run test:e2e:db |
Create test database |
npm run test:e2e |
API E2E tests |
npm run catalog:sync-images |
Sync catalog imageUrl fields from S3 (see below) |
See Testing and backend/test/README.md for details.
Links catalog rows in the database to objects already stored in S3. It does not upload files from your machine (except optional local dev files under backend/catalog/{brand}/images/ when an object is missing in the bucket).
S3 layout (per brand):
Catalog images live under catalog/{brand-slug}/, where {brand-slug} is the lowercase brand name from the catalog row (e.g. Rolex → rolex, Tissot → tissot).
| Brand | Example S3 key |
|---|---|
| Rolex | catalog/rolex/124060.png |
| Tissot | catalog/tissot/T137.407.11.041.00.png |
Slashes in reference numbers become underscores in the key (e.g. 5711/1A-010 → 5711_1A-010.png).
What it does:
- Loads every
watch_catalog_entriesrow (brand,reference_number) from PostgreSQL. - For each row, checks whether
catalog/{brand-slug}/{referenceNumber}.pngexists in the configured bucket (HeadObject). - When the object exists, sets
imageUrlon matching catalog rows to that S3 key. - Skips references with no object in the bucket; leaves their
imageUrlunchanged. - Skips rows that already have the correct S3 key (no S3 or DB writes on restart).
The API later turns those keys into CDN or presigned GET URLs for the frontend.
When to run:
| Situation | Action |
|---|---|
| Production deploy | Automatic — backend/docker-entrypoint.sh runs it after prisma migrate deploy on every backend start. |
| Local / manual | After seeding or updating objects in S3 (e.g. bucket copy from another env). |
Requirements: DATABASE_URL and S3_REGION, S3_BUCKET, S3_ACCESS_KEY, S3_SECRET_KEY in the root .env (IAM needs s3:HeadObject and s3:GetObject on the bucket). Build first, then run from backend/:
npm run build
npm run catalog:sync-imagesCatalog PNGs are not in git — populate the bucket out of band per brand folder (e.g. catalog/tissot/ for Tissot, catalog/rolex/ for Rolex), then sync.
| Command | Description |
|---|---|
npx prisma migrate dev --name <name> |
Create + run new migration |
npx prisma migrate deploy |
Run migrations in production |
npx prisma generate |
Regenerate Prisma client |
npx prisma studio |
Open database GUI in browser |
After schema changes, npm install in backend/ (or npx prisma generate) keeps the client in sync — also required if your IDE shows stale types for new columns (e.g. User.locale).
| Command | Description |
|---|---|
npm run dev |
Dev server (Webpack; lower RAM than default Turbopack) |
npm run dev:turbo |
Dev with Turbopack (faster HMR, higher memory use) |
npm run build |
Production build |
npm run start |
Production server |
npm run lint |
Run ESLint |
Frontend: Next.js 16, React 19, TypeScript, Tailwind CSS v4, Framer Motion, TanStack Query, Zustand, custom i18n (EN/DE)
Backend: NestJS 11, TypeScript, Prisma 7, PostgreSQL 16, Passport/JWT, Swagger/OpenAPI, AWS S3 SDK
| Setting | Endpoint | Values |
|---|---|---|
| Theme | GET/PATCH /api/users/me/theme |
neutral, evergreen, sport, heritage |
| Language | GET/PATCH /api/users/me/locale |
en, de |
| Export | Endpoint | Description |
|---|---|---|
| Single watch | GET /api/watches/:id/export |
Printable HTML with details, service history, documents |
| Full collection | GET /api/watches/export/insurance |
Owned watches with details, service history, documents |
After starting the backend, the interactive Swagger UI is available at:
http://localhost:3001/api/docs
Backend tests only (frontend is not covered yet). Two layers:
| Layer | Location | Database | Command |
|---|---|---|---|
| Unit | backend/src/**/*.spec.ts |
None (Prisma mocked) | npm test |
| E2E | backend/test/*.e2e-spec.ts |
watchvault_test |
npm run test:e2e |
Tests use a separate database so dev data is never touched. Config lives in .env.test at the repo root.
docker compose up -d # PostgreSQL must be running
cd backend
npm run test:e2e:db # create watchvault_test (once)
npm test # unit tests
npm run test:e2e # API e2e testsE2E runs migrations automatically via test/jest-e2e.global-setup.ts and truncates tables before each test.
Full test catalogue, helper overview, and rationale for file placement: backend/test/README.md