Development
Run locally
git clone https://github.com/achedon12/guestroll && cd guestroll
nvm use # Node 22, see .nvmrc
npm ci
echo "ADMIN_PASSWORD=dev-password-123" > .env.local
npm run dev # http://localhost:3000Fill it with demo events and photos (through the public API, in another terminal):
BASE_URL=http://localhost:3000 ADMIN_PASSWORD=dev-password-123 npm run seed:demoTo try the camera on a real phone, the page must be HTTPS. The quickest way is a tunnel (cloudflared tunnel --url http://localhost:3000) or next dev --experimental-https on your LAN.
Checks
npm run lint
npm run typecheck # generates Next's route types first
npm test # unit + integration (Vitest)
npm run test:coverage # the same, with a coverage report in coverage/
npm run build
npm run test:e2e # functional tests on the production build (Playwright)npm run test:e2e starts the production build on port 3210 with a temporary data folder, so it never touches your data. Locally it uses your installed Chrome; in CI, the Chromium installed by npx playwright install.
With npm run dev running, build into another folder: a build replaces files under the dev server and breaks it. GUESTROLL_BUILD_DIR sets the folder for both the build and the e2e server:
GUESTROLL_BUILD_DIR=.next-e2e npm run build
GUESTROLL_BUILD_DIR=.next-e2e npm run test:e2eThree layers of tests
| Layer | Where | What it runs against |
|---|---|---|
| Unit | tests/unit/ | Pure modules: access policy, validation, rate limiter, QR rendering… |
| Integration | tests/integration/ | Real SQLite in a temp folder, real images through sharp, real ZIPs, QR codes decoded by ZXing |
| Functional | tests/e2e/api/ | The HTTP API of a running production build: guest and admin scenarios, security, quotas |
| Browser | tests/e2e/browser/ | Real journeys: a guest on a phone, an admin on a desktop |
Unit-level files:
| File | What it locks |
|---|---|
policy.test.ts | Every access rule: who sees which photo, in which size |
routeGuards.test.ts | Every admin page and route checks the session; every guest mutation checks the origin |
qrScan.test.ts | Every QR preset and shape is rendered and decoded with ZXing |
qr.test.ts | Style parsing, escaping, logo excavation |
request.test.ts | Client IP and origin rules, the streaming body cap |
rateLimit.test.ts, validation.test.ts, ids.test.ts, negotiate.test.ts, translate.test.ts | The rest of the pure modules |
Branches and CI
develop is the default branch, where pull requests land; main is production. Every change on either branch runs:
| Workflow | What it does |
|---|---|
| Quality | Lint, types, production build |
| Tests | Vitest with coverage, then Playwright on a production build |
| Security | CodeQL (code and workflows) and npm audit on production dependencies, plus a weekly run |
| Docker image | Builds the image, runs it on a custom port and user, uploads a photo end to end, then upgrades: the latest release fills a data folder and the new image must start on it with every event and photo intact |
| Workflows | actionlint and zizmor on .github/ |
Publish to production (manual) fast-forwards main to develop once Quality and Tests passed there; Align develop carries anything that lands on main back to develop. Tagging main with vX.Y.Z runs Release: the multi-arch image on GHCR (and Docker Hub when its secrets are set) and a GitHub release with generated notes. Docs builds the website and the wiki from docs/ (see below) when develop changes them and when a release is tagged.
Upgrades
tests/integration/upgrade.test.ts rebuilds the database of every release from the migrations it shipped, fills it the way that version did, and opens it with today's code. When a release adds migrations, add it to SHIPPED_MIGRATIONS. Never edit a migration once it's released: instances already applied it.
Releasing
- Move the Unreleased entries of
CHANGELOG.mdunder a new## [X.Y.Z] - dateheading, with an Upgrading list when something needs the admin's attention, and update the compare links at the bottom. - Bump
versioninpackage.json, push todevelop, wait for the checks. - Run Publish to production, then tag
mainwithvX.Y.Z.
The release notes are that changelog section (the release fails without one), and the admin's update page shows it to everyone running an older version.
Documentation website
docs/ is both the documentation read on GitHub and the source of the website (VitePress) and of the wiki. Write Markdown there, nowhere else: the wiki is regenerated by the Docs workflow, edits made on it are lost.
cd docs
npm install
npm run dev # http://localhost:5173/guestroll/.github/scripts/build-docs.py builds every release's docs (the latest at the root, develop under /next/ once it differs, older ones under /vX.Y.Z/), with today's configuration and theme so they all share the version switcher. .github/scripts/build-wiki.py does the same for the wiki, with a page per release under vX.Y.Z-Page. Links to files outside docs/ (like ../SECURITY.md) point at GitHub on the website.
Housekeeping: Dependabot (grouped, one-week cooldown), stale issues, a welcome message on first pull requests, a "merge conflict" label, and pull requests sent back to draft when changes are requested.
Database changes
Edit src/lib/db/schema.ts, then:
npm run db:generate -- --name short_descriptionCommit the generated folder under drizzle/. Migrations run automatically on startup. Never edit a migration that has been released.
Conventions
- TypeScript strict, ESM, the
@/alias forsrc/. - Access rules go in
lib/policy.tswith a test. Routes stay thin. - UI text goes in
lib/i18n/messages.ts, in all four languages. - Colors only through the tokens in
globals.css(bg-surface,text-ink-muted…), so both themes keep working. - Comments explain why, not what.
- Commits follow Conventional Commits.
The visual direction is described in design-prompt.md: read it before changing the interface.