Adding a Guestbook And Its Risks
Adapting a guestbook from Sappho Sys and its quiet risks of opening web comments to strangers.
Comments are not configured for this post.
loading page…
Adapting a guestbook from Sappho Sys and its quiet risks of opening web comments to strangers.
Comments are not configured for this post.
The web is an open horizon. Without interaction, a personal site can feel like talking to a void... or a rubber duck. I implemented comments to allow Bluesky users to leave their mark under certain articles, but that still meant requiring an account from another social platform. I wanted another way, something that didn’t ask visitors to be anywhere but here. That’s when the idea of a guestbook resurfaced.
Traditionally, guestbooks were paper ledgers where visitors would leave a note when entering an establishment--such as a church or an inn. On the web they serve the same humble purpose: a space for wanderers to leave a public note simply because they stopped by.
Luckily for me, Sapphic Angels had already built a beautiful and simple implementation. I wanted to bring a similar experience to my own site, built on the same architecture so I took direct inspiration from their work. But building a guestbook also meant asking: what are the risks of inviting strangers to scribble on your corner of the web?
Before diving into the components, it helps to understand where everything lives. This site runs entirely on Cloudflare Workers, a serverless platform that executes code at the edge. I just deploy and it works.
Within that ecosystem, I use three Cloudflare services:
Everything is stitched together with the Astro framework, which natively supports Cloudflare as a deployment target.
I didn’t want to bother with a traditional backend, so I ended up with four main pieces:
GET for listing, a POST for signing.client:only) that handles Turnstile and submission.During submission, Turnstile verifies a user's authenticity, the worker sanitizes and stores the message, and the listing page fetches live data on every request.
I knew from the start that a public form may introduce spam and abuse.
Before anything else, the client must solve a Turnstile challenge. The token is sent to Cloudflare’s verification endpoint from the worker; if it fails, the request is rejected immediately. This eliminates 99% of scripted bots without forcing a real visitor to click traffic lights.
After Turnstile, submissions from an IP address in the last five minutes goes through the guestbook_submissions table. If any recent submission exists, the request is blocked with a 429 response.
A human spammer might wait out the rate limit and submit the same message again. To prevent that, I hash the message content with SHA‑256 and check if the same IP posted the same hash within 10 minutes. If so, the submission is rejected as a duplicate.
These combined measures mean that a spammer would need fresh IPs, fresh messages, and the ability to solve Turnstile every time--an effort rarely worth it for a small personal site. These security measures are very rudimentary and is very easily bypassed if they're motivated enough.
I wrote raw SQL with no ORM, but every query uses parameterised statements (? + .bind()), so SQL injection is impossible (to my knowledge). On the output side, a single regex (/<[^>]*>/g) strips HTML tags from names and messages. Because Astro and React escape content inside {}, even if something slips through, it won’t execute.
Turnstile stops most bots, but a determined human can still post unwanted content. For a personal site with low traffic, I decided against a full moderation queue--instead, I rely on:
If spam still gets through, the IP is stored temporarily in a separate table, so I can easily block it at the server level.
IP addresses are personal data. Initially, I stored them permanently in the entries table, a privacy risk I hadn’t fully considered. Now, the guestbook_entries table no longer holds IPs; the submissions table keeps them only for the rate‑limit window (auto‑cleaned after 24 hours). This felt like the minimum viable responsibility for a public guestbook.
If Cloudflare changes pricing, deprecates D1, or has an outage, the guestbook stops working. The code is portable and any SQLite‑compatible backend would work, but the serverless glue is proprietary. I mitigate this by keeping the business logic simple and agnostic.
Even with protections, a motivated person can still send distinct messages from different IPs. By design, there is no moderation queue to keep the module organic and spontaneous.
If the guestbook gets bigger and grows more popular, I’ll need a report system or a dashboard. For now, I'll rely on the obscurity and tiny size of my audience.
An interactive feature is a maintenance burden, no matter how small. I’m accepting that I must occasionally check for spam, prune garbage, and make sure that this thing remains a safe space.
D1, Workers, and Turnstile are all essentially free for a project this size. The whole guestbook runs without needing a traditional server, and I never think about scaling or uptime. However, there may come a time that the hidden costs will catch up.
The client renders a widget, the server verifies a token with a single API call, and the whole flow feels invisible to real users. Combined with rate limiting, it’s a simple anti‑spam prevention.
Overall, building this guestbook was a fun exercise in stitching together modern serverless tools whilst blending it old-web nostalgia. The result is a tiny, fast, and maintainable feature that fits right into the spirit of the archaic and free-spirited websites of eld.
The risks of an open space in the internet are always in my considerations, but we'll see if it'll be manageable.
You’re welcome to leave a note on the guestbook. Please be nice!