{"id":50749057,"url":"https://github.com/brandonhon/ember","last_synced_at":"2026-06-11T00:00:43.459Z","repository":{"id":360359507,"uuid":"1247185689","full_name":"brandonhon/ember","owner":"brandonhon","description":"Self-hosted RSS reader with on-device AI summaries. Single Go binary, embedded Svelte SPA, runs behind Caddy.","archived":false,"fork":false,"pushed_at":"2026-06-10T16:09:00.000Z","size":35092,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-10T16:17:01.302Z","etag":null,"topics":["ai","atom","docker","feed-reader","fever-api","golang","ollama","rss","rss-reader","self-hosted","sqlite","svelte"],"latest_commit_sha":null,"homepage":"https://brandonhon.github.io/ember/","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/brandonhon.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-23T02:08:11.000Z","updated_at":"2026-06-09T20:30:21.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/brandonhon/ember","commit_stats":null,"previous_names":["brandonhon/ember"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/brandonhon/ember","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brandonhon%2Fember","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brandonhon%2Fember/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brandonhon%2Fember/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brandonhon%2Fember/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/brandonhon","download_url":"https://codeload.github.com/brandonhon/ember/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brandonhon%2Fember/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34175887,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-10T02:00:07.152Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","atom","docker","feed-reader","fever-api","golang","ollama","rss","rss-reader","self-hosted","sqlite","svelte"],"created_at":"2026-06-11T00:00:26.097Z","updated_at":"2026-06-11T00:00:43.425Z","avatar_url":"https://github.com/brandonhon.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Ember\n\nSelf-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.\n\n\u003e **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 the `ollama` sidecar) 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.\n\n## Install\n\nThree options, in order of effort. Each has a walkthrough in [docs/getting-started.md](docs/getting-started.md):\n\n1. **Pre-built container** ([docs](https://brandonhon.github.io/ember/getting-started#run-from-the-released-container-image)) — `ghcr.io/brandonhon/ember:vX.Y.Z` (also `:X.Y`, `:X`, `:latest`). Multi-arch linux/amd64 + linux/arm64. Either `docker run` a single container to kick the tires, or swap the `build:` block in `deploy/docker-compose.yml` for `image: ghcr.io/brandonhon/ember:vX.Y.Z` to pull instead of building.\n2. **Pre-built binary** ([docs](https://brandonhon.github.io/ember/getting-started#run-from-a-pre-built-binary)) — download from [Releases](https://github.com/brandonhon/ember/releases). Four tarballs (`linux-{amd64,arm64}`, `darwin-{amd64,arm64}`) + `SHA256SUMS`. Includes a sample `systemd` unit.\n   ```sh\n   VERSION=v0.8.4\n   curl -L -o ember.tar.gz \\\n     \"https://github.com/brandonhon/ember/releases/download/${VERSION}/ember-${VERSION}-linux-amd64.tar.gz\"\n   tar -xzf ember.tar.gz \u0026\u0026 ./ember --version\n   ```\n3. **From source** — see [Local development](#local-development).\n\n## Quickstart (Docker)\n\n```\ncd deploy\ncp .env.example .env\n# Edit .env — set EMBER_SESSION_KEY (32+ random bytes) and EMBER_ADMIN_PASSWORD\ndocker compose up -d\n```\n\nOpen `https://localhost` (Caddy serves the SPA + reverse-proxies the API). Log in with the admin credentials you set in `.env`.\n\nOn first boot:\n1. The `ollama-pull` container fetches the configured model (default `qwen2.5:0.5b`).\n2. The `ember` container creates the admin user from `EMBER_ADMIN_USER` / `EMBER_ADMIN_PASSWORD`.\n3. The poller starts on a 60s tick (configurable via `EMBER_POLL_TICK`).\n\nYou'll land on an onboarding panel that points to starter packs or OPML import. Pick a pack and you're off.\n\n## Features\n\n### Reading\n- Three-pane layout (sidebar / list / reader); single-pane drawer on mobile (≤900px).\n- Smart views: Today, Fresh, All Unread, Starred, Read Later, Shared with me.\n- Folders (categories) with rename, color, drag-to-reorder.\n- Mute feeds; per-feed and aggregate unread badges; \"!\" badge on errored feeds.\n- Cross-feed article dedup with \"Also in N feeds\" pill.\n- Article actions: star, save for later, share (user / email / copy link), board pick.\n- Reading-time estimate (200 wpm) on cards and in the reader.\n\n### AI summaries\n- Paragraph + bullet-point summary card in the reader.\n- Per-user toggle for the summary card, plus install-time\n  `EMBER_DISABLE_SUMMARIES` / `EMBER_DISABLE_IMAGES`.\n- Admin-only LLM controls (Settings → Language model):\n  - Auto-detected hardware recommendation (`ember probe`).\n  - Switch active model live (no restart).\n  - Pull / delete models from Ollama's cache.\n  - Tuning sliders for temperature / top_p / num_ctx (persisted).\n- AI ad-stripping: the model also returns a `CLEANED` body with newsletter\n  signups, podcast/app promos, and social follow asks removed. Falls back to\n  the original when the model can't produce a full body.\n\n### Search + filters\n- FTS5 full-text search; submitting from the topbar opens a dedicated results view.\n- Saved searches: persist a query as a sidebar entry.\n- Filter rules with `mark_read`, `star`, `hide`, `tag`, or `add_to_board` actions; eight match fields including feed, tags, `published_at`, and `has_image`; per-rule priority; Preview button counts last-7-day matches before save.\n- \"Mute\" popover in the reader actions adds a hide-by-keyword rule in one click.\n- Per-article user tags, with a `?tag=…` filter on the list endpoint.\n\n### Onboarding + organization\n- Five curated starter packs (Technology, Programming, Security, DevOps \u0026 Infra, World News).\n- OPML import/export. Optional scheduled OPML export to `/data/exports/`.\n- **Tiny Tiny RSS migration**: pull your subscriptions (recreating TT-RSS categories as folders) plus starred/archived articles from a running instance via its API, or upload an article export file. Already-subscribed feeds are skipped, so it's safe to re-run.\n- **Subscribe by URL**: paste either a feed URL or just the homepage. Ember follows `\u003clink rel=alternate\u003e` and probes common feed paths (`/feed`, `/rss`, `/atom.xml`, `/feed.xml`, `/index.xml`).\n- Drag-to-reorder feeds and folders.\n- Mark-all-read at view / feed / category scope.\n\n### Sign-in\n- Password (argon2id) by default.\n- **Passkeys / WebAuthn**: optional. Register from Settings → Passkeys; sign in with Touch ID / Face ID / hardware key. Requires `EMBER_PUBLIC_URL`.\n\n### Daily digest email\n- Opt-in nightly email summarizing your chosen view (Fresh / Today / Unread / Starred / Later).\n- Pick the hour + minute in UTC, optionally override the From / To address.\n- Configured via Settings → Daily digest. Requires the `EMBER_SMTP_*` env vars.\n\n### Notifications + auto-refresh\n- 15-second polling for new articles while the tab is visible (also fires on tab refocus).\n- Canvas-rendered favicon with a green notification dot when unread items arrive.\n- Page-title prefix `(N) Ember` so narrow tab strips show the count too.\n- Installed as a PWA, new articles trigger an OS-level numeric badge on the app icon.\n\n### Themes + branding\n- 8 themes: Auto (matches OS), Light, Dark, Solarized, Sepia, Nord, Gruvbox, High contrast.\n- Custom theme: pick 3 colors (paper/ink/ember); the rest is derived via CSS `color-mix()`.\n- Admin branding: app name, browser-tab title, favicon URL.\n\n### Admin\n- `ember probe` subcommand reports RAM/CPU/GPU and recommends a model.\n- Settings → Database: size, manual backup (VACUUM INTO), manual cleanup, schedules.\n- Schedules persist in `app_settings` and run via an hourly maintenance goroutine.\n- User management (create / update / delete / role).\n- Resummarize-all to re-process every article after a prompt or model change.\n\n### Other\n- Reading stats: today/week/30-day, totals, top feeds.\n- All confirmations use an in-app modal (no `window.confirm`).\n- Fever-compatible mobile clients via `/fever`.\n- WCAG 2.1 AA passes (axe-core via Playwright).\n- PWA: manifest + service worker (cache-first assets, network-first `/api`).\n\n## Configuration\n\n### Required environment\n\n| Var | Default | Purpose |\n|---|---|---|\n| `EMBER_SESSION_KEY` | _(required)_ | securecookie key (32+ bytes) |\n| `EMBER_ADMIN_PASSWORD` | _(required first run)_ | first-run admin password |\n\n### Optional environment\n\n| Var | Default | Purpose |\n|---|---|---|\n| `EMBER_ADDR` | `:8080` | listen address |\n| `EMBER_DB_PATH` | `/data/ember.db` | SQLite file |\n| `EMBER_ADMIN_USER` | `admin` | first-run admin username |\n| `EMBER_OLLAMA_URL` | `http://ollama:11434` | summarizer endpoint |\n| `EMBER_OLLAMA_MODEL` | `qwen2.5:0.5b` | initial model (admin can swap later) |\n| `EMBER_DISABLE_SUMMARIES` | `0` | skip LLM summarization entirely |\n| `EMBER_DISABLE_IMAGES` | `0` | drop article hero images at ingest |\n| `EMBER_FRESH_WINDOW` | `6h` | \"Fresh\" cutoff |\n| `EMBER_POLL_CONCURRENCY` | `8` | poller workers |\n| `EMBER_POLL_TICK` | `60s` | scheduler tick |\n| `EMBER_POLL_MIN_INTERVAL` | `30m` | per-feed fetch floor (\"check feeds every…\"), 5m–24h; Settings → Feed check interval overrides at runtime |\n| `EMBER_SESSION_TTL` | `24h` | session cookie lifetime (5m–90d); Settings → Sessions overrides at runtime |\n| `EMBER_LOG_LEVEL` | `info` | slog level |\n| `EMBER_TEST_MODE` | `0` | enables fake fetcher/summarizer for e2e |\n| `EMBER_PUBLIC_URL` | _(unset)_ | canonical `scheme://host` users hit; required to enable passkey sign-in |\n| `EMBER_ALLOW_PRIVATE_URLS` | `0` | bypass SSRF block to subscribe to RFC1918 / loopback feeds (only set if you trust every user who can add feeds) |\n| `EMBER_SECURE_COOKIES` | `1` | `Secure` flag on session + CSRF cookies; set `0` only for deliberate plain-HTTP deployments |\n| `EMBER_TRUSTED_PROXIES` | _(unset)_ | CIDRs/IPs of fronting proxy; `X-Real-IP` + `X-Forwarded-Proto` are honored only from these peers |\n| `EMBER_HSTS_PRELOAD` | `0` | append `; preload` to the HSTS header; only set after submitting the domain to the preload list |\n| `EMBER_SMTP_HOST` | _(unset)_ | SMTP host; required to enable daily-digest emails |\n| `EMBER_SMTP_PORT` | `587` | SMTP port |\n| `EMBER_SMTP_USER` | _(unset)_ | SMTP auth user (optional) |\n| `EMBER_SMTP_PASSWORD` | _(unset)_ | SMTP auth password |\n| `EMBER_SMTP_FROM` | _(unset)_ | digest `From:` address |\n| `EMBER_SMTP_STARTTLS` | `1` | enable STARTTLS on submission ports |\n| `EMBER_EMAIL_DOMAIN` | _(unset)_ | enable per-user newsletter inbox; host part of generated addresses (see [docs/email-inbox.md](docs/email-inbox.md)) |\n| `EMBER_EMAIL_LISTEN_ADDR` | `:2525` | inbound SMTP bind address for the newsletter inbox |\n| `EMBER_EMAIL_MAX_BYTES` | `26214400` | per-message size cap (25 MiB) |\n\n### Runtime-tunable settings (Settings UI)\n\nStored in the `app_settings` KV; persist across restarts:\n\n- Active LLM model + temperature / top_p / num_ctx\n- Branding (name, page title, favicon URL)\n- DB backup schedule (off | daily | weekly) + keep-N\n- DB cleanup schedule (off | weekly | monthly) + window in days\n- OPML export schedule (off | weekly | monthly)\n- Session cookie TTL (overrides `EMBER_SESSION_TTL`)\n- SMTP relay (host / port / user / password / from / STARTTLS) for the daily digest\n- Initial feed-backlog window (default 48h)\n\n## Local development\n\n```\nmake web-install     # one-time\nmake test            # go tests\nmake web-test        # vitest\nmake embed           # build SPA + copy to internal/web/dist\nmake build           # produce ./bin/ember\nEMBER_TEST_MODE=1 ./bin/ember   # listens on :8080 with the noop summarizer\n```\n\nHot reload for the SPA:\n```\ncd web \u0026\u0026 npm run dev      # vite dev server, proxies /api → :8080\nEMBER_TEST_MODE=1 ./bin/ember   # in another terminal\n# visit http://localhost:5173\n```\n\n## Mobile clients\n\nReeder, FeedMe, and other Fever-compatible apps can connect via `/fever`. The `api_key` is `md5(\"\u003cusername\u003e:\u003cuser_id\u003e\")` — see `/api/me` for your user_id. (We can't use the canonical `md5(\"user:pass\")` because passwords are stored only as argon2id hashes.)\n\n## E2E\n\n```\nmake embed build              # produce ./bin/ember with the SPA embedded\ncd web \u0026\u0026 npx playwright install chromium\nnpx playwright test           # spawns the binary in test mode against a temp DB\n```\n\nIn 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.\n\n## Database\n\nSQLite 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.\n\nMigration files live under `internal/db/migrations/` and are embedded into the binary.\n\n## Architecture\n\nSee `docs/architecture.md` for the request lifecycle, poller state machine, and summarizer pipeline.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrandonhon%2Fember","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbrandonhon%2Fember","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrandonhon%2Fember/lists"}