Skip to content

Repository files navigation

WatchVault

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.

Prerequisites

Project Structure

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).

Quick Start

1. Environment Variables

cp .env.example .env

A 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.

2. Start Database

docker compose up -d

Starts PostgreSQL on port 5432. Credentials: watchvault / watchvault_dev.

docker compose ps          # verify it's running

3. Start Backend

cd backend
npm install                # runs prisma generate via postinstall
npx prisma migrate dev     # apply migrations
npm run start:dev           # http://localhost:3001

Swagger docs at http://localhost:3001/api/docs.

4. Start Frontend

cd frontend
npm install
npm run dev                 # http://localhost:3000

Features (overview)

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.

Environment

Both projects read from the root .env file:

  • Backend (NestJS): ConfigModule resolves ../.env relative to backend/.
  • Backend (npm scripts): dotenv-cli in backend/package.json injects the root .env via dotenv -e ../.env --.
  • Backend (Prisma): prisma.config.ts loads ../.env via dotenv. postinstall runs prisma generate.
  • Frontend (Next.js): Reads NEXT_PUBLIC_* vars from the root .env automatically.

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.

Commands Reference

Docker / Database

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

Backend (cd backend)

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.

catalog:sync-images

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. Rolexrolex, Tissottissot).

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-0105711_1A-010.png).

What it does:

  1. Loads every watch_catalog_entries row (brand, reference_number) from PostgreSQL.
  2. For each row, checks whether catalog/{brand-slug}/{referenceNumber}.png exists in the configured bucket (HeadObject).
  3. When the object exists, sets imageUrl on matching catalog rows to that S3 key.
  4. Skips references with no object in the bucket; leaves their imageUrl unchanged.
  5. 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-images

Catalog 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.

Prisma (cd backend)

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).

Frontend (cd frontend)

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

Tech Stack

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

User preferences API

Setting Endpoint Values
Theme GET/PATCH /api/users/me/theme neutral, evergreen, sport, heritage
Language GET/PATCH /api/users/me/locale en, de

Export API

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

API Documentation

After starting the backend, the interactive Swagger UI is available at:

http://localhost:3001/api/docs

Testing

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

Setup

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 tests

E2E runs migrations automatically via test/jest-e2e.global-setup.ts and truncates tables before each test.

Documentation

Full test catalogue, helper overview, and rationale for file placement: backend/test/README.md

About

Organizer for your watch collection

Resources

Stars

Watchers

Forks

Contributors

Languages