DEV Community

Cover image for I built my own GitHub Gists alternative — self-hosted, bilingual, and it has an Insights dashboard
Fedya Serafiev
Fedya Serafiev

Posted on Originally published at linuxdevops.dev

I built my own GitHub Gists alternative — self-hosted, bilingual, and it has an Insights dashboard

I've been using GitHub Gists and OpenGist for years, and something always felt a little off.

One thing: no Bulgarian anywhere in the interface. Another: GitHub Gists are tied to GitHub — you can't self-host them, there are no tags, no stats, no way to say "this snippet is for my server, on my domain, under my rules". OpenGist is honestly great and it inspired parts of this project, but I kept wanting a different flavor: a modern dashboard, a command palette, my own language in the UI — and a setup so simple that the whole thing is one SQLite file and one Docker command.

So I did what we all do: I built my own. Meet gistly — a self-hosted, open-source snippet sharing app.

Home — dark

The pitch

Paste code, get a link, share it. Everything lives on your server in a single SQLite file. It's like GitHub Gists, except:

  • 🎨 Syntax highlighting with Shiki and the official GitHub Light & Dark themes — 40+ languages, line numbers, soft-wrap toggle
  • 📝 Markdown preview — .md files render with tables, task lists and highlighted code blocks (GFM)
  • 🔐 Single-admin mode — only you (password login) create and edit; everyone else just opens links
  • 👁 Three visibility levels — Public (listed), Unlisted (link-only), Secret (link + token)
  • 🔎 Full-text search + tags (#typescript, #sql…) with a clickable tag cloud
  • 📊 Insights dashboard — a GitHub-style activity heatmap, language & tag breakdowns, most-viewed and latest gist
  • ⌨️ Command palette — Ctrl+K anywhere, search gists and run commands
  • 📄 Load-more pagination with live counters
  • 🌗 Dark & light theme, and a fully bilingual EN / BG interface — yes, that was point one 😄
  • 🐳 Docker-first — multi-stage image, persistent SQLite volume, ./deploy.sh and you're done

Insights dashboard

The Insights dashboard and the command palette are the two things I really missed elsewhere. The heatmap is the GitHub contribution graph for your snippets — 26 weeks, one cell per day, violet instead of green. The palette is the VS Code / Linear pattern: hit Ctrl+K, type three letters, jump to the gist.

Command palette

The stack

Boring on purpose, modern where it counts:

  • Next.js (App Router) + TypeScript + Tailwind CSS 4 + shadcn/ui
  • Prisma + SQLite — no database server. The whole "cluster" is one file you can docker cp out the door as a backup
  • Shiki for highlighting (the same engine as VS Code, with real GitHub themes)
  • Framer Motion, next-themes, Sonner for the polish
  • Docker multi-stage build with Bun — the final image runs Next.js standalone and applies the Prisma schema on startup, so a fresh container just works

Deploying it: 4 commands, honestly

git clone https://github.com/fedya-dev/gistly.git
cd gistly
echo "ADMIN_PASSWORD=change-me" > .env
echo "AUTH_SECRET=$(openssl rand -hex 32)" >> .env
./deploy.sh
Enter fullscreen mode Exit fullscreen mode

That's it — the app is on port 3000, your data survives rebuilds in the gistly_data volume, and any reverse proxy (Caddy gives you HTTPS automatically) can sit in front:

gist.example.com {
    reverse_proxy localhost:3000
}
Enter fullscreen mode Exit fullscreen mode

New gist editor

Two bugs I want to remember

Shipping this taught me a couple of things worth sharing.

1. navigator.clipboard doesn't exist on plain HTTP. My "Copy link" button worked on localhost and died with a TypeError on http://<server-ip>:3000 — the Clipboard API only exists in secure contexts. The fix is the classic fallback that still works everywhere:

if (window.isSecureContext && navigator.clipboard) {
  await navigator.clipboard.writeText(text)
} else {
  // legacy path: a hidden <textarea> + document.execCommand('copy')
}
Enter fullscreen mode Exit fullscreen mode

Ship both, and the button works before and after you set up TLS.

2. BuildKit Bake is strict about network: host. My docker-compose.yml builds on the host network (some VPSes have broken Docker bridge DNS — classic EAI_AGAIN hangs during bun install). Newer Compose versions (2.36+) build through BuildKit Bake and refuse that with additional privileges requested: pass "--allow=network.host". The workaround became a tiny deploy.sh:

docker buildx build --allow=network.host --network=host -t gistly:latest .
docker compose up -d
Enter fullscreen mode Exit fullscreen mode

If your Docker networking is healthy, plain docker compose up -d --build works too.

Gist page

What the stats look like under the hood

The Insights endpoint is one parallel Promise.all — counts, groupBy for tags/languages/visibility, and one raw SQLite query that counts code lines without ever loading file contents into Node:

db.$queryRaw`
  SELECT SUM(LENGTH(content) - LENGTH(REPLACE(content, CHAR(10), '')) + 1) AS lines
  FROM File
  WHERE gistId IN (SELECT id FROM Gist ${visibilityFragment})
`
Enter fullscreen mode Exit fullscreen mode

The activity heatmap is just 182 daily buckets aligned to Sundays — cheap, and it renders instantly client-side.

What's next

Some ideas on the list: raw URL shortening, gists that auto-expire, optional multi-user mode, and OG images that render the first code lines for pretty link previews. The whole thing is MIT licensed and small enough to read in one sitting — PRs and issue rants are welcome.

👉 github.com/fedya-dev/gistly

Mobile view

Thanks for reading! If you self-host your snippets too, tell me what you miss in yours — I'm collecting ideas in the issues.

Top comments (0)