Multiplayer location-based tag game built with Expo (React Native) and a Node.js/Socket.IO server.
- Node.js 18+
- npm
- Android: Android Emulator (AVD) or a physical Android device
- iOS (macOS only): Xcode with the iOS Simulator, plus CocoaPods
- Windows (for included scripts) or manual configuration for macOS/Linux
# Install server dependencies
cd server
npm install
# Install app dependencies
cd ../app
npm installcd server
npm run devStarts on http://localhost:3001.
iOS (macOS only)
cd app
npx expo run:iosBuilds the native project, installs CocoaPods dependencies, and launches the app on the first available iOS Simulator. You can pick a specific simulator with:
npx expo run:ios --device "iPhone 16 Pro"If CocoaPods is missing, install it first:
sudo gem install cocoapods
cd app/ios && pod installAndroid
cd app
npx expo run:androidBuilds and installs the app on the first available emulator. To install on a second emulator, use:
npx expo run:android # installs on emulator 1
adb install -r android/app/build/outputs/apk/debug/app-debug.apkThen open the app manually on the second emulator. Both instances reload from the same Metro dev server.
Set EXPO_PUBLIC_API_URL in app/.env to your computer's LAN IP on port 3001, start the server, then:
# Android
cd app
npx expo run:android --device
# iOS
npx expo run:ios --deviceOn iOS you must select a signing team: open app/ios/VancouverArtThieves.xcworkspace
in Xcode, go to the "VancouverArtThieves" target → Signing & Capabilities, and set your
Apple ID team. The iOS Simulator needs no signing.
server/— Express + Socket.IO backend with selectable memory, SQLite, or PostgreSQL storageapp/— Expo React Native app with Zustand stores, MapLibre maps, and Socket.IO client
All environment variables for the app (app/.env) and the server (server/.env)
are documented in ENV.md — including the storage driver, database connection
settings, the API base URL, and the map style. Templates with every option live in
app/.env.example and server/.env.example.
Game settings (duration, vicinity radius, no-tag grace period, etc.) are configurable by the host when creating a game.
The server selects its storage backend at startup with DB_DRIVER:
# Local persistent development (default)
DB_DRIVER=sqlite
DATABASE_PATH=./data/vat.db
# Azure or another managed PostgreSQL service
DB_DRIVER=postgres
DATABASE_URL=postgresql://user:password@host:5432/art_thieves
# Ephemeral development/testing
DB_DRIVER=memoryPostgreSQL uses the migrations in server/drizzle-postgres/. Existing SQLite data is not copied automatically; export and import it before switching a production deployment. Uploaded photos remain filesystem data and should use durable storage such as Azure Files or Blob Storage in production.
To copy an existing SQLite database into a migrated PostgreSQL database, set both
DATABASE_PATH and DATABASE_URL, then run:
cd server
npm run db:migrate:sqlite-to-postgresThe command copies database records only. Copy the corresponding uploads/ files separately.
A ready-to-run image is published to GitHub Container Registry as
ghcr.io/lo3wer/vancouver-art-thieves-server (multi-arch: linux/amd64 + linux/arm64),
tagged on each v* release plus a rolling latest. No build tools needed on the host.
docker run -d --name art-thieves \
-p 3001:3001 \
-v art-thieves-data:/data \
-v art-thieves-uploads:/app/uploads \
ghcr.io/lo3wer/vancouver-art-thieves-server:latestThe server listens on http://<host>:3001. With the default SQLite driver, it creates the
database, runs migrations, and seeds a default map. The database lives in the
art-thieves-data volume and uploaded photos in art-thieves-uploads — both survive
restarts and upgrades.
| Variable | Default | Purpose |
|---|---|---|
PORT |
3001 |
HTTP/Socket.IO listen port |
DB_DRIVER |
sqlite |
memory, sqlite, or postgres |
PERSIST |
true |
Legacy switch; false selects memory |
DATABASE_PATH |
/data/vat.db |
SQLite database file path |
DATABASE_URL |
unset | PostgreSQL connection URL |
MIGRATIONS_DIR |
./drizzle |
Drizzle migrations folder (in the image) |
POSTGRES_MIGRATIONS_DIR |
./drizzle-postgres |
PostgreSQL migrations folder |
UPLOADS_DIR |
/app/uploads |
Folder for uploaded game photos |
The image serves plain HTTP on port 3001. Put a reverse proxy in front for TLS. Example with Caddy (auto HTTPS):
services:
app:
image: ghcr.io/lo3wer/vancouver-art-thieves-server:latest
restart: unless-stopped
volumes:
- art-thieves-data:/data
- art-thieves-uploads:/app/uploads
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports: ["80:80", "443:443"]
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on: [app]
volumes:
art-thieves-data:
art-thieves-uploads:
caddy_data:
caddy_config:with a Caddyfile like:
your.domain.com {
reverse_proxy app:3001
}
Set EXPO_PUBLIC_API_URL in app/.env to your server URL (e.g. https://your.domain.com/)
and rebuild the app. EXPO_PUBLIC_* values are baked in at build time.