the self-hosted alternative to disqus

Comments that live on your server.

A FastAPI backend, MongoDB, and one <script> tag you drop into your pages. Sign in with GitHub. Self-host on a VPS with Docker, or deploy to Vercelno ads, no tracking, no third party reading your visitors' data. Maintained by byandrev, open source, MIT.

The Hilo widget: a comment composer above two nested replies, each with a GitHub avatar and relative timestamp.
install
docker pull ghcr.io/byandrev/hilo:latest
widget
npm install hilo_comments

# or skip the install, use the CDN:
# https://cdn.jsdelivr.net/npm/hilo_comments/embed.js

Or grab the files directly — embed.js · embed.css

  1. 1.0DATABASE
  2. 2.0CONFIGURE
  3. 3.0STYLE
  4. 4.0EMBED

Four steps, from server to your blog.

One self-hosted instance. Your database, your users, your rules. This is the whole journey.

1.0DATABASE

Set up MongoDB

Two ways to get one: the mongo service already in docker-compose.yml, nothing else to install — or an external instance like MongoDB Atlas. Create a database there, copy its connection string into MONGO_URI, and you're done.

Using Atlas with Docker? Drop the mongo service and its MONGO_URI override from docker-compose.yml. Deploying to Vercel instead? Fork the repo, import it there, and point MONGO_URI at Atlas — same environment variables, no server or container to manage.

docker-compose.yml
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
.env (Atlas / Vercel)
MONGO_URI=mongodb+srv://user:pass@cluster.mongodb.net
MONGO_DB=hilo

2.0CONFIGURE

Configure the environment

Copy the template and generate a SECRET_KEY. BASE_URL, ALLOWED_ORIGINS and ALLOWED_SITES are the base. OAuth credentials go here too.

ALLOWED_SITES is what stops your instance being an open database — without it, anyone who finds the URL can write comments under any site they invent.

.env
# python3 -c "import secrets; print(secrets.token_urlsafe(32))"
SECRET_KEY=

BASE_URL=https://comments.example.com
ALLOWED_ORIGINS=https://example.com
ALLOWED_SITES=myblog

GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=

3.0STYLE

Link the CSS

The widget is published on npm as hilo_comments. Pull embed.css from the CDN, or download it and serve it yourself. A theme is optional, layered on top.

A theme is just CSS overriding the same custom properties — write your own in one :root block instead if you'd rather.

HTML
<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/hilo_comments/embed.css"
/>
<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/hilo_comments/themes/nord.css"
/>

4.0EMBED

Paste the tag

The widget renders wherever you put the tag. data-api is your own server; data-page is optional and defaults to location.pathname.

data-sort=oldest|newest · data-scheme=light|dark

HTML
<script
  src="https://cdn.jsdelivr.net/npm/hilo_comments/embed.js"
  data-site="myblog"
  data-page="/posts/hello-world"
  data-api="https://comments.example.com"
></script>

Themes.

Each one overrides the same custom properties from embed.css. Link one from the CDN, or download the file and serve it yourself.

What it ships with.

No magic. Each of these is a real line in the README.md.

  • GitHub login

    No passwords, no account management.

  • Unlimited reply nesting

    Resolved client-side from a single flat query.

  • Soft deletes

    Authors delete their own, admins delete any. Replies survive.

  • Rate limited

    Per user, per minute. No moderation queue needed.

  • MongoDB via Beanie

    Backup is mongodump.

  • Multi-site

    One instance serves every blog in ALLOWED_SITES.

  • Published on npm

    hilo_comments — CDN or self-hosted, no bundler required.

  • Restyleable

    Plain CSS custom properties. No Shadow DOM, no build, no fork.

  • Automatic dark mode

    Follows prefers-color-scheme.

  • Relative timestamps

    Localized to each visitor's language.

What it doesn't do.

No markdown, no editing, no votes or reactions, no notifications, no pagination, no moderation queue, no spam filter. Mandatory OAuth plus the rate limit is the whole anti-abuse story — and for a personal blog that's enough.

  • no markdown
  • no editing
  • no votes
  • no notifications
  • no pagination
  • no moderation
  • no spam filter

A small API, on purpose.

Six routes. Interactive docs at /docs. Everything else is easy to add later.

Method · Route Auth Notes
GET /api/comments?site=&page= Flat list ordered by creation. Deleted rows come back blanked.
POST /api/comments Bearer {site, page, body, parent_id?} → 201 with the new row.
DELETE /api/comments/{id} Bearer Author or admin → 204.
GET /auth/{provider}/login?origin= provider = github.
GET /auth/{provider}/callback Returns the postMessage bridge page.
GET /embed.js · /embed.css · /themes/{name}.css The widget assets, also served by your own instance.

Comments on your server, today.

A docker pull, a .env and a script tag — or fork the repo and deploy on Vercel. Either way, the database is yours.