Self-hosted RSS/Atom reader. A single Go binary serving an embedded Svelte SPA, a JSON API + Fever shim, and a background poller that ingests feeds into SQLite (FTS5). Everything runs in containers.
AI is fully optional. Ember can summarize articles with a small local LLM via Ollama, but it's an opt-out feature, not a dependency. Set
EMBER_DISABLE_SUMMARIES=1(or run the stack without theollamasidecar) and the reader works exactly the same — no summary card, no model download, no inference, no LLM-related code paths. Even when enabled, everything runs on your own box; no article content leaves the host. Pick the deployment that matches your stance.
Three options, in order of effort. Each has a walkthrough in docs/getting-started.md:
- Pre-built container (docs) —
ghcr.io/brandonhon/ember:vX.Y.Z(also:X.Y,:X,:latest). Multi-arch linux/amd64 + linux/arm64. Eitherdocker runa single container to kick the tires, or swap thebuild:block indeploy/docker-compose.ymlforimage: ghcr.io/brandonhon/ember:vX.Y.Zto pull instead of building. - Pre-built binary (docs) — download from Releases. Four tarballs (
linux-{amd64,arm64},darwin-{amd64,arm64}) +SHA256SUMS. Includes a samplesystemdunit.VERSION=v0.6.0 curl -L -o ember.tar.gz \ "https://github.com/brandonhon/ember/releases/download/${VERSION}/ember-${VERSION}-linux-amd64.tar.gz" tar -xzf ember.tar.gz && ./ember --version
- From source — see Local development.
cd deploy
cp .env.example .env
# Edit .env — set EMBER_SESSION_KEY (32+ random bytes) and EMBER_ADMIN_PASSWORD
docker compose up -d
Open https://localhost (Caddy serves the SPA + reverse-proxies the API). Log in with the admin credentials you set in .env.
On first boot:
- The
ollama-pullcontainer fetches the configured model (defaultqwen2.5:0.5b). - The
embercontainer creates the admin user fromEMBER_ADMIN_USER/EMBER_ADMIN_PASSWORD. - The poller starts on a 60s tick (configurable via
EMBER_POLL_TICK).
You'll land on an onboarding panel that points to starter packs or OPML import. Pick a pack and you're off.
- Three-pane layout (sidebar / list / reader); single-pane drawer on mobile (≤900px).
- Smart views: Today, Fresh, All Unread, Starred, Read Later, Shared with me.
- Folders (categories) with rename, color, drag-to-reorder.
- Mute feeds; per-feed and aggregate unread badges; "!" badge on errored feeds.
- Cross-feed article dedup with "Also in N feeds" pill.
- Article actions: star, save for later, share (user / email / copy link), board pick.
- Reading-time estimate (200 wpm) on cards and in the reader.
- Paragraph + bullet-point summary card in the reader.
- Per-user toggle for the summary card, plus install-time
EMBER_DISABLE_SUMMARIES/EMBER_DISABLE_IMAGES. - Admin-only LLM controls (Settings → Language model):
- Auto-detected hardware recommendation (
ember probe). - Switch active model live (no restart).
- Pull / delete models from Ollama's cache.
- Tuning sliders for temperature / top_p / num_ctx (persisted).
- Auto-detected hardware recommendation (
- AI ad-stripping: the model also returns a
CLEANEDbody with newsletter signups, podcast/app promos, and social follow asks removed. Falls back to the original when the model can't produce a full body.
- FTS5 full-text search; submitting from the topbar opens a dedicated results view.
- Saved searches: persist a query as a sidebar entry.
- Filter rules with
mark_read,star, orhideactions. - "Mute" popover in the reader actions adds a hide-by-keyword rule in one click.
- Per-article user tags, with a
?tag=…filter on the list endpoint.
- Five curated starter packs (Technology, Programming, Security, DevOps & Infra, World News).
- OPML import/export. Optional scheduled OPML export to
/data/exports/. - Subscribe by URL: paste either a feed URL or just the homepage. Ember follows
<link rel=alternate>and probes common feed paths (/feed,/rss,/atom.xml,/feed.xml,/index.xml). - Drag-to-reorder feeds and folders.
- Mark-all-read at view / feed / category scope.
- Password (argon2id) by default.
- Passkeys / WebAuthn: optional. Register from Settings → Passkeys; sign in with Touch ID / Face ID / hardware key. Requires
EMBER_PUBLIC_URL.
- Opt-in nightly email summarizing your chosen view (Fresh / Today / Unread / Starred / Later).
- Pick the hour + minute in UTC, optionally override the From / To address.
- Configured via Settings → Daily digest. Requires the
EMBER_SMTP_*env vars.
- 15-second polling for new articles while the tab is visible (also fires on tab refocus).
- Canvas-rendered favicon with a green notification dot when unread items arrive.
- Page-title prefix
(N) Emberso narrow tab strips show the count too. - Installed as a PWA, new articles trigger an OS-level numeric badge on the app icon.
- 8 themes: Auto (matches OS), Light, Dark, Solarized, Sepia, Nord, Gruvbox, High contrast.
- Custom theme: pick 3 colors (paper/ink/ember); the rest is derived via CSS
color-mix(). - Admin branding: app name, browser-tab title, favicon URL.
ember probesubcommand reports RAM/CPU/GPU and recommends a model.- Settings → Database: size, manual backup (VACUUM INTO), manual cleanup, schedules.
- Schedules persist in
app_settingsand run via an hourly maintenance goroutine. - User management (create / update / delete / role).
- Resummarize-all to re-process every article after a prompt or model change.
- Reading stats: today/week/30-day, totals, top feeds.
- All confirmations use an in-app modal (no
window.confirm). - Fever-compatible mobile clients via
/fever. - WCAG 2.1 AA passes (axe-core via Playwright).
- PWA: manifest + service worker (cache-first assets, network-first
/api).
| Var | Default | Purpose |
|---|---|---|
EMBER_SESSION_KEY |
(required) | securecookie key (32+ bytes) |
EMBER_ADMIN_PASSWORD |
(required first run) | first-run admin password |
| Var | Default | Purpose |
|---|---|---|
EMBER_ADDR |
:8080 |
listen address |
EMBER_DB_PATH |
/data/ember.db |
SQLite file |
EMBER_ADMIN_USER |
admin |
first-run admin username |
EMBER_OLLAMA_URL |
http://ollama:11434 |
summarizer endpoint |
EMBER_OLLAMA_MODEL |
qwen2.5:0.5b |
initial model (admin can swap later) |
EMBER_DISABLE_SUMMARIES |
0 |
skip LLM summarization entirely |
EMBER_DISABLE_IMAGES |
0 |
drop article hero images at ingest |
EMBER_FRESH_WINDOW |
6h |
"Fresh" cutoff |
EMBER_POLL_CONCURRENCY |
8 |
poller workers |
EMBER_POLL_TICK |
60s |
scheduler tick |
EMBER_SESSION_TTL |
24h |
session cookie lifetime (5m–90d); Settings → Sessions overrides at runtime |
EMBER_LOG_LEVEL |
info |
slog level |
EMBER_TEST_MODE |
0 |
enables fake fetcher/summarizer for e2e |
EMBER_PUBLIC_URL |
(unset) | canonical scheme://host users hit; required to enable passkey sign-in |
EMBER_SMTP_HOST |
(unset) | SMTP host; required to enable daily-digest emails |
EMBER_SMTP_PORT |
587 |
SMTP port |
EMBER_SMTP_USER |
(unset) | SMTP auth user (optional) |
EMBER_SMTP_PASSWORD |
(unset) | SMTP auth password |
EMBER_SMTP_FROM |
(unset) | digest From: address |
EMBER_SMTP_STARTTLS |
1 |
enable STARTTLS on submission ports |
Stored in the app_settings KV; persist across restarts:
- Active LLM model + temperature / top_p / num_ctx
- Branding (name, page title, favicon URL)
- DB backup schedule (off | daily | weekly) + keep-N
- DB cleanup schedule (off | weekly | monthly) + window in days
- OPML export schedule (off | weekly | monthly)
- Session cookie TTL (overrides
EMBER_SESSION_TTL) - SMTP relay (host / port / user / password / from / STARTTLS) for the daily digest
- Initial feed-backlog window (default 48h)
make web-install # one-time
make test # go tests
make web-test # vitest
make embed # build SPA + copy to internal/web/dist
make build # produce ./bin/ember
EMBER_TEST_MODE=1 ./bin/ember # listens on :8080 with the noop summarizer
Hot reload for the SPA:
cd web && npm run dev # vite dev server, proxies /api → :8080
EMBER_TEST_MODE=1 ./bin/ember # in another terminal
# visit http://localhost:5173
Reeder, FeedMe, and other Fever-compatible apps can connect via /fever. The api_key is md5("<username>:<user_id>") — see /api/me for your user_id. (We can't use the canonical md5("user:pass") because passwords are stored only as argon2id hashes.)
make embed build # produce ./bin/ember with the SPA embedded
cd web && npx playwright install chromium
npx playwright test # spawns the binary in test mode against a temp DB
In test mode (EMBER_TEST_MODE=1) the binary seeds a deterministic admin (admin / admintest) plus 12 fixture articles and a single feed, so every spec has known data to assert against.
SQLite with WAL mode, 64 MiB cache, 256 MiB mmap, busy_timeout=5s, synchronous=NORMAL. Single connection — SQLite serializes writes, and the workload is small enough that the connection pool isn't a bottleneck. PRAGMA optimize runs after every startup migrate. Backups via VACUUM INTO are safe to run live.
Migration files live under internal/db/migrations/ and are embedded into the binary.
See docs/architecture.md for the request lifecycle, poller state machine, and summarizer pipeline.