Next.js (App Router) app with ShadCN-inspired UI, JSON persistence, and atomic backups to digitize the pantry raffle flow.
- Staff dashboard (
/admin) to set ranges, toggle random vs sequential, append tickets, re-randomize, update “now serving,” and reset with confirmations. - Public display (
/) with airport-style grid and QR code sharing, auto-polling/api/stateevery 4s. - Multilingual display UI with language switcher (English, 中文, Español, Русский, Українська, Tiếng Việt, فارسی, العربية) and automatic RTL direction for Farsi/Arabic.
- Built-in read-only board in Next.js plus an optional standalone server (
npm run readonly) on its own port for edge/legacy hosting. - File-based datastore with atomic writes, timestamped backups, and append logic that preserves prior random order.
- Tests written with Vitest + Testing Library for the state manager and grid highlighting.
- Display: http://localhost:3000/
- Admin: http://localhost:3000/admin
- Login: http://localhost:3000/login
- Staff intro: http://localhost:3000/staff
- Live: https://williamtemple.app (Vercel, custom domain)
- Auth: Magic link + OTP fallback; restricted to
@williamtemple.org - Email: Resend sender
[email protected](add DMARC/SPF/DKIM in DNS) - Database: Neon Postgres (serverless) with shared connection pool
- Hosting: Next.js 16 on Vercel (proxy.ts middleware, serverless runtime)
AUTH_BYPASS=false
AUTH_SECRET=<generated>
AUTH_TRUST_HOST=true
DATABASE_URL=postgresql://...sslmode=require
[email protected]
RESEND_API_KEY=re_...
ADMIN_EMAIL_DOMAIN=williamtemple.org
NODE_ENV=production
npm run dev— start the Next.js dev server.npm run build— production build.npm start— run the built app.npm run readonly— start the optional standalone read-only board on port 4000 (configurable viaREADONLY_PORT).npm test— run Vitest suite.npm run lint— run ESLint.
- Built-in:
/in Next.js, polling/api/stateevery 4s; high-contrast wall-screen UI with WTH logo. - Optional standalone:
npm run readonlyon port4000, still pollingdata/state.jsonfor legacy/edge hosting. - Configure standalone via env vars:
READONLY_PORT— port to listen on (default4000).READONLY_POLL_MS— poll interval in milliseconds (default4000).READONLY_DATA_DIR— directory containingstate.json(default./data).
npm run readonly # open http://localhost:4000
- State stored under
data/state.jsonwith timestamped backup files (state-YYYYMMDDHHMMSSmmm-XXXXXX.json). - Data dir is ignored by Git except for
data/.gitkeepto preserve the folder.
- Target hosting is Vercel’s hobby/free tier, which uses Neon-backed Postgres for managed storage.
- Vercel filesystem is ephemeral (
/tmponly), so production persistence must move off local files to a durable store (e.g., Neon Postgres via Vercel Postgres/Neon SDK). - Stay within hobby limits (approx. 190 compute hours, ~512 MB DB storage, up to 10 DBs); avoid features that require paid plans.
- Plan to map current
state.json+ snapshot history to a Postgres schema (or equivalent durable store) so undo/redo and backups remain available across deployments.
- Provision storage (Neon via Vercel Marketplace Postgres integration)
- Install the Neon integration from the Vercel Marketplace and attach it to this project; note
POSTGRES_URL/DATABASE_URLfrom the Storage tab. - Initialize tables (run once via Neon console/psql):
create table if not exists raffle_state ( id text primary key default 'singleton', payload jsonb not null, updated_at timestamptz not null default now() ); create table if not exists raffle_snapshots ( id text primary key, payload jsonb not null, created_at timestamptz not null default now() ); create index if not exists raffle_snapshots_created_at_idx on raffle_snapshots (created_at desc);
- Plan a retention policy (e.g., trim to last N snapshots or last N days) to stay within ~512 MB free storage.
- SDK: prefer
@neondatabase/serverless(actively maintained). If upgrading from legacy@vercel/postgres, use@neondatabase/vercel-postgres-compatas a drop-in during transition. - Env:
DATABASE_URLis required for production and preferred locally; file-based state storage is only used whenDATABASE_URLis absent in development.
- SDK: prefer
- Configure auth (magic links, domain-locked)
- Use NextAuth Email provider with Resend free tier for mail delivery.
- Enforce
@williamtemple.orgallowlist in the sign-in callback and/or before sending links. - Required env vars (set in Vercel project):
DATABASE_URL(Neon connection string)AUTH_SECRET(strong random string)AUTH_TRUST_HOST=trueEMAIL_FROM(verified sender in Resend)RESEND_API_KEYADMIN_EMAIL_DOMAIN=williamtemple.org
- Wire persistence to Postgres
- Replace the file-based state manager with Postgres-backed reads/writes using
@neondatabase/serverless(or@neondatabase/vercel-postgres-compatas a drop-in). - On persist: upsert
raffle_stateand insert intoraffle_snapshotsunless backups are skipped. - On load: read
raffle_state, seeding a default row if missing. - Undo/redo/list/restore operations should query
raffle_snapshotsordered bycreated_at desc. - Keep
/tmpcaching optional only for short-lived warm instances; treat the DB as source of truth.
- Snapshot retention
- Add a daily cron job (Vercel Cron: max 2 jobs on Hobby) to call a small API route that trims old snapshots (e.g., keep last 500 or last 30 days).
- Avoid per-request rate limiting in KV for public reads; if desired, rate-limit only admin/write routes (tiny traffic) using Upstash Redis free or simple in-process guards.
- Migration from local files (one-time)
- Write a script to read
data/state.jsonanddata/state-*.jsonand insert intoraffle_state/raffle_snapshots. - Run the script locally with
DATABASE_URLpointing to Neon; verify counts and sample undo/redo in the admin UI.
- Deploy
- Set all env vars in Vercel.
- Deploy the Next.js app; verify
/(public board),/admin(auth required), and/api/statereads/writes against Neon. - Confirm magic-link delivery works for an
@williamtemple.orgaddress.
- Observability
- Vercel free logs are short-lived; optionally add Sentry free or a lightweight
errorstable in Neon for aggregation (avoid PII). - Add a simple health/readiness route; ensure errors return 4xx/5xx without stack traces in production.
- Production domain:
williamtemple.app(custom domain in Vercel). - Planned routes:
/→ public read-only board (default homepage)./login→ magic-link entry; after sign-in, redirect to the staff landing page (current homepage content)./admin→ staff dashboard (unchanged), linked from the staff landing page after login./staff→ staff welcome/intro (former homepage).
- Update Vercel project settings to point the production domain at this app; keep localhost paths for development (
http://localhost:3000app with/, optionalhttp://localhost:4000standalone read-only server).
docker-composeruns the app, Postgres, and MailDev (SMTP + web UI). Default.env.localusesDATABASE_URL=postgresql://postgres:postgres@db:5432/neondb?sslmode=disable,EMAIL_SERVER_HOST=maildev,EMAIL_SERVER_PORT=1025.- Fully offline: leave
RESEND_API_KEYunset, keepEMAIL_FROM=login@localhost, and optionally setAUTH_BYPASS=trueto skip auth. - To exercise the full email flow locally, keep
AUTH_BYPASS=false, start docker, and open magic links from MailDev athttp://localhost:1080.
- Copy the example environment file:
cp .env.example .env.local
- Generate an auth secret:
openssl rand -base64 32
- Fill
.env.localwith required values:AUTH_SECRET(required) andAUTH_TRUST_HOST=trueDATABASE_URL=postgresql://postgres:postgres@db:5432/neondb?sslmode=disableEMAIL_FROM=login@localhost,EMAIL_SERVER_HOST=maildev,EMAIL_SERVER_PORT=1025ADMIN_EMAIL_DOMAIN(optional; restrict sign-ins)- Optional:
RESEND_API_KEY+ productionEMAIL_FROMwhen testing Resend instead of MailDev - Optional:
AUTH_BYPASS=trueto skip auth during UI work
- Required services:
- Provided by docker compose: app, Postgres, MailDev (open http://localhost:1080 to view emails)
- Neon/Resend are only needed for production or remote testing
See .env.example for the full list. Critical vars:
AUTH_SECRET— required for JWT encryption (generate with openssl)DATABASE_URL— required for magic-link/OTP storageRESEND_API_KEY— required to send emails via ResendEMAIL_FROM— must be verified in Resend (production default[email protected])ADMIN_EMAIL_DOMAIN— restricts login to your domain
Local options:
- Set
AUTH_BYPASS=trueto bypass login during UI work (still requiresDATABASE_URLfor server start). - Add
RESEND_API_KEYand a productionEMAIL_FROMto test Resend instead of MailDev.
- Build and start locally (includes a bind mount for persistent
data/):docker compose up --build
- App listens on
http://localhost:3000(public board/, staff dashboard/admin, staff intro/staff). - Stored state lives in your host
./datadirectory so it survives container restarts.
- Next.js 16 (App Router) + Tailwind CSS.
- ShadCN-style UI components (Radix + cva).
- Vitest + Testing Library.
- 1.0.3 (2025-11-29) — Added operating hours with timezone selection, preserved through reset, plus closed-day display messaging and pantry hours table.
- 1.0.1 (2025-11-28) — Added Vietnamese/Farsi/Arabic translations, RTL-aware public display (scoped to display only), and per-language timestamp localization.
- 1.0.0 (2025-11-27) — Production release with magic link + OTP auth, Neon/Resend, snapshot cleanup, Speed Insights, custom domain.
- 0.9.0 — Initial Vercel deployment and custom domain setup.
- Global palette and design tokens live in
src/app/globals.css(--color-primary, surfaces, borders, focus, status colors). - UI components (buttons, badges, cards, inputs, switches, tooltips) consume those tokens rather than hard-coded colors. Update tokens to change app-wide styling.
- The public display no longer shows a mode pill; mode selection is still managed in the admin controls but not surfaced in the UI chrome.
- Snapshot history: admin can undo/redo and restore from timestamped snapshots (backed by
data/state-*.jsonfiles) via/api/stateactions and UI controls.