Repository navigation
The Demo Instance
A fictional wedding anyone can poke at, on an instance that cannot be changed.
There is one running. No login — walk into
/adminand change anything you like; a refresh puts it all back.
A second stack running the same image as production with one environment variable flipped. It holds a completely fictional wedding, it has no login, and nothing anyone does to it is ever saved. That combination is the point: you can hand the URL to somebody, tell them to go anywhere and change anything, and know that the next visitor sees the demo exactly as seeded.
docker compose -f docker/docker-compose.demo.yml up -d
# http://localhost:3001 — no password, and no /admin/loginThat is the whole setup. It shares nothing with production but the image: its own database, its own volumes, its own port.
DEMO_MODE=true on the normal image:
| Normal instance | Demo instance | |
|---|---|---|
/admin |
Redirects to /admin/login
|
Open. /admin/login redirects inwards
|
| ADMIN button in the nav | Only when logged in | Always |
A write to /api/…
|
Runs | Answered, not stored |
| Top of every page | — | A banner saying so |
Writes are dropped in the middleware, not in the routes. Every POST /
PATCH / PUT / DELETE under /api/ is intercepted before it reaches its
handler and answered with a plausible success — the fields you sent, echoed back,
plus a synthetic id. Client code carries on as if it worked, so the UI updates
and the change is there on screen; the next GET returns the seeded data
again, which is exactly the "change anything, refresh, it's all back" behaviour
the demo wants.
Doing it in one place rather than in the 26 routes that write is deliberate: a feature added next month cannot forget to be immutable, and the guarantee doesn't depend on any particular handler being careful. It is also what makes having no login safe.
Two consequences worth knowing. The instance is shared, not per-visitor — two people looking at once each see the seed plus their own unsaved edits, never each other's. And a photo upload is refused like any other write, so the file never lands (the grid may show a placeholder until you navigate away).
src/lib/demo.ts refuses DEMO_MODE=true unless the database in DATABASE_URL
is actually named demo, and logs once when it refuses. Every default fails
towards "this is production":
- Not set, or set to anything but
true→ normal instance. - Set, but pointed at the real database → refused, and the site keeps its login and its writes.
- The banner and the always-on ADMIN button come from the same function, so an instance cannot be silently read-only either — if writes are being dropped, the banner is up.
The demo compose file still sets ADMIN_PASSWORD=demo. It is vestigial while the
flag holds — there is no login page to type it into — and it is there so that if
the flag is ever refused, the instance falls back to asking for a password
rather than standing wide open.
Postgres is deliberately not published to the host in that compose file. The
demo is meant to be reachable by anyone and its password is demo; a published
port would let a visitor write to the database directly with psql, straight
past a flag that only guards the application.
The stack has a one-shot seed service that waits for the database, wipes it and
fills it — the guest list, the RSVPs, the budget, the seating, the photos, the
honeymoon — writing config and photos straight into the same volumes the web
service mounts. No docker cp step.
Reseeding on every start is safe because the instance is immutable: there is
no visitor's work to protect. It is also how the demo stays current — a fresh
seed is how it gains data for features that shipped since it was last deployed,
which is the whole reason the seeder is built and published on every push to
main. Drop SEED_ALWAYS=true and the seeder reverts to filling only an empty
database.
The seed image is ghcr.io/soccerbeats/weddingwebsite-seeder:latest — the
seeder stage of docker/Dockerfile, published by the Demo Instance
workflow. See Deployment.
To seed by hand instead, against any database:
DATABASE_URL=postgresql://demo:demo@host:5432/demo \
CONFIG_DIR=/tmp/demo/config PHOTO_DIR=/tmp/demo/photos \
npm run seed:demo -- --yes-wipeIt refuses to run without --yes-wipe and prints the database it is about to
empty. Never point it at production.
The content lives in src/lib/demoSeed.ts and is entirely invented: Maya and
Theo, ninety guests with addresses and dietary notes, twenty-six RSVPs, a budget
of twenty-eight lines, thirteen tables with a hundred-odd people seated, and a
sixteen-day honeymoon in Portugal. The guest list is generated from a fixed seed,
so re-seeding gives the same names and a screenshot stays true.
Two deliberate choices. Photographs are fetched from Lorem Picsum with a fixed
seed per file, so the demo ships with real images rather than grey rectangles and
the same run always produces the same pictures — --no-photos skips the download
when re-seeding. And the honeymoon's places are real Portuguese landmarks with
approximate coordinates, because a map of invented points looks like a bug.
docker/docker-compose.demo.yml is the file, and it pins its project name to
weddingdemo so the stack keeps that name (and its volumes) however you invoke
it. Anything else you need to know is in the comments at the top of it.
To redeploy after a push, once the image has published:
docker compose -f docker/docker-compose.demo.yml pull
docker compose -f docker/docker-compose.demo.yml up -dThe pull gets the new app image and the new seeder; up -d recreates the web
container and runs the seed again, so the demo comes back holding data for
whatever just shipped.
Setting it up
Using the admin panel
- Guests and RSVPs
- Seating and the Export
- Registry
- Finances
- Honeymoon Portal
- Content and Settings
- Offline
How it works