Next.js application for managing an NBA card collection: collection browsing, player pages, reference administration, and SQLite persistence.
- Node.js 24 (see
.nvmrc) - npm
cp .env.example .env.local
# Set AUTH_SECRET in .env.local (openssl rand -hex 32)
npm ci
npm run devThe application is available at http://localhost:3000.
On first launch, create the administrator account from the login screen (bootstrap).
French (default) and English (US). The selected language is stored in the hh_locale cookie and can be changed from the sidebar or the login screen.
Translation files: messages/fr.json, messages/en.json.
npm run ci # fast local checks (recommended before pushing)
npm run ci:full # lint + typecheck + npm audit high+ (used by GitHub Actions)
npm run check # alias for npm run ci
npm run lint
npm run typecheck
npm run build
npm start
npm run clean # removes .next, TypeScript caches, etc.Everything is stored in data/hobbyhoops.db (SQLite via better-sqlite3): collection, references, accounts, sessions, and rate limiting. This file and its WAL journals are local and must never be committed.
On first launch, an empty database is created; create the administrator account from the login screen.
The SQLite file is the single source of truth. The database runs in WAL mode (hobbyhoops.db plus optional -wal / -shm sidecar files). Back it up on the host (the production container has no sqlite3 CLI). Keep a local script or cron on the server — it is not versioned in this repository.
Backup — use SQLite’s online backup API (consistent snapshot, safe while the app is running). Do not copy hobbyhoops.db with cp while the application is open; that can produce an incomplete file.
# Prerequisite: sqlite3 (apt install sqlite3)
mkdir -p data/backups
sqlite3 data/hobbyhoops.db ".backup 'data/backups/hobbyhoops-$(date +%Y-%m-%dT%H-%M-%S).db'"
# Optional: delete backups older than 14 days
find data/backups -maxdepth 1 -name 'hobbyhoops-*.db' -mtime +14 -deleteDaily cron example:
0 3 * * * cd /path/to/hobbyhoops && mkdir -p data/backups && sqlite3 data/hobbyhoops.db ".backup 'data/backups/hobbyhoops-$(date +\%Y-\%m-\%dT\%H-\%M-\%S).db'" && find data/backups -maxdepth 1 -name 'hobbyhoops-*.db' -mtime +14 -delete >> /var/log/hobbyhoops-backup.log 2>&1Restore — stop the application first, replace the database file, then delete stale WAL journals. Skipping the last step (or restoring while the app is still running) can corrupt the database (SQLITE_CORRUPT / database disk image is malformed).
Production:
docker compose stop app
cp data/backups/hobbyhoops-YYYY-MM-DDTHH-MM-SS.db data/hobbyhoops.db
rm -f data/hobbyhoops.db-wal data/hobbyhoops.db-shm
docker compose start appLocal development (npm run dev):
# Stop the dev server (Ctrl+C), then:
cp data/backups/hobbyhoops-YYYY-MM-DDTHH-MM-SS.db data/hobbyhoops.db
rm -f data/hobbyhoops.db-wal data/hobbyhoops.db-shm
npm run devCorruption recovery — if the database is already corrupted after a bad restore, stop the app and run:
node scripts/recover-sqlite.mjsThis copies readable tables into a fresh file, rebuilds references_state from cards, and recreates the FTS index. The broken file is kept as data/hobbyhoops.db.corrupt-<timestamp>.
Backups are stored in data/backups/ (gitignored). See SECURITY.md.
Export or import cards from Admin → CSV import / export.
- Export — filtered view or full collection (UTF-8 CSV, comma-separated).
- Import — create-only or upsert by
id(max 5 000 rows per file).
Full format specification, column aliases (FR/EN), and Excel/Sheets workflow: docs/csv-import-export.md.
This complements the SQLite backup (hobbyhoops.db): CSV is ideal for spreadsheets and portability; the database file remains the complete instance backup.
The image starts with an empty collection: only a writable data/ directory is required (the SQLite database is created there automatically). The container runs as the hobbyhoops system user (UID/GID 1111). Mount data/ at /app/data and make it writable by that user.
The application listens on 127.0.0.1:3000 (not exposed on all interfaces). In production, place a reverse proxy (e.g. Apache) in front of the host and proxy to that address.
Two Compose files:
| File | Usage |
|---|---|
docker-compose.yml |
Production — pinned image from GHCR, hardened runtime |
docker-compose.dev.yml |
Local Docker — build from the Dockerfile |
After each semantic-release run on main, images are published to GitHub Container Registry:
ghcr.io/mcinquin/hobbyhoops:latestghcr.io/mcinquin/hobbyhoops:X.Y.Z(e.g.1.33.0)
Prefer a version tag in production, not latest.
cp .env.example .env
# Required: AUTH_SECRET (openssl rand -hex 32)
mkdir -p data && sudo chown -R 1111:1111 data
export HOBBYHOOPS_VERSION=1.33.0 # tag matching your GHCR release
docker compose pull
docker compose up -ddocker-compose.yml expects a .env file (required: true). AUTH_SECRET must be set or Compose will fail at startup. Defaults suited to an HTTPS reverse proxy that rewrites forwarding headers:
TRUST_PROXY=true(setfalsein.envif the proxy does not setX-Forwarded-For/X-Forwarded-Protocorrectly)COOKIE_SECURE=true
Override via .env if needed. See SECURITY.md.
Production-oriented options in Compose include: non-root user, read_only root filesystem, dropped capabilities, health check on /api/health, and log rotation. No CPU/RAM cap by default (a low mem_limit / cpus makes Next.js feel very slow). The reverse proxy stays outside Compose (Apache on the host).
To build and run the image from the repository (development or testing the Dockerfile):
cp .env.example .env
mkdir -p data && sudo chown -R 1111:1111 data
docker compose -f docker-compose.dev.yml up --build -dAUTH_SECRET is optional here (a dev-only default is provided); still set a real value to match production behaviour.
The .github/workflows/ci.yml workflow runs on every push and pull request to main or master:
npm ci, thennpm run ci:full(Node, ESLint, TypeScript, npm audit high+)- build and push the test Docker image on PRs
- on push to
mainonly: semantic-release (Git tagvX.Y.Z, GitHub Release,CHANGELOG.md,package.jsonbump, deploy examples inREADME.md/docker-compose.yml), then publish the versioned Docker image from thereleasejob
Locally, run npm run ci before pushing. The full network audit remains available with npm run ci:full.
Config: release.config.cjs. Releases are created automatically after a successful CI run on main, based on Conventional Commits (feat:, fix:, refactor:, etc.).
Deploy examples (HOBBYHOOPS_VERSION in README.md and docker-compose.yml) are bumped in the same release commit as package.json via scripts/sync-release-examples.mjs.
- Open a PR and squash-merge with a conventional title (e.g.
feat: add collection filter). Direct pushes tomainare blocked by branch protection. - Merge commits such as
Merge pull request #12do not trigger a version by themselves. - GitHub App
hobbyhoops-releasepushes release commits and tags (bypass ruleset). Repository secrets:RELEASE_APP_ID,RELEASE_APP_PRIVATE_KEY. - Settings → Actions → General → Workflow permissions → Read and write permissions (Docker GHCR still uses
GITHUB_TOKEN). - Docker images
:latestand:X.Y.Zpoint to the commit of thevX.Y.ZGit tag created by semantic-release (not the intermediate push SHA).
Check that no secret or local data will be versioned:
git status
git check-ignore -v data/hobbyhoops.db .env.localThen commit, push a branch, and open a PR toward main:
npm run ci
git checkout -b feat/my-change
git add .
git commit -m "feat: application HobbyHoops"
git push -u origin feat/my-changeMerge the PR on GitHub (squash-merge with a conventional title). semantic-release runs on the resulting push to main.
Never push .env, .env.local, local accounts, or the development SQLite database. See SECURITY.md.
npm run ci: local checks without network dependency (Node, lint, typecheck, tests)npm run test: Vitest unit tests (auth, CSRF, collection query, card CRUD)npm run ci:full: GitHub Actions checks with npm audit high+- Husky:
pre-commit(ESLint on staged files),pre-push(npm run ci),commit-msg(commitlint conventional) - Dependabot: weekly npm and GitHub Actions updates