Get in touch,

Maarten Poirot5 min read

Login-less comments on this static site

You can leave a comment at the bottom of this page right now, and it will cost you nothing: no account, no email address, no confirmation link. You get a random animal avatar and a name, both of which you can change, and you can reply to anyone else and edit or delete what you wrote. Nothing in that comment section blocks the article from loading, and all of it is styled like the rest of the site, because I built it myself out of one Lambda function and a pile of small JSON files on S3. I will go over why I built it myself, the write and read path, what a comment file contains, how a thread is just parent and child ids, and how ownership works without a login.

Key takeaways

  • Reads are unauthenticated GETs of JSON. Writes all go through one Lambda.
  • Identity is two cookies: a public author id, and a secret the server stores on first comment.
  • I would rather own the styling and the tradeoffs than drop in a commenting platform.

Why I built it myself

I wanted comments from the first version of this site, in early 2022. That build started from a Gatsby template that already wired up Disqus, a hosted commenting widget you drop into a page. I got it on screen. I did not want it on the site.

Disqus looked like Disqus. Extra chrome, reactions, a visual language that was not mine. A personal site, for me, is as much about making the thing I have in my head as it is about shipping a feature. The comments here are not used all that much. I am still glad they exist, because I built them. The work was the point more than the traffic.

The door actually opened in 2024, when I built a serverless contact form with Google reCAPTCHA, API Gateway and Lambda. A static site could receive input, validate it, and do something with it. S3 was already there for storage. Once those pieces sat together I could see how to build other functions the same way: take a payload, check it in Lambda, write a file, serve the file statically. Comments in 2025 are that pattern pointed at a thread of JSON instead of at email.

I also wanted no login. The usual cost is that you cannot email someone when there is a reply. For me, as a reader, the bottleneck on other sites is creating an account. Here you do not. Volume is low, reCAPTCHA sits on the write path, and I can delete a comment I do not want. That has been enough.

System design

A comment has two lives. Loading a post walks a tree of public files. Submitting, editing or deleting is a POST to API Gateway that wakes a Lambda. The Lambda is the only writer. The diagram below is that split.

Browser
GET /slug/comments/id
POST JSON + reCAPTCHA
validate, write, invalidate cache
Blog post
Comment form
meta.maartenpoirot.com
API Gateway
Lambda
S3 JSON files

The React on the page can style whatever JSON comes back. I have restyled the site since comments shipped. A third-party widget would still have looked like itself. This way the thread is just data.

What a comment is

Each file is self-contained: display name, animal avatar, colour, date, message, and an array of reply ids. The file below is made up, but a real one is served at exactly this kind of URL, for instance this comment under my UTMB World post. The authorId is supposed to be public. That is the point of the last section.

comment.json
Copy
{ "animal": "Platypus", "authorId": "ASzSGqkhcvxk8Ak2", "authorName": "Polite Platypus", "color": "#D4E157", "date": "2025-09-11", "isApproved": false, "isEdited": false, "message": "Hello world", "replies": [] }

The write is a small JSON body in the other direction: slug, parent id, new comment id, message, name, animal, colour, the public authorId, the cookie authorSecret, and a reCAPTCHA token. Lambda checks lengths, checks reCAPTCHA, then writes or updates the file.

Threads are ids, not nested documents

Storage is flat. Every post has a root comment that the page never renders. Its replies list is the top-level thread. Each of those files can list more ids. The client starts at the root and fetches children until the thread is on screen.

A root with a single top-level comment on it, the one from the previous section, looks like this:

root-comment.json
Copy
{ "animal": "Bug", "authorId": "0000000000000000", "authorName": "admin", "color": "#AFCBFF", "date": "2025-01-01", "isApproved": true, "isEdited": false, "message": "", "replies": ["Hq3vT9mZpL2xR7bN"] }

Deletes are the part that is not a single file write. If a comment has no replies, Lambda removes the object and takes its id off the parent. If it already has replies, the message is replaced with a tombstone so the thread keeps its shape. I have to keep parent and child in sync. That is the one place the simple model gets fussy.

Ownership without an account

authorId is in the public JSON, so copying it into a cookie would be enough to impersonate someone if that were the only check. It is not. On first comment, Lambda stores a secret for that id in a private bucket. Later writes must send the same secret from the cookie. The page never shows it.

That is a small lock, not a full identity system. Someone who has both cookies can act as that visitor. I am fine with that at this scale.

Edit and delete on your own comment last as long as the cookies do. They default to a year. In practice you are more likely to have cleared cookies, switched browser, or opened a private window. Then the comment is still there and you cannot change it. I like that better than accounts.

I have a separate admin identity, backed by a secret the Lambda can read and the website cannot. That profile can use a different avatar, and I can remove other people’s comments. There is no login screen. I have not needed much of that power. I have not had spam. I have not had a production bug I remember. The main moderation so far was demonstrating the system to friends, who immediately left dumb comments I then had to delete.

I am not planning a larger commenting product. Optional email-on-reply is the obvious extra, and I would not opt into it myself. For now the files, the form, and the tree are the whole thing.