Plan your merch purchases for anime conventions and travel destinations. Upload catalog pages, draw rectangles on items, mark them off as you buy them, compare prices across shops, and track per-booth spending — all in a self-hosted Nuxt 4 + SQLite app.
- Convention mode: Halls → Booths → Catalogs/Articles/Receipts → Products
- Travel mode: Countries / Cities → Shops with date ranges
- Interactive hall plans: Upload a floor plan image, click to drop booth markers, drag to reposition
- OCR booth-number detection: Tesseract.js reads booth numbers off your floor plan image and pins them automatically
- Catalog annotations: Drag a rectangle over an item on a scanned catalog page → it becomes a product with size and price variants
- Article galleries: Multiple price sources per item (shop name, price, link) with "planned" / "paid" tracking
- Receipt mode: Tick off purchases against a checklist while standing at the booth
- Cost tracking: Per-booth, per-event, per-currency totals
- User accounts: Admin / editor / regular-user roles, scrypt-hashed passwords, httpOnly session cookies
- Public / private events: Mark an event public for anyone-can-view, or keep it private to you and people you invite
- Per-event sharing: Invite individual users or whole groups to view or edit specific events
- Magic-link invites: Generate a shareable URL — the recipient signs up (or signs in) and is auto-added to the event with the level you chose
- Groups: Create named groups, add members, share events with the whole group at once
- Per-product privacy: Products you add to a shared event are visible only to you + the event owner + edit-share collaborators — other view-share users don't see your "wishlist" items
- Admin user management: Admins can create users, change roles, reset passwords from
/admin/users - Guest view: Browse public events without an account — personal info, paid status, and spent totals are hidden
- i18n: English + German throughout
git clone https://github.com/<you>/merch-tracker.git
cd merch-trackercp .env.example .envEdit .env and set:
ADMIN_DEFAULT_USERNAME=admin
ADMIN_DEFAULT_PASSWORD=your-strong-password-hereIf you skip this step, a random password is generated on first boot and printed to the container logs once. Read it from
docker compose logs merch-tracker.
docker compose up -d --buildThe app is now running on http://localhost:3000.
Open http://localhost:3000/login and use the credentials from your .env (or the random one from the logs). Change it in Account → Change password afterwards.
The compose file creates two bind-mounted directories in your project root:
./data/ — SQLite database (merch-tracker.db + WAL files)
./uploads/ — Uploaded catalog and floor-plan images
Back these up to preserve your data. Everything else is ephemeral.
All settings are environment variables. The compose file already wires env_file: .env so anything in .env is passed straight into the container.
| Variable | Purpose | Default |
|---|---|---|
ADMIN_DEFAULT_USERNAME |
First-run admin username | admin |
ADMIN_DEFAULT_PASSWORD |
First-run admin password (only used if the users table is empty) |
random, printed once |
UPLOAD_DIR |
Where uploaded images are written | /app/uploads (set by compose) |
HOST |
Listen address | 0.0.0.0 |
PORT |
Listen port | 3000 |
ADMIN_DEFAULT_* are only consulted on the very first run when no users exist. After that, change passwords through the Account page or by editing the DB directly.
git pull
docker compose up -d --buildThe DB schema auto-migrates additive columns on startup (idempotent ALTER TABLE).
docker compose logs -f merch-trackerdocker compose down
rm -rf data uploads
docker compose up -d --buildNon-destructive — keeps all your events/booths/products, only rewrites the admin password hash and clears active sessions.
Local:
npm run reset-adminDocker (host has Node installed):
docker compose stop merch-tracker
npm run reset-admin # writes to ./data/merch-tracker.db via the bind mount
docker compose start merch-trackerDocker (host has no Node — run the script inside the container):
docker compose up -d --build # rebuild once to bake scripts/ and better-sqlite3 into the image
docker compose exec merch-tracker \
node /app/scripts/reset-admin.mjs /app/data/merch-tracker.dbThe script reads ADMIN_DEFAULT_USERNAME / ADMIN_DEFAULT_PASSWORD from .env. Change those first if you want a different value.
If you'd rather wipe the database entirely (loses everything):
docker compose down
rm data/merch-tracker.db data/merch-tracker.db-shm data/merch-tracker.db-wal
docker compose up -d --buildThe app listens on plain HTTP. Terminate TLS at the proxy and forward to http://merch-tracker:3000. Sessions use Secure cookies in production (!import.meta.dev), so HTTPS at the edge is required for cookies to stick.
Caddy example:
merch.example.com {
reverse_proxy localhost:3000
}Requires Node 20+.
npm install
cp .env.example .env # optional, set ADMIN_DEFAULT_PASSWORD
npm run dev # http://localhost:3000Scripts:
npm run dev # Nuxt dev server with HMR
npm run build # Production build to .output/
npm run preview # Serve the production build locally
npm run db:studio # Drizzle Studio (DB browser)
- Nuxt 4 with Vue 3 Composition API
- @nuxt/ui for components (Tailwind under the hood)
- Pinia for state
- Drizzle ORM + better-sqlite3
- Tesseract.js for client-side OCR
- Rolled-in-house auth: Node's built-in
scryptfor password hashing, httpOnly cookie sessions. No third-party auth service, no extra dependencies.
app/ Nuxt frontend (pages, layouts, components, stores)
server/ Nitro backend (API routes, DB, auth utilities, file server)
data/ SQLite database (gitignored)
public/ Static assets (uploads also land here in dev)
.claude/ Notes for AI agents working on the codebase
Dockerfile Multi-stage build (Node 20-alpine)
docker-compose.yml One-service compose definition with bind mounts
For deeper details — schema, auth design, file-server architecture, known quirks — see .claude/CLAUDE.md.
| Action | Who can do it |
|---|---|
| View a public event | Anyone, even logged out |
| View a private event | Admin, event owner, any user who has been shared with directly, any member of a group the event is shared with, or anyone holding a valid invite link |
| See another collaborator's products on a shared event | Only the product creator + event owner + admin + edit-share collaborators. View-only shares see the event owner's products plus their own |
| Edit content in an event (add booths/products/upload images, mark items purchased) | Admin, editor role, event owner, any user with an edit-level share |
| Toggle public/private, share/unshare, mint/revoke invite links, delete the event | Admin or event owner only |
| Create a new event | Any logged-in user (they become the owner) |
| Manage users (create/delete/change role/reset password) | Admin only — /admin/users |
| Manage groups | Group owner or admin — /admin/groups |
| Add/remove members of a group | Group owner or admin; members can remove themselves (leave) |
Sessions are httpOnly, SameSite=Lax, 30-day TTL, and Secure in production. Passwords are scrypt-hashed with random per-user salts. No third-party auth service is involved. If a session expires mid-action, the client auto-redirects to /login with a ?redirect= back-link.
MIT