v0.1.0 Architecture - achedon12/guestroll GitHub Wiki
Note: Documentation of Guestroll v0.1.0. The latest release is v0.5.0: this page in v0.5.0 Β· what changed Β· other versions
One Next.js app, one process, one SQLite file, one folder of photos.
βββββββββββββββββββββββ Guestroll (Node.js) ββββββββββββββββββββββββ
phone ββQR codeβββΆ β /e/[slug] guest page (server component + client app) β
β /api/e/[slug]/* join, unlock, name, upload, own photos β
β /api/media/[id] the only way a photo leaves the server β
laptop βββββββββββΆ β /admin/* admin pages β
β /api/admin/* events, moderation, QR, archives, stats β
β β
β lib/policy.ts βββ every access decision, pure and tested β
β lib/* βββΆ SQLite (Drizzle) βββΆ DATA_DIR/guestroll.db β
β βββΆ files βββΆ DATA_DIR/uploads/ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- The QR code opens
/e/<slug>. The server component looks the event up; if the browser hasn't joined it yet, it rendersAutoJoin(no code) orUnlockScreen(code). Joining writes the event id into the encryptedgr_guestcookie. - The guest page renders the capture component chosen for the event
(
components/capture/registry.ts) insideCaptureHost. - The capture component calls
onFiles(files).useUploadQueuecompresses each file in the browser, then uploads it with progress, one at a time, retrying on network errors and waiting while offline. -
POST /api/e/<slug>/photoschecks the origin, the join, the upload state and quota (lib/policy.ts), takes a per-guest lock, reads the body with a size cap, sniffs and re-encodes the image (lib/images/process.ts) into three variants, writes them underDATA_DIR/uploads/<event>/and inserts the row. - The photo comes back as a
PhotoDto: ids and dimensions, never a path. Thumbnails load from/api/media/<id>?v=thumb, which re-checks access.
src/
βββ app/ routes (Next.js App Router)
β βββ e/[slug]/ guest page and its social card
β βββ admin/ login, (panel)/ pages, poster, live wall
β βββ api/ route handlers, thin: validate, ask policy, call lib
βββ components/
β βββ ui/ design-system primitives (Button, Field, SegmentedControlβ¦)
β βββ capture/ swappable capture components and their contract
β βββ upload/ CaptureHost, upload queue, exposure counter, prints tray
β βββ guest/ guest page pieces
β βββ admin/ admin pieces (event-form/, moderation/, qr/β¦)
βββ lib/
βββ policy.ts all access rules, no I/O
βββ validation.ts request body parsing, no I/O
βββ db/ Drizzle schema and client (migrations in /drizzle)
βββ auth/ admin password, sessions, page guard
βββ security/ rate limiter, request helpers, ids
βββ images/ upload and logo processing (sharp)
βββ storage/ file paths and I/O
βββ qr/ style parsing, the SVG renderer, server export
βββ i18n/ messages (en, fr, it, es), locale resolution
βββ events.ts, photos.ts, stats.ts, archive.ts data access
Two rules keep this maintainable:
-
Access rules live in
lib/policy.tsonly. Routes ask it; they never re-derive a rule inline. The SQL filter inlistPhotosmirrorscanViewPhotoand is the one exception, commented as such. -
Browser code never imports server modules. Anything shared with client
components lives in modules without Node imports (
lib/constants.ts,lib/qr/render.ts,lib/i18n/messages.ts). Server-only modules importserver-onlyso a mistake fails the build.
| Table | Holds |
|---|---|
events |
Settings, access-code hash, QR style (JSON), logo version |
photos |
One row per photo: event, guest id, optional name, size, status, download count |
activity |
Append-only log behind every stat (view, upload, download, failed codeβ¦) |
settings |
Key/value: admin password hash and its version |
Guests have no table: a guest is a random id in their cookie. Their name is copied onto each photo they send.
Files: uploads/<event-id>/<photo-id>.{original.jpg,display.webp,thumb.webp}
and uploads/<event-id>/logo.png.
- SQLite in WAL mode handles a wedding's burst of uploads comfortably and makes backups a file copy. The flip side: one process, no horizontal scaling. That's also why the rate limiter is in memory.
- Re-encoding every upload costs CPU but removes GPS metadata and any payload hidden in the file.
- One upload at a time per guest, enforced on both sides, keeps quotas exact and phones responsive.
- The QR renderer is pure and shared by the live preview and the server export, so the downloaded file matches the preview.