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/       β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Request flow: a guest takes a photo

  1. The QR code opens /e/<slug>. The server component looks the event up; if the browser hasn't joined it yet, it renders AutoJoin (no code) or UnlockScreen (code). Joining writes the event id into the encrypted gr_guest cookie.
  2. The guest page renders the capture component chosen for the event (components/capture/registry.ts) inside CaptureHost.
  3. The capture component calls onFiles(files). useUploadQueue compresses each file in the browser, then uploads it with progress, one at a time, retrying on network errors and waiting while offline.
  4. POST /api/e/<slug>/photos checks 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 under DATA_DIR/uploads/<event>/ and inserts the row.
  5. 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.

Code layout

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.ts only. Routes ask it; they never re-derive a rule inline. The SQL filter in listPhotos mirrors canViewPhoto and 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 import server-only so a mistake fails the build.

Data

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.

Choices worth knowing

  • 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.
⚠️ **GitHub.com Fallback** ⚠️