byandrev's blog

6 min read

My own comment server instead of using Discus

I was trying to add comments to my blog, byandrev.dev. What I wanted was narrow: privacy, a small script, a simple UI, GitHub sign-in, and something I could deploy on my own server. Disqus, giscus, utterances and Remark42 all cover part of that list. None of them covered all of it the way I wanted.

So I wrote my own. It's called Hilo, it's MIT.

Hilo comments homepage
Hilo comments homepage

What I tried first

Disqus was out immediately. You load hundreds of kilobytes of third-party JavaScript and hand your readers' traffic to an ad network.

giscus is a great idea and I ran it on this blog for a while. Two things pushed me off it. Theme switching between light and dark mode kept giving me trouble, and my comments lived in a repository, in a schema I don't control.

Remark42 was the closest fit, and if you want a mature self-hosted option today, use it. I wanted something smaller. Just comments and GitHub login. No Google, no Facebook, no Microsoft, no voting, no markdown. Plain text.

I wanted my own database, a schema containing only what I need, an instance for each site I manage, and a widget whose style I can customise without having to create a fork.

The Hilo widget. No ads.
The Hilo widget. No ads.

Five routes, and that's the whole API

MethodPathAuthNotes
GET/api/comments?site=&page=—Flat list ordered by creation. Deleted rows come back blanked
POST/api/commentsBearer{site, page, body, parent_id?} → 201 with the new row
DELETE/api/comments/{id}BearerAuthor or admin → 204
GET/auth/{provider}/login?origin=—provider = github
GET/auth/{provider}/callback—Returns the postMessage bridge page

Interactive docs at /docs. Nesting isn't in there because there's nothing to add: the list comes back flat, and the widget builds the tree client-side from parent_id. At blog scale that's cheaper than a recursive query and it means one request per thread.

What Hilo doesn't do

No markdown. No editing. No votes or reactions. No pagination. No moderation queue. No spam filter. Mandatory GitHub OAuth plus a per-user, per-minute rate limit is the entire anti-abuse story.

A moderation queue for a blog that gets twelve comments a month is a second product. It needs an interface, state, notifications, and someone to actually check it. What I have instead is DELETE /api/comments/{id}, and the delete is soft, so removing a comment doesn't take its replies down with it.

Markdown is the same trade. I'd need an editor, a renderer, HTML sanitization, and an XSS surface I'd have to keep watching. For plain-text replies on a personal blog, that's a lot of code to own.

Every one of these gaps is something I don't have to maintain.

Running it

Full details are in DEPLOY.md. Four steps.

1. Database. Use the mongo service that already ships in docker-compose.yml, or point MONGO_URI at an external instance like Atlas.

2. Environment. cp .env.example .env, generate a key with python3 -c "import secrets; print(secrets.token_urlsafe(32))", then fill in BASE_URL, ALLOWED_ORIGINS, ALLOWED_SITES and the credentials from a GitHub OAuth App. ALLOWED_SITES is the one to get right: without it, anyone who finds your URL can write comments under any site name they invent.

3. Styles. Link embed.css from the CDN (hilo_comments on npm), and a theme on top if you want one. A theme is just CSS overriding the same custom properties.

4. Embed. Paste the <script> tag with data-site, data-api and data-page.

The compose file includes Mongo, so docker compose up -d works as-is with nothing else installed:

yaml
services:
  comments:
    image: ghcr.io/byandrev/hilo:latest
    ports: ["8000:8000"]
    env_file: .env
    environment:
      MONGO_URI: mongodb://mongo:27017
    depends_on: [mongo]
    restart: unless-stopped
  mongo:
    image: mongo:7
    volumes: ["./data:/data/db"]
    restart: unless-stopped

Or skip the server entirely. Fork the repo, import it on Vercel, set the same environment variables from .env.example, and deploy. Same app, same MONGO_URI pointing at Atlas, nothing to keep patched. The demo runs against a Vercel deployment.

Environment variables for deployment on Vercel
Environment variables for deployment on Vercel

The operation is a typical serverless one, cold starts on a thread that nobody has loaded for some time, and a connection to Mongo that is opened on every invocation, rather than using a connection pool. It’s noticeable to some extent, but it’s nothing serious, in my view, it’s acceptable.

The project is new. I have no numbers to show you, no comment counts, no instances running other than mine. If you try it, the repo is byandrev/hilo and the issues are open.

Frequently asked questions

Is Hilo free?

The software is MIT, so yes. Hosting isn't. You need somewhere to run a FastAPI app and somewhere to keep a MongoDB database. Vercel's free tier plus MongoDB Atlas's free tier covers a personal blog at zero cost. A small VPS with Docker runs about five dollars a month and gives you both.

Do visitors need a GitHub account to comment?

Yes. GitHub OAuth is the only sign-in method, which means Hilo fits a developer audience and not much else.

Does Hilo use cookies or track visitors?

No adverts, no data analysis, no third-party scripts. The only data stored is what GitHub returns about a logged-in user and the comment itself, both in your database. The GitHub token is stored in local storage.

Comments