Newbury Nights
A fan-made AR ghost-hunting web app — a tribute to the AR mechanics of the LEGO® Hidden Side™ app. The focus is the Hunter Mode AR loop: raise your phone as a ghost detector, scan the colour wheel to uncover gloom, lock on, and blast ghosts down before your battery dies.
Fan-made tribute. Not affiliated with, sponsored by, or endorsed by the LEGO Group. LEGO® and Hidden Side™ are trademarks of the LEGO Group.
What's inside
- Backend — Node + Express 5, SQLite via
better-sqlite3, bcrypt password hashing, JWT auth (8h), multer image uploads. - Frontend — Three.js (ES module import maps via CDN),
getUserMediacamera passthrough withDeviceOrientationgyro look (iOS Safari friendly),BarcodeDetectorQR scanning with a manual code fallback. - Ghost rendering — animated-GIF billboards (texture pumped via
texture.needsUpdateeach frame) when a ghost has an uploaded image; procedural Three.js wisp meshes otherwise. - Spawning — rarity-weighted: ★ common down to ★★★★ legendary.
- Admin panel (
/admin) — JWT-gated. Upload / enable / disable / delete ghost images, create sets with scan codes linked to ghost rosters (many-to-many viaset_ghosts). - Public endpoint —
GET /api/scan/:coderequires no auth and returns a set's ghost roster.
Data
The ghost roster, stats, abilities, and boss→set references are seeded from data/*.json, which are generated from the source spreadsheet by scripts/extract_ghosts.py. Three ghost types (red / yellow / blue) and four rarity tiers drive the colour-wheel and damage mechanics.
To regenerate the JSON from a spreadsheet:
python3 scripts/extract_ghosts.py path/to/Ghost_Data.xlsx data
Step-by-step setup
This walks through a fresh deploy on the Ubuntu server, behind the existing nginx (which already terminates HTTPS).
1. Prerequisites
Node 18 or newer, plus the build toolchain so better-sqlite3 can compile its native module:
node --version # expect v18+; install via nvm or nodesource if missing
sudo apt-get update
sudo apt-get install -y build-essential python3
build-essential is required — without it npm install fails while compiling better-sqlite3.
2. Get the code and install dependencies
git clone https://gitea.hideawaygaming.com.au/jessikitty/newbury-nights.git
cd newbury-nights
npm install
npm install compiles the native SQLite module, so it may take a minute on first run.
3. Configure environment
cp .env.example .env
.env is loaded automatically at startup via dotenv, so npm start and npm run seed read it directly — no process manager or manual export needed. Edit .env and set, at minimum:
JWT_SECRET— a long random string (e.g.openssl rand -hex 32). This signs login tokens; treat it like a password.ADMIN_USER/ADMIN_PASS— the bootstrap admin login created on first seed. You'll change the password from the admin panel right after.PORT— defaults to33033; change if that port is taken. Keep it in sync with the nginx upstream (step 6).
4. Seed the database
npm run seed
This creates db/newbury.sqlite, loads the 111 ghosts / 36 abilities / 17 sets from data/*.json, and creates the admin user. Expected output ends with something like Seed complete: { ghosts: 111, abilities: 36, sets: 17, rosterLinks: 153 }.
Re-running npm run seed is safe — it only seeds ghost/set data when the ghosts table is empty, so it never clobbers admin edits. It always ensures an admin user exists.
5. Start the app
npm start
You should see Newbury Nights listening on http://127.0.0.1:33033 (or whatever PORT you set). Test it locally:
curl http://127.0.0.1:33033/healthz # -> {"ok":true}
curl http://127.0.0.1:33033/api/scan/NN-70419 # -> Wrecked Shrimp Boat roster + Captain Archibald
For production, run it under a process manager so it restarts on reboot/crash — e.g. pm2 start server.js --name newbury-nights, or a systemd unit. dotenv still loads .env under either.
6. Put nginx in front (HTTPS)
Camera and gyro APIs only work in a secure context, so the site must be served over HTTPS. Copy the example server block and adjust server_name and certificate paths (the example upstream already points at 127.0.0.1:33033 — match it to your PORT):
sudo cp deploy/nginx.conf.example /etc/nginx/sites-available/newbury-nights
# edit server_name + ssl_certificate paths to match your domain/certs
sudo ln -s /etc/nginx/sites-available/newbury-nights /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
The app runs plain HTTP on 127.0.0.1:PORT; nginx terminates TLS and forwards to it. app.set('trust proxy', 1) is already set so secure cookies work behind the proxy.
7. First login and lock-down
- Open
https://your-domain/admin. - Sign in with the
ADMIN_USER/ADMIN_PASSfrom your.env. - Go to the Account tab and change the password immediately.
- Under Ghosts, upload billboard GIFs and enable/disable ghosts. Under Sets, edit scan codes and rosters.
8. Play
Open https://your-domain/ on a phone. Use Scan a Set (camera QR or type a code like NN-70419), Free Hunt for a quick random session, or Ghost Index to browse the roster. Allow camera and motion access when prompted.
nginx
See deploy/nginx.conf.example for the full reverse-proxy block referenced in step 6.
API quick reference
Public:
GET /api/scan/:code— set roster + boss for a scan codeGET /api/freehunt?n=&type=— rarity-weighted random spawnsGET /api/ghosts?type=&rarity=&boss=— public ghost index (enabled only)GET /api/abilities— ability reference
Auth:
POST /auth/login·POST /auth/logout·GET /auth/me·POST /auth/change-password
Admin (JWT required):
GET/POST /api/admin/ghosts,PATCH/DELETE /api/admin/ghosts/:id,POST /api/admin/ghosts/:id/imageGET/POST /api/admin/sets,PATCH/DELETE /api/admin/sets/:id,PUT /api/admin/sets/:id/roster
Project layout
server.js Express app + static hosting
db/index.js SQLite schema + connection
routes/ auth, public api, admin api, auth middleware
scripts/extract_ghosts.py xlsx -> data/*.json
scripts/seed.js seed DB from data/*.json + bootstrap admin
data/ ghosts.json, abilities.json, sets.json
public/ index.html (game), admin.html, css/, js/
deploy/ nginx.conf.example
uploads/ uploaded ghost billboards (gitignored)