Skip to content

Dim145/FigureCollector

Repository files navigation

FigureCollector

Catalogue your shelf β€” figure by figure.

FigureCollector is a self-hosted PWA for figurine collectors: catalogue every piece you own (or want), log purchase prices + stores, track pre-orders with deposits and slipped release dates, get notified when a parcel is overdue, and discover figures across series β€” all behind a hardened Rust backend running in a FROM scratch container.

It works offline-first, is installable on iOS / Android / Desktop, and pairs with MangaCollector (same author, same architecture).

πŸ“š Full documentation: https://dim145.github.io/FigureCollector/


Highlights

  • πŸ“¦ Catalogue + collection β€” every figure you own or want, with manufacturer / series / character / sculptor / scale / NSFW metadata.
  • πŸ“· Barcode scan β€” point your camera at a JAN / EAN / UPC to open a catalogued figure or jump to adding it (native BarcodeDetector, manual fallback β€” no extra dependency).
  • β˜‘οΈ Bulk collection edit β€” multi-select pieces to re-shelve, set condition, archive, or delete in one pass.
  • 🧾 Proof-of-purchase β€” attach receipts / invoices / customs slips (PDF / JPG / PNG / WebP) to a piece, stored as-is behind an owner-only proxy.
  • πŸ—„οΈ Vitrines β€” arrange your collection into glass display cabinets with drag-and-drop, plus a "where is…?" search.
  • πŸ’΄ La Cote, auto-priced β€” what your collection is worth vs what you paid: an admin-scheduled sweep prices owned figures from the market (orzgk + proxy boutiques), historizes every change, and charts the evolution; your manual valuations always win.
  • πŸ’± One display currency β€” buy in Β₯, € and $, read everything in your currency (ECB rates, on by default, originals on hover). Costs keep their purchase-time exchange rate, so the plus-value never drifts with the market.
  • ⭐ Wishlist with price alerts β€” target prices that notify you when the market dips below them, an ownedβ‰ wishlist rule with at-a-glance catalogue markers, bulk import (public orzgk wishlist Β· proxy-handled boutiques Β· MFC CSV), and a shareable gift list (public link, anonymous reservations hidden from you).
  • πŸ›’ Pre-orders with deposit tracking β€” record the upfront acompte (OrzGK / AmiAmi style), see the balance left to pay, and get notified when delivery is overdue.
  • βœ‚οΈ Cancellations with refund accounting β€” cancelled preorder + partial refund? The piece is auto-archived, the loss surfaces in the yearly recap.
  • πŸ“Έ Photo gallery β€” multi-upload, edit in place (crop / filters / background removal), per-user covers, NSFW blurring, 360Β° turntable scans, fullscreen lightbox with pinch-zoom.
  • πŸ‘₯ Collectors β€” an opt-in public profile; follow other collectors, discover by collection size, and compare collections.
  • πŸ“Š Insights & year-in-review β€” spend over time, series completion, wishlist cost, next-milestone palier, and an annual recap with losses on cancellations.
  • πŸ† Achievements β€” milestone seals (印) the user collects as their collection grows.
  • πŸ’Ύ Data export β€” your collection / wishlist / pre-orders as CSV or JSON, plus a one-file backup.
  • πŸ”” Notifications β€” in-app + email + ntfy + webhook + Apprise + Web Push, with per-channel routing per event (release J-day, J-7, delivery today, delivery overdue, price below target, achievement unlocked, …).
  • πŸ› οΈ Operator-friendly β€” live admin settings (3D-creation policy, price-sweep cron), every scheduled job run historized with a manual re-trigger, worker fleet status.
  • πŸ”’ Hardened from the kernel up β€” FROM scratch backend, distroless nginx, read-only filesystems, dropped capabilities, no shell, no OpenSSL anywhere.

Stack

Backend (server/)

  • Rust 2024 (rustc β‰₯ 1.95), Axum 0.8, Tokio
  • SeaORM + PostgreSQL 16
  • Rustls with aws-lc-rs β€” zero OpenSSL anywhere in the dependency tree
  • Static musl binary shipped in FROM scratch
  • OpenID Connect (Google or generic IdP) plus local username/password (Argon2id)

