Public Access
4.1 KiB
4.1 KiB
visitor-signin — architecture
Current state of the design. Established 2026-09-23 (first recorded session). When a later session changes something here, edit the relevant section and note the date and reason.
Process layout
One Node process in one container:
- HTTPS listener on container port 3000 (kiosk, admin, APIs).
- Plain-HTTP helper on 3001: serves the CA certificate and redirects everything else to HTTPS.
- Background jobs in-process: photo retention purge, optional nightly auto sign-out, daily certificate renewal check, Google Sheet sync retries.
Data
- SQLite at
/data/visitors.db(bind-mounted./data), WAL mode. - Photos on disk under
/data/photos/YYYY/MM/; banners under/data/branding/; certs under/data/certs/. - Migrations are additive and idempotent (
addColumninsrc/db.js) — never destructive. visitsdenormalises name, contact, host and site name, so history survives deletion of the related records.
Identity and access
- Admin accounts: email + scrypt password + TOTP (RFC 6238), eight one-shot recovery codes.
Roles
owner(manages admins and sites) andadmin(optionally scoped to one site). Optional email-domain restriction. First account created once fromADMIN_BOOTSTRAP_*. - Recurring visitors: mobile number is the username; PIN is 4 digits, stored AES-256-GCM
encrypted with a keyed HMAC
pin_lookupfor uniqueness. Phone, email and PIN are each unique. APP_SECRETkeys sessions, PIN encryption andpin_lookup. Changing it breaks every PIN.
Multi-site
sitesowns: people to visit (hosts), badge settings, printer settings, branding.- A kiosk is bound to a site by
/?site=<slug>(remembered in the tablet's local storage) or a first-run picker; a single-site install never asks. - Sign in, sign out and duplicate detection are scoped per site.
TLS
- Local CA (10 years) + server certificate (398 days), generated by the container with openssl.
- Server cert renews inside 30 days and is hot-swapped with
setSecureContext; the CA is never replaced except by an explicit--force, so devices trust it once. - Reissue is triggered by a change in
HTTPS_HOSTNAMES(tracked incerts/.hostnames.json), not by the full SAN list, because the container's own bridge IP changes between restarts. - CA distributed as PEM
.crt, DER.cer(required by Jamf Pro) and a zip bundle (browsers block bare certificate downloads).
Printing
- The server renders the badge (
src/printer.js,@napi-rs/canvas) at 300 dpi, 696 dots across (QL-820NWB head width on a 62 mm roll), and sends it withbrother_qlover TCP 9100. - Photos are halftoned in the renderer (Floyd–Steinberg by default) so preview equals print.
- The loaded roll is an explicit per-site setting (
printer_label:62or62red).62redalways adds--red; without it the job is monochrome and the printer refuses it. - Sign in waits up to
PRINT_SIGNIN_WAIT_MSfor the print; on failure the kiosk falls back to a browser print of the HTML badge (src/sites.js). brother_ql's network backend never reads the socket and the QL-820NWB does not answer status requests on 9100, so the server cannot confirm a label came out. Brother Status Monitor on a connected PC is the source of truth for refusals.
Google Sheet
- Evacuation mirror only: one tab ("On site now"), rewritten in full on every sign in/out and every 15 minutes. No history is sent to Google; history lives in the app (Visit log, CSV).
- A failed write marks the tab stale and is retried; it never blocks the kiosk.
Front end
- Three separate documents: kiosk (
index.html), login (login.html), console (admin.html). - Kiosk theming is per site: four chosen colours, the rest derived with WCAG contrast rules.
- HTML/JS/CSS served with
Cache-Control: no-cacheso an upgrade is never masked by a cached console.
Deployment
- Author on Windows, deploy on an Ubuntu docker host from the
deploybranch withgit reset --hard origin/deploy(the host is a mirror, never merges). docker-entrypoint.shfixes ownership of the bind-mounted/data, then drops tonode.pull_policy: build— the image is always built locally.