Skip to content

Latest commit

 

History

166 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Model Workshop Manager

Track your model kit stash, paint shelf, and builds — self-hosted, on your own hardware.

No more guessing whether you already own that Revell kit, digging through drawers to check if you have the right shade of Tamiya paint, or losing track of build progress. Model Workshop Manager keeps your catalog, inventory, and builds in one place, installable as an app on your phone or desktop, running entirely on your own server.

Dashboard on mobile — stash stats and the active build A completed build with its photo and progress bar The model catalog, browsable on the go

What it does

  • Dashboard — kit/paint/build counts, low-stock paints, missing-paint alerts, recent activity.
  • Models & Paints — a searchable catalog, kept separate from what you actually own.
  • Builds — start a build in one guided flow, log progress with timestamps and photos, and get automatic "required vs. owned" paint matching per project.
  • Shopping list & Wishlist — one tap turns a build's missing paints into a shopping list; longer-term wants live separately in the wishlist.
  • Supplies — track tools and consumables (brushes, cement, masking tape, etc.) too.
  • Import & export — bring in an existing collection via CSV/JSON, back it up from the UI.
  • Login-protected — single-user auth, nothing exposed without a password.

PWA/offline installability is temporarily unavailable after a recent migration (see Tech stack) — tracked as follow-up work.

Quick start (Docker)

Two ways to run it: pull the published image (no clone needed — this is the one to use for a Portainer stack), or build from source.

Option A: pull the published image

Multi-arch (linux/amd64 + linux/arm64 — Raspberry Pi and ARM NAS boxes included):

  • GHCR: ghcr.io/lucifix/model-workshop-manager
  • Docker Hub: docker.io/lucifix/model-workshop-manager

Paste this straight into Portainer (Stacks → Add stack → Web editor) or save it as docker-compose.yml anywhere — nothing needs to be filled in:

services:
  app:
    image: ghcr.io/lucifix/model-workshop-manager:latest
    container_name: model-workshop-manager
    restart: unless-stopped
    ports:
      - 8080:3000
    environment:
      # Set true once you're serving this over HTTPS, or login will silently fail.
      SESSION_COOKIE_SECURE: "false"
      # Number of reverse proxies in front of the app, if any. Omit if none.
      # TRUST_PROXY: 1
    volumes:
      - workshop-data:/data
      - workshop-backups:/backups

volumes:
  workshop-data:
  workshop-backups:

Everything else (database path, uploads, port inside the container) is baked into the image. For the full file — including the optional nightly-backup sidecar and bind-mount options — use docker-compose.yml from this repo:

mkdir model-workshop-manager && cd model-workshop-manager
curl -O https://raw.githubusercontent.com/Lucifix/model-workshop-manager/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/Lucifix/model-workshop-manager/main/.env.example
cp .env.example .env          # optional tweaks — nothing required
docker compose pull
docker compose up -d

Updating: docker compose pull && docker compose up -d

Option B: build from source

git clone <this-repo-url> && cd model-workshop-manager
cp .env.example .env          # optional tweaks — nothing required
docker compose up -d --build
docker compose exec app npm run db:seed   # optional: sample data (migrations run automatically on boot)

Updating: git pull && docker compose up -d --build

Both options open http://localhost:8080 (or whatever HOST_PORT you set) — first visit shows a one-time setup screen to create your username/password, then you're logged in.

Stopping: docker compose down (your data lives in the workshop-data volume, untouched)

Before you expose this anywhere: it's a single-user app meant for your LAN or a VPN (Tailscale/WireGuard), not the open internet. See Security below.

Environment variables

All set in .env (copied from .env.example), read by docker-compose.yml.

Variable Required Default What it does
SESSION_SECRET No (generated) Signs the session cookie. Unset, a random key is generated on first boot into the data volume (/data/session-secret). Set it only to pin your own key.
SESSION_COOKIE_SECURE No false Set true once served over HTTPS — otherwise the browser won't send the cookie and login silently fails.
TRUST_PROXY No (off) Set to the number of reverse proxies in front of the app (usually 1) so the login rate limiter sees real client IPs. true is refused — it's spoofable.
HOST_PORT No 8080 Host port the app is served on.
APP_PORT No 3000 Port the app listens on inside the container. Rarely needs changing.
APP_IMAGE_TAG No latest Which published image tag to run (latest, or a pinned version like 1.2.3). Ignored when building from source.
APP_UID / APP_GID No 1000 uid:gid the container runs as. Set these to match the owner of WORKSHOP_DATA_DIR/BACKUP_DIR if bind-mounting.
WORKSHOP_DATA_DIR No (named volume) Host path for the database + uploaded photos, instead of the workshop-data Docker volume — e.g. a NAS mount.
BACKUP_DIR No ./backups Host path where backups (in-app, manual script, and the nightly job) are written.
BACKUP_RETENTION_DAYS No 14 How many days of nightly backups to keep before pruning.

