simple.remind.rsvp, or the casserole without the machinery
Six questions, one link, no account. A two-thousand-line Go program that tries very hard not to break.
The last post was about remind.rsvp and all the machinery behind it: 47,000 lines of Rust, passkeys, parser sandboxes, GeoDNS, a chat network. All of that, so a garden club can find out who’s bringing a casserole.
Then I wanted a version that a neighbor could use in thirty seconds and that I could read in an afternoon. The brief I gave myself was short: dreadfully simple, and it can’t break.
That’s https://simple.remind.rsvp. This post is about what it does, what it leaves out, and how something this small stays up.
Audience: people who run small web services, and anyone who’s ever organized a block party from a group text.
Our running example is the Elm Street block party, Saturday, November 14 at 3:00, with a potluck at 5:00 and cleanup on Sunday morning.
🔺 1. Six questions
The home page asks six things:
- What is it?
- When does it start? (Chicago time)
- Does it repeat?
- Where? (an address or a link)
- Is it online? Is it private?
- Anything else?
Press Make the event and you get two addresses. One is the link to share. The other is your private host page, where you watch RSVPs come in. Nobody makes an account. The host page’s address is the key, so don’t paste it into the group chat.
The whole form fits on a 360×640 phone screen without scrolling. I check that with a headless browser after every layout change, and it currently ends at 638 pixels. The site is held to the same rules as the big one: WCAG 2.2 AAA, 7:1 text contrast, and buttons at least 44 pixels tall.
🔺 2. What a guest sees
The neighbor at number 1200 opens the shared link. They type a name, pick Yes, Maybe or No, say whether they’re bringing anyone, and can leave an email address for a reminder.
The email is optional and it’s guarded. An address gets exactly one message: a link to check it’s really theirs. Nothing else is sent until they press the button on that page. If they never press it, that RSVP and its address are deleted after a week. The page also says, plainly, that the host can see the address.
Once confirmed, they get up to three reminders:
| Reminder | When | Who gets it |
|---|---|---|
| A week before | 7 days out | Yes and Maybe |
| The day before | 24 hours out | Yes and Maybe |
| Soon | 2 hours out | Yes only |
The wording lives in one table in rules.go, and a test renders every message, so a typo fails the build instead of a Saturday.
Guests who don’t want to give an email address at all can follow a public event two other ways. Each event has an RSS feed, and an ntfy topic for phone notifications. A host who ticks Private gets neither.
Every guest also gets a page of their own. It holds their answer, their reminders, the follow links, and a check-in code.
🔺 3. At the door
The check-in code is a QR code. The guest shows it at the door, and the host scans it with their phone’s camera.
The trick is knowing who’s scanning. Opening the host page drops a cookie in that browser, scoped to the check-in addresses only. When the host’s phone opens a guest’s code, it sees the cookie and checks the guest in on the spot, with an Undo button for the inevitable wrong person. When anybody else opens it, it just says “This is a check-in code for the Elm Street block party.”
After the check-in, the host can ask whether the guest would like a welcome packet by email. It holds the schedule and a link back to their own page. It goes to the address on file, to a different address the guest gives right then (and that one isn’t kept), or nowhere. Each guest can get it once.
The QR codes are drawn by BWIPP, the Barcode Writer in Pure PostScript. I used the bwip-js port of it, cut down to QR only, running inside a JavaScript interpreter inside the Go program. That sounds like a lot of machinery for one square. It is. BWIPP is the reference for how these symbols should be drawn, and a code that won’t scan in bad light at a folding table is worse than no code. The first one takes about a tenth of a second; after that they’re cached.
🔺 4. A schedule, and your calendar
The block party has parts: coffee on the porch at 3:00, kids’ games at 3:30, the potluck at 5:00, cleanup on Sunday at 10:00. The host types those into a spreadsheet and uploads it as CSV:
date,start,end,title,where
2026-11-14,15:00,15:30,Welcome and coffee,Front porch
2026-11-14,15:30,17:00,Kids games,Back yard
2026-11-14,17:00,19:00,Potluck dinner,Street
2026-11-15,10:00,11:00,Cleanup,
Times are in 15-minute blocks. If a line is wrong, the error names the line. There’s an example file to download, and uploading a new file replaces the old schedule.
Guests see the schedule with a checkbox beside each part. Tick the potluck, skip the cleanup (we’ve all done it), and press Add the events I select to my calendar. Tick nothing and you get the whole event.
🔺 5. Things that happen every Thursday
Not everything is a block party. Board game night is every Thursday at 7:00. So the start time has a Repeat menu beside it: every day, every weekday, every week, every two weeks, every month on the same date, every month on the same weekday, or every year.
A repeating event is stored once, with the date it started. Whenever a page is shown, the program works out which occurrence is current and shows that one. When Thursday night ends, the page rolls forward to next Thursday, and the RSVPs come along with it. Reminders go out for each occurrence, once each, and check-ins start fresh every week.
Calendars get one entry with a repeat rule, not a hundred copies. The time is pinned to Chicago, so 7:00 PM stays 7:00 PM when the clocks change. The edge cases follow what calendars do: “every month on the 31st” skips the months that don’t have one, a fifth Thursday becomes “the last Thursday,” and February 29 comes around every four years.
🔺 6. Two thousand lines
remind.rsvp is 47,000 lines. This one is about 2,000 lines of Go, plus 1,200 lines of tests:
| File | Job |
|---|---|
web.go | the pages and forms |
store.go | PostgreSQL |
rules.go | the reminder wording |
mail.go | sending mail |
schedule.go | the CSV schedule |
repeat.go | repeating events |
qr.go | BWIPP |
pages.go | the HTML and CSS |
It has two direct dependencies: pgx for PostgreSQL, and goja to run BWIPP. The pages, icons, logo and time zone data are all built into the binary. There’s no JavaScript in the pages and no tracking. The Content Security Policy allows nothing from anywhere else.
Every request is limited, and the limits are in the database, so all the copies share them. One address can get 3 check-it’s-you links a day. One event can trigger 50 an hour, and the whole site 500. One network address can create 5 events and send 20 RSVPs an hour. When a send fails, its hit is refunded.
The tidy-up job runs on the hour. It deletes RSVPs whose address was never confirmed after a week, and events 90 days after they end, except repeating events, which don’t end.
🔺 7. Can’t break
“Can’t break” is a big promise for any program. Here’s how this one tries to keep it.
Five copies. The program runs on all four remind.rsvp colors: Chicago, Toronto, Washington and Los Angeles. DNS sends you to the nearest one, and if that one’s app is down, its web server hands you to another over the fd53 mesh. The fifth copy is a Nanos unikernel on BOOK-01, which is every color’s last resort. It’s the same binary, booted as its own little operating system, with nothing else inside it. It reaches the database over the mesh in IPv6, like everyone else.
One database. All five share one PostgreSQL database. The connection string lists every color’s address and asks for whichever one accepts writes, so when the primary moves, nobody edits a config file.
Every copy runs the scheduler, and each mail still goes once. Before a reminder is sent, a row is claimed in a sent table, keyed by guest, reminder and date. Five copies can wake up in the same minute and race for it, and only one wins. I tested that with all four colors running at once. It also makes the rate limits safe to fail open: if the limit check can’t reach the database, nothing can be sent anyway, because nothing goes out without its sent row.
It waits for the network. The unikernel’s IPv6 address comes up about two seconds after it boots. The program retries the database for 30 seconds at startup instead of falling over.
It rolls back. Every deploy keeps the previous binary beside the new one. Going back is one copy and one restart.
🔺 8. What it doesn’t do
There’s no sign-in, so there’s no account to recover. The host can’t send announcements to guests. There are no texts, no chat, no photos and no payments. If you need those, the big remind.rsvp has them. The small one sends a reminder, checks people in at the door, and gets out of the way.
Try it: make an event at https://simple.remind.rsvp, send yourself the link, and say yes.