Foedus
Self-hosted wedding landing site with guest management and RSVP collection
About
Fœdus is a self-hosted web app for wedding logistics: a landing site for the couple, dedicated invitations for each guest (or guest group), RSVP collection, a collaborative Spotify playlist, a money-transfer gift registry and a small AI chatbot to answer guests' questions.
It's designed to be deployed once per wedding: spin it up, configure it via the admin dashboard, share the public URL and per-guest invitation links, and let it collect everything you'd otherwise gather via WhatsApp threads and spreadsheets.
Usage
Public site
The homepage (/) shows the couple's landing page: ceremony and reception venues, an optional collaborative Spotify soundtrack, places that mattered to the couple, honeymoon stops on a map, and a gift registry with per-item progress.
Each guest gets a dedicated invitation URL of the form https://your-domain/<code>, where <code> is a short opaque identifier generated for each invitation from the dashboard. The invitation page renders the personalized invite, lets the guest RSVP, and — when visited the first time — flips a "viewed" flag in the database so the couple can see who has actually opened theirs.
Loading the homepage with ?invite=<code> unlocks two extra affordances: a floating button to update the RSVP, and the input box on the soundtrack section that lets the guest add tracks to the Spotify playlist.
Admin dashboard
The admin lives at /dashboard, behind HTTP basic auth (credentials set via ADMIN_USER and ADMIN_PASSWORD). From there, the couple can:
- set everything that shows up on the landing page (names, venues, dates, photos, places, soundtrack, gift registry items, accommodations, OG metadata);
- manage the guest list, with bulk CSV import and per-guest RSVP/confirmation tracking;
- generate invitations, group guests into a single invitation when needed, and reset the viewed-state if a guest needs a fresh link;
- review gifts received and tweak amounts when somebody transfers a wrong figure;
- run polls to gather guests' opinions on something (e.g. song requests, dietary needs);
- override homepage labels in any supported language without touching the source.
Docker
The simplest way to run it is via the bundled compose file:
ADMIN_USER=admin ADMIN_PASSWORD=secret docker compose up -d
This starts Fœdus on port 3000, with the SQLite database persisted in a named Docker volume.
Reverse proxy
Fœdus expects to sit behind a reverse proxy in production (nginx, caddy, etc.) — the proxy terminates TLS and forwards to Fœdus on its plain HTTP port. Make sure to forward the X-Forwarded-For header so per-IP rate limiters bucket correctly: set TRUSTED_PROXIES to the comma-separated list of upstream IPs in addition to loopback.
Chatbot
If OPENROUTER_API_KEY is set, a small chat bubble appears on the homepage. It uses OpenRouter as the LLM gateway and defaults to meta-llama/llama-3.3-70b-instruct:free; override via OPENROUTER_MODEL.
The bot is fed the wedding settings as context, so it can answer common guest questions ("what time does the ceremony start?", "is there parking?") without the couple having to repeat themselves.
Collaborative soundtrack
If SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET and SPOTIFY_REFRESH_TOKEN are set, and a playlist URL is configured in the dashboard, invited guests can search Spotify and queue tracks to the wedding playlist directly from the homepage. The search box is gated on the invite query param — visitors arriving without an invite see a disabled hint instead.
Installation
Docker
The recommended way to run Fœdus is via Docker, pulling the prebuilt image from GHCR:
docker pull ghcr.io/streambinder/foedus:latest
A docker-compose.yml is shipped at the repository root, ready to be tweaked:
ADMIN_USER=admin ADMIN_PASSWORD=secret docker compose up -d
The compose file persists the SQLite database in a named volume (foedus_data) mounted at /data inside the container.
Custom build
Fœdus is plain Go, so the usual toolchain works. templ is needed once to generate the view code:
git clone https://github.com/streambinder/foedus.git
cd foedus
go install github.com/a-h/templ/cmd/templ@latest
templ generate
go build
./foedus
To stamp the binary with a cache-busting asset version (used to invalidate /static/* and /media/* on the client), pass an ASSET_VERSION value through -ldflags — this is what the Dockerfile does on every build:
ASSET_VERSION="$(date -u +%Y%m%d%H%M%S)" \
go build -ldflags "-X github.com/streambinder/foedus/internal/buildinfo.AssetVersion=${ASSET_VERSION}"
Configuration
Fœdus reads its configuration from environment variables:
| Variable | Required | Default | Purpose |
|---|---|---|---|
ADMIN_USER | yes* | — | HTTP basic auth username for /dashboard |
ADMIN_PASSWORD | yes* | — | HTTP basic auth password for /dashboard |
ADMIN_USER1..9 | no | — | Additional admin usernames (numbered suffix); paired with the same-numbered ADMIN_PASSWORD1..9. At least one pair is required overall, else the app refuses to boot. |
ADMIN_PASSWORD1..9 | no | — | See above |
DATABASE_URL | no | foedus.db | Path to the SQLite file |
PORT | no | 3000 | TCP port to bind |
TRUSTED_PROXIES | no | 127.0.0.1,::1 | Extra reverse-proxy IPs to trust for X-Forwarded-For (loopback is always trusted regardless) |
LOG_LEVEL | no | info | slog level: debug, info, warn, error |
LOG_FORMAT | no | text | text (default) or json for structured logging |
OPENROUTER_API_KEY | no | — | Enables the homepage chatbot via OpenRouter |
OPENROUTER_MODEL | no | meta-llama/llama-3.3-70b-instruct:free | OpenRouter model name |
SPOTIFY_CLIENT_ID | no | — | Enables the collaborative soundtrack |
SPOTIFY_CLIENT_SECRET | no | — | Same as above |
SPOTIFY_REFRESH_TOKEN | no | — | Same as above |
* The app accepts either the unsuffixed ADMIN_USER / ADMIN_PASSWORD pair or any number of suffixed ADMIN_USER1..9 / ADMIN_PASSWORD1..9 pairs — at least one valid pair must be set, otherwise the process panics at startup rather than expose the dashboard with default credentials.
Everything else (couple names, venues, photos, gift list, accommodations, guest list, invitations, polls) lives in the SQLite database and is configured from the admin dashboard at /dashboard.
Design
Fœdus is a single Go binary serving a Fiber-based HTTP app, with views rendered server-side via templ and state persisted in a single SQLite file.
There is no separate frontend build pipeline: the only first-party JavaScript shipped is a handful of vanilla .js files under static/, served gzipped and cached aggressively. No SPA, no bundler, no node toolchain at runtime. static/places.js is the one exception that loads a third-party library (Leaflet) at runtime from unpkg.com rather than bundling it.
Routes
The router is split into three logical groups, registered in this order:
- Public unauthenticated routes (
/,/media/*,/og-image,/gift/claim,/chat,/soundtrack/*). Every state-changing POST goes through a 256KB body cap and a CSRF token issued via a cookie on every public GET. Per-IP rate limiting (10 req/min) is applied additionally to/chatand/soundtrack/addonly, since those endpoints fan out to an LLM provider and to write-heavy SQLite paths respectively. RSVP, gift-claim and invitation-viewed POSTs rely on the CSRF + body-cap pair without an explicit rate limiter. - Admin routes mounted under
/dashboard, gated by HTTP basic auth and a stricter same-site CSRF policy. The dashboard accepts up to 32MB per request because the settings form ships base64-encoded photos inline. - Invitation catch-all routes (
/:code,/:code/viewed,/:code/rsvp), registered last so they don't shadow any other path.
Storage
A single SQLite file holds everything: settings, guests, invitations, RSVPs, gifts, soundtrack events, polls. Schema is created in code via idempotent CREATE TABLE blocks; manual ALTER TABLE statements are applied out-of-band when columns need to change, then mirrored into the CREATE block for fresh installs.
Photos and other binary assets are stored as blobs in the same database, served via /media/:id. This keeps the deployment a single mountable volume — no separate object storage to back up.
Templating
All HTML is generated by templ: .templ files compile to plain Go that streams strings into the response. Helper functions live alongside templates in the templates/ package, so any logic that's awkward to express in templ syntax (URL shaping, conditional section ordering, JSON payload assembly) ends up as a regular Go function.
Observability
Structured logs go to stdout via log/slog. An access-log middleware annotates every request with method, path, status, latency and the resolved client IP (post-X-Forwarded-For). A /healthz endpoint returns 200 unconditionally and is wired into the Docker HEALTHCHECK.
Internationalization
Translations live in a single internal/i18n/i18n.go map keyed by language code. The currently shipped languages are English and Italian. The dashboard exposes a subset of keys (HomepageKeys) that the couple can override per-language without redeploying, so wording on the public site can be tweaked without code changes.
Why so few moving parts
The deployment target is "a small VPS owned by the couple, running for ~6 months, then shut down". The whole stack picks the smallest viable option at every layer: SQLite over Postgres, server-rendered HTML over SPA, vanilla JS over framework, single binary over microservices. The cost of operating it should be a couple of euros a month and zero ongoing maintenance.