Tech stack

A single React Router v8 (framework mode) app → Express → Drizzle ORM → SQLite, one Node process behind Docker Compose. React + TypeScript UI and API resource routes live in the same app; photos live on disk, never as DB blobs.

Local development

Requires Node.js 22+ (see .nvmrc — better-sqlite3 needs a matching native build; older Node versions fail silently instead of erroring).

npm install
npm run db:migrate && npm run db:seed   # first time only
npm run dev                              # :3000 — one process, UI + API

Open http://localhost:3000. Run tests with npm test.

Security

This is a single-user private app and it requires login — but login raises the bar, it isn't a substitute for not exposing an unaudited personal app to the open internet. Keep it behind a reverse proxy, LAN-only or on a VPN.

Implementation details
  • The whole API is protected by default (allow-list of exactly four public paths: /api/health, /api/auth/login, /api/auth/me, /api/auth/setup) — new routes are guarded automatically.
  • Credentials are one username/password row in the database (no user table — deliberately single-user), hashed with scrypt. First visit to a fresh install shows a one-time setup screen to create them; AUTH_USERNAME/AUTH_PASSWORD in .env are an optional way to seed that same row on first boot instead (e.g. bringing an existing deployment forward) and are never read again afterwards. Change your password any time from Settings. Forgot it? There's no email flow — run docker compose exec app npm run auth:reset and you'll get the setup screen again (or, if AUTH_USERNAME/AUTH_PASSWORD are still set in .env, the next restart quietly recreates that same login instead — remove them from .env first if you want a blank setup screen).
  • Sessions are a signed, httpOnly cookie (React Router's cookie session storage) — no server-side session store to run or lose. The 30-day expiry is stored inside the signed payload and checked on every request, not left to the browser's cookie lifetime.
  • With no session store there is nothing to delete, so logging out can only clear the browser's copy of the cookie. Changing your password (or running auth:reset) invalidates every other existing session immediately, the same way — no reason to also rotate the signing key for that. Deleting /data/session-secret (or changing SESSION_SECRET, if you set one) and restarting remains the blunter, whole-app option.
  • The login endpoint is rate-limited (5 attempts/minute). Behind a reverse proxy, set TRUST_PROXY (see the table above) or that budget is shared across all clients rather than counted per client.
  • The cookie-signing key is 32 random bytes generated on first boot and stored with 0600 permissions at /data/session-secret, outside the database — so a backup restore doesn't sign you out. SESSION_SECRET overrides it if set.
  • Set SESSION_COOKIE_SECURE=true once this is served over HTTPS, otherwise the browser won't send the cookie and login will silently fail.
  • File uploads are size-capped (25 MB/file) but not otherwise scanned.

Backup & restore

Three ways to get a backup, easiest first: the Backups tab in the app (create, list, download, restore, delete), an automated nightly snapshot (a docker-volume-backup sidecar container, opt-in — see below), or ./scripts/backup.sh for a manual one from the shell. All three produce the same tarball format, so a backup from any one can be restored via any other.

Details
  • In-app Backups tab (Import & Export page) — the easiest path day to day. Uploaded archives are capped at 2 GiB, and refused if they expand past 4 GiB on extraction.
  • Automated nightly backup — opt-in: docker compose --profile backup up -d. The backup service in docker-compose.yml (offen/docker-volume-backup) snapshots the workshop-data volume to BACKUP_DIR every night at 03:00, pruning by BACKUP_RETENTION_DAYS. Point BACKUP_DIR at a real host path already covered by whatever backs up your other apps. It's opt-in because it needs access to the Docker socket to pause the app during the snapshot, which is root-equivalent on the host — the in-app Backups tab above doesn't need it.
  • Manual script — ./scripts/backup.sh writes a timestamped .tar.gz of the database and uploaded photos to $BACKUP_DIR. Restore steps are in the comments at the top of the script: stop the stack, extract the tarball's database/ and uploads/ into the workshop-data volume, restart.

Contributing

Bug reports, feature requests, and PRs are welcome — see CONTRIBUTING.md for dev setup and conventions. Please also read the Code of Conduct. Found a security issue? See SECURITY.md instead of opening a public issue.

License

MIT

About

Track your model kit stash, paint shelf, and builds — self-hosted, on your own hardware.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

6 stars

Watchers

1 watching

Forks

Packages

Used by

Contributors

Languages