Spelljammer Ship Tracker

A phone-friendly web app for tracking Spelljammer ships during D&D sessions. Supports multi-ship CRUD, real-time sync via WebSocket, and a battle rules reference.

See App plan.md for the full product spec.

Tech Stack

  • Backend: Node.js + Fastify + Socket.IO + better-sqlite3 + Zod
  • Frontend: React 19 + Vite + react-router-dom + zustand + socket.io-client
  • Language: TypeScript everywhere
  • Database: SQLite (file-based, zero config)
  • Deployment: Docker + Docker Compose

Prerequisites

  • Node.js >= 18 (tested with v18.20.8)
  • npm >= 8
  • Docker + Docker Compose (for production deployment only)

Development Setup

# Install dependencies for both server and web
cd server && npm install
cd ../web && npm install
cd ..

# Or use the convenience script:
npm install          # installs root deps (concurrently)
npm run install:all  # installs server + web deps

Running in Development

Option A: Two terminals

# Terminal 1 — Backend (port 3000)
cd server && npm run dev

# Terminal 2 — Frontend (port 5173, proxies API to 3000)
cd web && npm run dev

Option B: Single command

npm run dev

Then open http://localhost:5173 in your browser.

The Vite dev server proxies /api and /socket.io requests to the Fastify server, so everything works from a single URL.

Production Deployment (Docker)

# Build and start
docker compose up -d --build

# Open http://localhost:3000

The Docker image:

  • Multi-stage build (web build → server build → production)
  • Serves both API and frontend from a single container on port 3000
  • SQLite database stored in a Docker volume at /data/spelljammer.sqlite

Stopping

docker compose down

Database Backup

# Copy the SQLite file out of the container
docker cp spelljammer-ships:/data/spelljammer.sqlite ./backup-$(date +%F).sqlite

Reverse Proxy (Optional)

If you want HTTPS, put Caddy or nginx in front:

# Example Caddyfile
spelljammer.yourdomain.com {
    reverse_proxy localhost:3000
}

Testing

API Tests

Start the server, then run the test script:

cd server && npm run dev
# In another terminal:
bash server/test-api.sh

This runs through the full CRUD lifecycle: create ship → update stats → add weapons → fire (reduce ammo) → delete.

Real-Time Sync Test

  1. Open the app in two browser tabs
  2. Create a ship in one tab — it should appear in the other when you refresh the list
  3. Open the same ship in both tabs
  4. Change hull points in one — the other should update within ~1 second
  5. Add/edit/delete weapons — verify sync

Mobile Test

  • Use Chrome DevTools (F12 → device toggle) to simulate a phone
  • Or open the URL from your phone on the same network

Project Structure

├── server/                  # Fastify backend
│   ├── src/
│   │   ├── index.ts         # Entry point (Fastify + Socket.IO bootstrap)
│   │   ├── config.ts        # Environment config
│   │   ├── db/              # SQLite connection + migrations
│   │   ├── routes/          # REST API endpoints
│   │   ├── services/        # Database query logic
│   │   ├── socket/          # Socket.IO event handlers
│   │   └── validation/      # Zod schemas
│   └── test-api.sh          # curl-based API test script
├── web/                     # React frontend
│   ├── src/
│   │   ├── App.tsx          # Router setup
│   │   ├── socket.ts        # Socket.IO client
│   │   ├── store/           # Zustand state stores
│   │   ├── pages/           # Route pages
│   │   ├── components/      # UI components
│   │   ├── rules/           # Battle reference content
│   │   └── types/           # TypeScript interfaces
│   └── vite.config.ts       # Dev proxy config
├── Dockerfile               # Multi-stage production build
├── docker-compose.yml       # Single-service compose
└── App plan.md              # Full product specification

API Reference

Ships

Method Endpoint Description
GET /api/ships List all ships
POST /api/ships Create a new ship
GET /api/ships/:id Get ship with weapons
PATCH /api/ships/:id Update ship fields
DELETE /api/ships/:id Delete ship + weapons

Weapons

Method Endpoint Description
POST /api/ships/:id/weapons Add weapon to ship
PATCH /api/weapons/:weaponId Update weapon
DELETE /api/weapons/:weaponId Delete weapon

WebSocket Events

Direction Event Purpose
Client → Server ship:join Join a ship's room
Client → Server ship:leave Leave a ship's room
Client → Server ship:update Update ship stats
Client → Server weapon:create Add a weapon
Client → Server weapon:update Update a weapon
Client → Server weapon:delete Delete a weapon
Server → Client ship:state Full ship+weapons state (on join)
Server → Client ship:patched Ship field(s) updated
Server → Client weapon:created New weapon added
Server → Client weapon:patched Weapon field(s) updated
Server → Client weapon:deleted Weapon removed

Troubleshooting

Server won't start: Check that port 3000 is free (lsof -i :3000 or netstat -an | grep 3000).

WebSocket not connecting: In development, make sure Vite proxy is configured (check web/vite.config.ts). The proxy forwards /socket.io to port 3000.

Database locked: If the server crashes without clean shutdown, the SQLite WAL/journal files may remain. Delete data/spelljammer.sqlite-wal and data/spelljammer.sqlite-shm, then restart.

Docker build fails on better-sqlite3: The node:18-alpine image includes prebuilt binaries. If issues persist, switch to node:18-slim in the Dockerfile.

Description
No description provided
Readme 164 KiB
Languages
TypeScript 88.4%
CSS 8.9%
Shell 1.7%
Dockerfile 0.8%
HTML 0.2%