Frontend (client/)

  • React 19 + Vite 8 + Tailwind v4
  • TanStack Query (offline-first) + Dexie (IndexedDB)
  • PWA via vite-plugin-pwa + Workbox (with NetworkFirst on catalog reads so mutations show up on the next navigation)
  • Distroless nginx runtime (Chainguard) β€” no shell, no package manager, non-root user
  • Doubles as the reverse proxy: this nginx serves the static PWA and proxies /api/* + /api/ws to the Rust backend. Only this container exposes a host port; the server container is internal-only. Single-port ingress = single attack surface.

Storage

  • Postgres 16 for the relational graph
  • Garage (Deuxfleurs) β€” S3-compatible distributed object store, lighter than MinIO and designed for federated self-hosting. Filesystem fallback when S3 isn't configured.

Security contract

Layer Backend Frontend Docs
Base image FROM scratch cgr.dev/chainguard/nginx cgr.dev/chainguard/nginx
User 65532:65532 65532 65532
Filesystem read_only: true + tmpfs /tmp 16M (noexec,nosuid,nodev) read_only: true + tmpfs /tmp, /tmp/nginx, /var/cache/nginx same as frontend
Capabilities cap_drop: ALL cap_drop: ALL cap_drop: ALL
Privilege escalation no-new-privileges:true no-new-privileges:true no-new-privileges:true
Healthcheck --health subcommand (no curl/wget) upstream nginx upstream nginx

Other hardening:

  • TLS: Rustls + aws-lc-rs end-to-end on the backend.
  • HTTP security headers: strict CSP, COOP, CORP, Referrer-Policy, Permissions-Policy on the frontend (client/nginx.conf).
  • Image uploads: magic-bytes mimetype validation, EXIF strip, size and dimension caps.
  • External scraping: orzgk fetched server-side with an aggressive PG cache (24 h TTL) + identifiable User-Agent; MFC parsed from pasted page HTML (it blocks direct fetch behind Cloudflare).
  • Session-fixation defense: session token rotation on login.
  • Rate limiting: tower_governor on auth-sensitive routes.

Repo layout

FigureCollector/
β”œβ”€β”€ server/                # Rust backend (Cargo crate, scratch container)
β”‚   β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ migrations/        # SeaORM SQL migrations
β”‚   β”œβ”€β”€ Cargo.toml
β”‚   └── Dockerfile
β”œβ”€β”€ client/                # React + Vite PWA
β”‚   β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ public/
β”‚   β”œβ”€β”€ package.json
β”‚   β”œβ”€β”€ vite.config.js
β”‚   β”œβ”€β”€ nginx.conf
β”‚   └── Dockerfile
β”œβ”€β”€ docs/                  # All documentation under one roof
β”‚   β”œβ”€β”€ content/           # MkDocs Material source (published to GH Pages)
β”‚   β”œβ”€β”€ mkdocs.yml
β”‚   β”œβ”€β”€ nginx.conf
β”‚   β”œβ”€β”€ Dockerfile
β”‚   └── design/             # per-feature visual-direction maquettes (HTML)
β”œβ”€β”€ .github/workflows/
β”‚   β”œβ”€β”€ release.yml        # GHCR image push on tag
β”‚   └── docs.yml           # MkDocs β†’ GitHub Pages
β”œβ”€β”€ docker-compose.yml         # local development stack
β”œβ”€β”€ docker-compose.prod.yml    # production stack (Traefik-fronted)
β”œβ”€β”€ docker-compose.docs.yml    # optional: self-host the docs
└── README.md

Quick start

Local dev (hot reload)

Prerequisites: Docker (with BuildKit), Rust β‰₯ 1.95 (via rustup), Node 24 with corepack/pnpm.

# Spin up PostgreSQL (+ Garage) for local dev
docker compose up -d postgres garage

# Backend (terminal A)
cd server
cp .env.example .env
cargo run

# Frontend (terminal B)
cd client
corepack enable
pnpm install
pnpm dev

Open http://localhost:5173. The dev server proxies /api/* and /api/ws to the backend on :3000.

Fully containerised dev

docker compose up --build

This builds both images (backend FROM scratch, frontend distroless) and runs them with the full hardening profile. Slower iteration but matches production exactly.

Production deployment

# Pre-flight: create the Traefik edge network if it does not exist
docker network create traefik_edge

# Configure secrets
cp .env.example .env.prod        # then edit

# Bring up the stack
docker compose -f docker-compose.prod.yml --env-file .env.prod up -d

Required env vars (no defaults): POSTGRES_PASSWORD, FRONTEND_URL, WEB_DOMAIN, S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY. Everything else (OIDC providers, rate limiting, parcel tracking…) is optional β€” see the environment-variables reference.

Self-host the docs

docker compose -f docker-compose.docs.yml up -d

Then open http://localhost:8000. The container builds the MkDocs site at image build time, then serves it from a read-only nginx with the same hardening profile as the main frontend.


Documentation

Detailed install / configuration / feature / API docs live at https://dim145.github.io/FigureCollector/.

Local browsing:

cd docs
pip install -r requirements.txt
mkdocs serve
# β†’ open http://localhost:8000

License

MIT β€” Β© 2026 Dimitri Dubois.