Archived. The wedding happened, voting is over and the service has been shut down. This repository is a snapshot of the code as it stood at shutdown; it is not maintained.
Not included: the database of guest entries (personal data, deleted) and the licensed brand face Syndicate. The font fallback chain lands on Overpass or Georgia instead. Names, domains and the venue link are placeholders.
A small site where guests enter a menu choice per person. One guest can enter
several people (partner, children). On submit, one confirmation mail goes to the
person who voted; the organisers receive a daily summary at a time set by
CRON_SCHEDULE.
The couple get a live dashboard at /dashboard, refreshing every 20 s: a
tally per course plus one card per submission (who · what · when). It is behind
HTTP basic auth — the username is ignored, only the password counts, taken from
DASHBOARD_PASSWORD.
The guest form has no password (that was removed); it is reachable by URL only.
Search engines are shut out three ways: robots.txt (Disallow: /), an
X-Robots-Tag: noindex, nofollow, noarchive header on every response, and
<meta name="robots" content="noindex, …"> in the HTML.
- Host:
menue.example.de, behind the shared Traefik stack - Stack: Node 22 + Fastify, SQLite (file volume), Nodemailer, node-cron
- Design: taken from the live wedding site (Kadence theme) — Overpass for body
copy, Manrope for labels and nav, accent
#a81f24. The name lettering originally used Syndicate, deliberately only for the couple's names — it is unreadable for whole sentences. Those font files are licensed and are not part of this archive.
src/
server.js HTTP server, /api/submit, robots.txt/noindex, cron registration
menu.js >> CENTRAL menu configuration (edit dishes here) <<
db.js SQLite schema + upsert (voting again overwrites)
mailer.js Nodemailer (an empty SMTP_HOST means dev/console mode)
evaluation.js Summary (cron, plus `--once` for testing)
templates/ HTML mails (confirmation + summary)
public/ index.html (the form), app.js, styles.css
The per-course columns in person are named after the course ids in menu.js — the
API reads them as person[course.id], so renaming a course means renaming its column.
Copy .env.example and fill it in.
| Variable | Purpose |
|---|---|
DASHBOARD_PASSWORD |
Password for /dashboard. Required — the server refuses to start without it |
VOTING_OPEN |
true/false — the manual close switch |
VOTING_DEADLINE |
Cut-off YYYY-MM-DD; closes automatically once that day has passed |
SMTP_HOST/PORT/USER/PASS, MAIL_FROM |
Mail delivery. An empty SMTP_HOST logs mails to the container output instead of sending them |
ORGANIZER_EMAILS |
Recipients of the summary, comma separated |
CRON_SCHEDULE |
When the summary goes out; default 0 8 * * 1 (Mondays 08:00, Europe/Berlin) |
KIDS_MENU_URL |
Link to the kids' menu (optional) |
Two ways:
- Cut-off:
VOTING_DEADLINE=2026-07-05— the site closes on its own once that day is over (end of day, Europe/Berlin). Nothing to do. - Immediately: set
VOTING_OPEN=falseand rundocker compose up -d.
Either way the page shows "Voting closed" and submissions are refused server-side as well.
- DNS: an A record for the host → server IP (Traefik fetches the TLS certificate).
- Fill in
.env(for mail delivery,SMTP_*plusORGANIZER_EMAILS). docker compose up -d --builddocker compose ps→ is the container healthy? If not, Traefik gets no cert.
- Send or log the summary right away:
docker compose exec menue-voting node src/evaluation.js --once - Without
SMTP_HOST, every mail is written to the logs as JSON (docker compose logs -f menue-voting) — handy for checking before going live.
Every course, dish and piece of copy lives in src/menu.js. Rebuild the container
after editing. Do not change the key values of options once guests have voted —
old entries would no longer match their labels.