simple.remind.rsvp, or the code you stick on the fridge
Same day, same block party. An admin view nobody on the internet can reach, a sign-in loop that was the browser being right, and an eight-character code that gets a host back in.
This morning’s post said simple.remind.rsvp was about two thousand lines of Go and that it tries very hard not to break. By dinner it was 3,490 lines. That’s what happens when you publish something and then keep using it.
Nothing here replaces the first post. This one covers what got added the same day, and why: a better layout, a help page, an admin view, and a way for a host who loses their link to get back in without an account.
Audience: the same people as last time. Operators of small web services, and anyone who has lost the one link that mattered.
The running example is still the Elm Street block party. This time the host has lost their host page. Not the share link, which is in forty group texts. The private one, the one that shows who’s coming. It was in a browser tab, and the browser updated.
🔺 1. What changed
- The pages have a shape now. On a phone everything stacks in one column, in the order you need it. On a desktop the event, guest and host pages become cards in two columns. The make-an-event form still fits on one 360 by 640 phone screen, with two pixels to spare.
- There’s a help page at https://simple.remind.rsvp/docs/. It says what the site does and, just as plainly, what it can’t.
- There’s an admin view, for me, reachable only from my own network.
- Hosts get a code. Eight characters, shown once, to write down. With the share link it gets you back to your host page.
The first two are what they sound like. The last two are the rest of this post.
🔺 2. An admin view for a site with no accounts
Every site that takes input from strangers eventually needs someone to look at it. The question for this one was what “someone” looks at, when there are no accounts.
There are three things worth watching:
- Events. What was made, when, how many said yes, how many emails went out.
- Guest addresses. The email addresses people gave, how many events each appears at, and whether it was ever confirmed.
- Networks. Where things came from. Not the address, the network: the first three parts of an IPv4 address, or the first three groups of an IPv6 one. That’s enough to see that one office made forty events this afternoon, and not enough to point at a person. It’s deleted with the event.
From those, the abuse page asks five questions:
- Which rate limits are full, or nearly?
- Which networks are making lots of events, or lots of RSVPs?
- Which events are collecting addresses that nobody confirms? That’s someone using the RSVP form to send confirmation links to strangers.
- Which addresses keep turning up at events and never confirm?
- Which events look like spam: a link in the title, a pile of links in the notes, the same title a dozen times in a week?
And it can do something about it. It can close an event, which hides its pages, refuses RSVPs and stops its mail. It can delete an RSVP, forget an address, and block an address or a network. Every action is written to a log in the same database transaction, with who did it and why. There is no action without its record.
🔺 Nobody on the internet can reach it
The admin view lives at a name on the fd53 mesh, served from BOOK-01. Three things stand between it and the internet:
- The name. It only resolves on the mesh and the tailnet.
- The web server. Caddy drops any connection that isn’t from a mesh, tailnet or loopback address.
- FD53 sign-in. The same Keycloak, the same passkey, the group
adminsand nothing else.
There’s one more piece, and it’s the one I almost got wrong. The app trusts a header from the sign-in gate that says who you are. That’s normal behind a gate. But BOOK-01 runs a lot of other things, and anything on it could connect to a local port and send that header itself. So the admin view doesn’t listen on a port. It listens on a Unix socket that only the web server’s group can open. Everything else on the box gets “permission denied,” which is the right answer.
🔺 3. The sign-in loop that was the browser being right
The first time I signed in, it worked, and then it sent me back to the sign-in page. Every time. The gate’s log said I’d signed in fine.
The gate’s session cookie is SameSite=Strict. That tells the browser: only send this cookie when the request starts on this same site. It’s a good default for a cookie that grants admin access.
Here’s the dance:
- You go to the admin view. You’re not signed in, so you’re sent to
sso.itys.net. - You sign in there with your passkey.
sso.itys.netsends you back to the gate, which sets the session cookie and sends you on to the page you wanted.- The browser asks for that page. The chain of redirects started at
sso.itys.net, which is a different site. So the browser holds back the Strict cookie. - No cookie, so the gate sends you to sign in. Go to step 1.
The browser was doing exactly what I asked. The same gate had worked for weeks on extent.itys.net, because extent.itys.net and sso.itys.net are the same site as far as the browser cares. The admin view is on fd53.net, which isn’t.
The fix keeps the cookie Strict. After sign-in, the gate now answers with a tiny page that says “Signed in” and moves on to the admin view by itself. That last step starts on the admin site, so the cookie goes with it. It’s one setting, on for the gate that needs it and off for the one that doesn’t.
🔺 4. A code to write down
The host page’s address is the key to the event. That’s what makes the site work without accounts, and it’s also the problem: lose the address, and you’ve lost the event. There’s no “forgot password” when there’s no password.
So now, when you make an event, the host page shows you a code once:
Your code: GR4Y-F9A6. Write it down.
With the event’s share link, that code gets you back to your host page at https://simple.remind.rsvp/recover. Back to the Elm Street host: they find the share link in any of those forty group texts, find the code on the sticky note on the fridge, and they’re back in.
🔺 Built for people typing on a phone
- The alphabet is Crockford’s base32: digits and capital letters, minus I, L, O and U. If you type an O, it’s read as a zero. An I or an L is read as a one. Capitals or not, dash or not, spaces or not.
- The last character checks the others (Luhn, done in base 32). Every single mistyped character is caught before it counts against you. The one swap it misses is a 0 next to a Z, which is the same blind spot the check digit on your credit card has with 0 and 9.
- A typo costs nothing. It’s caught as a typo and you’re told so.
- A well-formed wrong code costs a try. Five an hour per event. That’s the line between “I mistyped it” and “someone is guessing.”
🔺 Nothing to steal
The site doesn’t store the code, and it doesn’t store the host link either.
The code and the moment the event was made go through a keyed hash (HMAC-SHA256), with a secret that lives on the servers and not in the database. That produces the 128-bit key in the host link. The database keeps a hash of that key, which is enough to check a link and not enough to make one.
| What | How much |
|---|---|
| The code | 35 secret bits, plus 5 check bits, in 8 characters |
| The host link | 128 bits, made from the code; still only 35 secret bits behind it |
| The database | A hash of the link: useless without the server’s secret |
| Guessing online | 5 tries an hour per event. 235 codes. Bring a lunch, and a lot of grandchildren |
| Database and secret both stolen | About a minute of computing per event to find its code |
That last row is the honest one. If someone takes both the database and the secret, a 35-bit code doesn’t hold for long. But before this change the database held every host link in plain text, which held for no time at all. It’s better than it was, and it’s written down where I’ll see it.
The secret has a version number, and each event remembers which version made its link. A new secret can go in at any time: new events use it, and old links keep working.
🔺 Two more buttons
- Make a new link. If the host page’s address got out, type your code, and you get a new address. The old one stops working, and the code stays the same. It has to ask for the code, because the site never kept it.
- I didn’t write my code down. You get a new code and a new link, and the old link stops working.
Events made before this morning don’t have a code. Their host pages have one button, Make a recovery code, and after that they behave like any other event.
🔺 5. Changing the database under five copies
The site runs as five copies: four colors and the unikernel on BOOK-01, all sharing one database. That made the last step trickier than the code.
The old copies read a column called host_key. The new ones don’t want it to exist. If the first new copy to start drops the column, every copy that hasn’t been updated yet starts failing, until it is.
So it went out in two releases:
- Release one added the new columns and stopped writing to
host_key, but left it there. Old and new copies could run side by side. - Release two, once every copy ran release one, hashed the few old keys that were left and dropped the column.
Release one still read host_key, so even release two had a few seconds of overlap. I rehearsed it on a scratch database first: old schema, old-style rows, then the new build. Then I copied the new build to all five copies and restarted them at once, so the overlap was a few seconds instead of a minute.
The four events made before codes are fine. Their links still work, and they’re stored as hashes now too.
🔺 6. Where it stands
| Lines | |
|---|---|
| The site | 3,490 |
| Tests | 1,712 |
So this morning’s “about two thousand” is out of date. The morning post stays as it was, because that’s how this blog works. This is the newer number.
The tests include the test vectors from the design, checked byte for byte, plus a test that dumps the events table and fails if a host key or a code appears anywhere in it.
What it still doesn’t have, on purpose: accounts, passwords, texting, payments, or any way for the site to email a host. A host who loses both the link and the code is out of luck. That’s the price of not having accounts, and it’s on the help page in those words.
If you’ve ever made an event and lost the page, make one at https://simple.remind.rsvp, and this time write the code down.