The idea
A lightweight self-hosted service where you write changelog entries in a minimal admin UI, and your users see a notification badge in your app's corner whenever there is unread news. One <script> tag in your HTML is all it takes on the consumer side. Entries are plain text with an optional label — "new", "improved", or "fixed" — and a date. The embed reads a public JSON endpoint on your server, renders a small panel when the badge is clicked, and persists the user's "last read" position in localStorage so the badge clears itself.
Everything runs in a single Docker container with a SQLite database. You bring the domain; Traefik handles TLS.
Why build this
Beamer, Headway, and Canny all solve this problem well but charge $49–99 per month and stamp their own branding on the widget. For an indie developer shipping a small SaaS or an internal tool, that is a significant overhead for what is fundamentally a JSON list and a small UI component.
The technical surface is genuinely small. The public-facing side is a cacheable JSON endpoint and a few hundred lines of vanilla JS. The admin side is a handful of CRUD routes protected by a static token. What does not exist yet is a well-packaged, self-hostable version that is ready to deploy in under ten minutes and does not require managing an NPM package or a separate CDN.
The audience is the long tail of developers who ship tools with users and want to communicate changes without paying for infrastructure they do not need.
Stack sketch
- Backend: FastAPI + SQLite via
aiosqliteand SQLAlchemy Core — minimal dependencies, trivial to back up (one file), no migration tooling required for the scope of v1 - Embed: a vanilla JS custom element (
<changelog-widget>) bundled withesbuildinto a singlewidget.jsfile served from your own origin; no React, no build step on the consumer's side - Admin UI: HTMX + Jinja2 templates; server-rendered forms for creating, editing, and publishing entries — no frontend build pipeline to maintain
- Public API:
GET /api/entriesreturns the last 30 published entries as JSON; versioned URL (/api/v1/entries) so you can evolve the shape without breaking existing embeds - Changelog page: a server-rendered
/changelogroute for users who want the full history outside the widget - RSS feed:
/rss.xmlgenerated from the same entry list; gives readers a subscribe option with no extra infrastructure - Auth: a single
ADMIN_TOKENenvironment variable; the admin routes check a Bearer token; no user accounts, no sessions
Scope for v1
- Create, edit, publish, and unpublish changelog entries with a title, Markdown body, date, and an optional label (new / improved / fixed)
- Embed script that shows a colored badge when unread entries exist; clicking opens a slide-in panel listing the five most recent entries with a link to
/changelogfor the full list - "Last read" timestamp stored in
localStoragekeyed to the widget's origin; the badge clears when the panel is opened - Public JSON and RSS endpoints with a 60-second
Cache-Controlheader so the server is not hit on every page load - Single Docker image;
ADMIN_TOKENandBASE_URLare the only required environment variables - Deliberately out of scope for v1: email digests for subscribers, reader reactions or comments, per-user segmentation by plan, analytics on who opened the panel, OAuth admin login
Where it could go
The most natural follow-on is an email digest. After publishing a batch of entries, a single "send digest" button compiles recent changes into a plain-text email and delivers it through a configured SMTP relay (Amazon SES, Postmark, or a self-hosted msmtp setup). Users who subscribe by entering their address on the changelog page get the email; no external ESP account required beyond SMTP credentials. That feature alone justifies the project for teams with an established user list.
A second expansion is segmentation: pass a plan="pro" attribute in the script tag and mark entries as visible only to certain plan tiers. The embed filters client-side against the attribute value; the JSON endpoint returns all entries and the embed discards irrelevant ones locally. This requires no server-side user model and keeps the architecture flat — the complexity stays in the six-line filter function in widget.js, not in a new database schema.
Watch out for
The embed script runs inside your users' production apps. If your changelog server goes down, the badge fetch will fail — make sure widget.js catches network errors silently and degrades to "no badge" rather than throwing a console exception or blocking the page. Set the fetch with a short timeout (two seconds) and wrap it in a try/catch; a down changelog server should be invisible to your users.