A private, two-person (Yusuf & Sumayyah) purchase-planning app for big-ticket items. Wishlist items each have a budget cap; candidate listings are logged against them with price history, pros/cons, and per-person ratings/priority votes.
This repo is the real, persistent implementation (Node/Express + SQLite API,
React/Vite frontend) built from the original Big Buys.dc.html design
prototype, packaged as a single Docker image for a self-hosted NAS.
- Backend: Node.js + Express, SQLite via
better-sqlite3(single file DB, no separate database container). - Frontend: React + Vite, built to static assets and served by the same Express server — one container, one port.
- Auth: no real accounts. A client-side Yusuf/Sumayyah profile picker
(stored in
localStorage) decides whose vote is being set. Optionally gate the whole app behind a shared passphrase (see below) since it's a private tool that may end up reachable on the open internet. - Sync: plain REST, refetch-on-load / refetch-after-mutation. No websockets — fine for two people making occasional edits.
server/ Express API + static file server
src/
index.js app entry point, mounts routes, serves client build
db.js SQLite schema (items, candidates, price_history, votes, notes)
serialize.js turns DB rows into the JSON shape the frontend expects
routes/
items.js /api/items, /api/items/:id, notes, votes
candidates.js /api/items/:id/candidates, /api/candidates/:id, purchase
public/ built frontend assets (generated, not committed)
client/ React + Vite source
src/
screens/ Wishlist, Item Detail, Compare, Votes, Add
styles/ Nocturne design tokens (tokens.css) + app layout (app.css)
Dockerfile multi-stage build: client build -> server deps -> runtime
docker-compose.yml
GET /api/items— all items with nested candidates, notes, votesPOST /api/items— create item{ name, iconKey, budgetCap, photoUrl?, notes? }PATCH /api/items/:id— update fields / status (active|later|purchased)DELETE /api/items/:idPOST /api/items/:id/notes—{ text }PUT /api/items/:id/votes/:profile—{ tier: 1|2|3 }, profile isyusuforsumayyahPOST /api/items/:id/candidates—{ name, price, link?, photoUrl?, pros?, cons?, ratings? }PATCH /api/candidates/:id— editing price appends a price-history point automaticallyDELETE /api/candidates/:idPOST /api/candidates/:id/purchase— marks the parent itempurchasedwith this candidate's price/today's dateGET /api/health
All derived values (best price, budget status, priority rank, sparkline points) are computed client-side from the raw data, same as the design prototype — nothing is stored redundantly.
# terminal 1 — API on :3000
cd server && npm install && npm run dev
# terminal 2 — Vite dev server on :5173, proxies /api to :3000
cd client && npm install && npm run devOpen http://localhost:5173.
From the repo root, on the NAS (or anywhere with Docker):
docker compose up -d --buildThis builds the frontend, installs the server's native SQLite binding, and
starts the container listening on port 3000, with the SQLite file persisted
to ./data/bigbuys.db on the host (bind-mounted to /data in the
container) — so the data survives container rebuilds/updates.
To update after pulling new code:
docker compose up -d --buildContainer state (the DB) is untouched by rebuilds since it lives in the
mounted ./data folder, not inside the image.
| Variable | Default | Purpose |
|---|---|---|
PORT |
3000 |
Port the server listens on |
HOST |
0.0.0.0 |
Bind address |
DATA_DIR |
/data (in container) |
Where bigbuys.db is stored |
APP_PASSPHRASE |
unset | If set, gates the entire app (API + static files) behind HTTP Basic auth — any username, this password. Recommended if you're not putting the app behind Cloudflare Access or similar, since it has no real accounts. |
- Install Docker (via UGOS Pro's app store, or Portainer/Container Manager if you prefer a UI) on the NAS.
- Copy this repo onto the NAS (e.g.
git cloneover SSH, or use Container Manager's "build from repo" flow). - From the project folder:
docker compose up -d --build. - Confirm it's up:
curl http://<nas-ip>:3000/api/healthshould return{"ok":true}. - Expose it at
bigbuys.yusufav.ukusing your own reverse proxy — Cloudflare Tunnel (cloudflared) pointed athttp://localhost:3000on the NAS is the simplest option, or port-forward + your router's TLS/cert handling. This app only needs to listen on one port; it has no opinion on how you expose it. - If the tunnel/port-forward makes this reachable from the open internet,
set
APP_PASSPHRASEindocker-compose.ymland restart — otherwise anyone with the URL can read and edit the wishlist.
The SQLite file at ./data/bigbuys.db (next to docker-compose.yml) is
the only copy of your data. A simple nightly cron job on the NAS is enough:
# crontab -e
0 3 * * * cp /volume1/docker/bigbuys/data/bigbuys.db /volume1/docker/bigbuys/backups/bigbuys-$(date +\%Y\%m\%d).dbPrune old backups periodically (e.g. keep the last 30).
client/src/styles/tokens.css is the Nocturne design-token sheet ported
verbatim from the original prototype's styles.css — colors, spacing,
radii, and component classes (.btn, .tag, .seg, .dialog, …) are
unchanged. app.css adds the layout-specific classes for the five screens
(wishlist list/detail, compare, votes, add) that were inline styles in the
.dc.html prototype.
One deliberate deviation from the prototype: the "Add" screen's simulated link-fetch (canned demo copy filled in after a fake 900ms delay) was prototype-only flavor with no real backend behind it. The real app instead asks for Title/Price directly, with the link field kept as an optional attached URL — actual link scraping was out of scope for this build.