A personal collection management app for tracking music albums and guitar gear. Built with a Scala/Tapir backend and a React/TypeScript frontend.
- Music Wishlist — Add albums you want, track their status through Wanted → Ordered → Received
- Album Collection — Browse your owned albums with search, sort, and filter by format/year
- Guitar Gear — Manage guitars (with string change history), amplifiers, and pedals
| Layer | Technology |
|---|---|
| Backend | Scala 3, Tapir, Netty, Cats Effect, FS2, Circe |
| Frontend | React 19, TypeScript, Vite, TailwindCSS v4, TanStack Query, React Router |
| UI | shadcn/ui, Lucide icons, OKLch color themes |
| API Contract | OpenAPI 3.1 (auto-generated from Tapir endpoints) |
| Persistence | File-based JSON (no external database) |
personal-app/
├── backend/ # Scala API server (port 8080)
│ ├── App.scala # Entry point, server setup
│ ├── album-inventory/ # Music domain (albums, wishlist, event bus)
│ ├── guitar-gear/ # Gear domain (guitars, amps, pedals)
│ └── json/ # JSON file I/O utilities
├── frontend/ # React SPA
│ ├── src/
│ │ ├── api/ # Typed API client (generated from OpenAPI)
│ │ ├── pages/ # Route pages
│ │ └── components/ # Reusable UI components
│ └── ...
├── openapi.yaml # Auto-generated OpenAPI spec
├── generateOpenApi.scala # Script to regenerate the spec
└── .env # Environment variables
- Scala CLI — install
- Node.js 22 (see
.nvmrc) - pnpm —
npm install -g pnpm
With a .env file in place (see below), build the frontend and run everything from the backend:
just runThen open http://localhost:8080 — the backend serves both the API (under /api) and the built frontend (with SPA fallback to index.html). The frontend build directory can be overridden with the FRONTEND_DIST env var (defaults to frontend/dist).
Create a .env file at the project root:
DB_BASE_PATH=/path/to/your/data/directoryThe backend stores JSON files in this directory. It will be created if it doesn't exist.
scala-cli run . --main-class AppThe API server starts on http://localhost:8080.
cd frontend
pnpm install
pnpm devThe dev server starts on http://localhost:5173 and proxies /api requests to the backend.
Navigate to http://localhost:5173.
Build a self-contained image (builds the frontend and packages the backend as an assembly jar, served together on port 8080):
just docker-build # or: docker build -t personal-app .Run it with your data repository mounted at /data:
just docker-run
# or manually:
docker run --rm -p 8080:8080 -v /path/to/your/data-repo:/data personal-appEvery data change is committed to git, so DB_BASE_PATH must live inside a git repository. The container initializes one at /data on first run if none is found. If your data directory is a subdirectory of the repo (e.g. repo/db), mount the repo root and point the app at the subdirectory with -e DB_BASE_PATH=/data/db — just docker-run figures this out automatically from $DB_BASE_PATH in .env.
To keep the app running automatically whenever Docker is running (including after a reboot, once Docker Desktop relaunches):
just redeployThis rebuilds the image, removes any previous instance, and starts a detached container named personal-app with --restart unless-stopped. The container stays up across Docker/host restarts and only stops if you stop it explicitly. Re-run just redeploy whenever you want the latest build live — a running container keeps using the image it was created with, so it won't pick up code changes on its own.
just docker-logs # tail logs
just docker-stop # stop and remove the serviceAll endpoints are documented in openapi.yaml. To regenerate it after changing backend endpoints:
scala-cli run . --main-class generateOpenApiOr via just:
just generate-openapiTo regenerate the frontend TypeScript types from the spec:
cd frontend
pnpm generate-apiAll API endpoints are prefixed with /api, so the backend can also serve the frontend static files at the root path.
| Method | Path | Description |
|---|---|---|
| GET | /api/albums |
List all owned albums |
| GET | /api/wishlist/albums |
List wishlist albums |
| POST | /api/wishlist/albums |
Add album to wishlist |
| POST | /api/wishlist/albums/order |
Mark album as ordered |
| POST | /api/wishlist/albums/received |
Mark album as received (moves to collection) |
| GET | /api/guitars |
List all guitars |
| GET | /api/guitars/{serialNumber}/events |
Guitar event history |
| POST | /api/guitars/{serialNumber}/commands |
Execute command (e.g. ChangeStrings) |
| GET | /api/amplifiers |
List amplifiers |
| GET | /api/guitar-pedals |
List pedals |
No external database. All data is stored as JSON files under $DB_BASE_PATH:
$DB_BASE_PATH/
├── music-inventory/
│ ├── wishlist.json # Wishlist albums
│ └── albums/
│ ├── a.json # Albums by artists starting with A
│ ├── b.json # Albums by artists starting with B
│ └── ...
└── guitar-gear/
├── guitar/ # One JSON file per guitar (by serial number)
├── guitar-amp/ # One JSON file per amplifier
└── guitar-pedal/ # One JSON file per pedal