A tiny, fast, self-hosted feed reader for engineering and research signals.
Server-rendered UI · SQLite storage · Scheduled refresh · Docker-friendly · Private by default
- Multi-source feed aggregation
- Hacker News
- GitHub Trending
- Hugging Face Papers Trending
- alphaXiv Explore
- Persistent local storage with SQLite
- Incremental fetch model that keeps older items in the database
- Server-backed incremental loading: first page loads 12 items, first-load bootstrap/filter/search/refresh show a toast-based loading state, and
View moreappends more items in place - Source-aware card summaries
- Hacker News cards show points and comments
- GitHub cards show stars, today's stars, and forks
- Hugging Face cards show upvotes
- alphaXiv cards show likes
- Responsive, minimalist UI with:
- source filters
- real source icons in filters, dialog rows, and card metadata
- RSS-based app icon/favicon branding
- dark/light mode
- inline expanding search
- refresh control
- source configuration dialog
- Configurable visible sources stored in
localStorage- choose which source buttons are shown
- when 2+ sources are enabled,
Allstays visible and aggregates over the enabled set - when exactly 1 source is enabled, only that source button is shown
- Debounced client-side search UX backed by the server API
- Explicit empty states for no-result source filters and searches
- Connectivity toasts for internet disconnect/reconnect events
- Scheduled refresh every 3 hours on wall-clock boundaries in UTC+7
- Manual refresh from the UI updates the current feed list in place without a full page reload and shows a toast-based loading state while refresh + refetch are running
- Persisted visited-link dimming for feed card titles across reload/reopen using local storage
- PWA-ready assets and offline caching including manifest, service worker, touch icons, cached shell assets, and cached
/api/itemsresponses for previously visited views - Reconnect list refresh re-fetches the current view from backend stored items only; it does not refresh upstream sources
- Docker deployment with reverse-proxy-friendly HTTP service
feedreader is designed for people who want a small, understandable, self-hosted reader instead of a large feed platform.
It optimizes for:
- simple operations
- low memory usage
- straightforward data ownership
- easy extension when adding more sources
- Go
net/httphtml/templatemodernc.org/sqlitegoquery
- Server-rendered HTML
- Vanilla JavaScript
- Plain CSS
- SQLite
- Docker
- Reverse proxy compatible
At a high level:
- source adapters fetch upstream content
- items are upserted into SQLite by
(source, external_id) - the web app reads stored items ordered by article date descending
- the scheduler refreshes on 3-hour clock boundaries
Key properties:
- old items are retained in the database
- fetch failures do not wipe existing data
- sources without a native article date fall back to the initial fetch time (
first_seen_at) for ordering - later refreshes preserve the original published/fetched ordering timestamps for existing items
cmd/feedreader/ CLI entrypoint
internal/config/ configuration loading
internal/db/ SQLite bootstrap and pragmas
internal/domain/ domain models
internal/repository/ persistence layer
internal/service/ refresh orchestration and scheduler
internal/sources/ upstream source adapters
internal/web/ HTTP handlers and page rendering
web/templates/ HTML templates
web/static/ CSS, JS, icons, PWA assets
docs/assets/ README screenshots and supporting images
Host-level implementation notes for this deployment live at:
~/.hermes/implementations/2026-06-18_feedreader-service-implementation.md
- Go 1.24+ for local builds
- or Docker for containerized usage
go run ./cmd/feedreader serve --host 0.0.0.0 --port 8080Then open:
http://127.0.0.1:8080
go run ./cmd/feedreader fetchdocker build -t feedreader .docker run --rm -p 8080:8080 -v $(pwd)/data:/data feedreaderThen open:
http://127.0.0.1:8080
Environment variables:
| Variable | Default | Description |
|---|---|---|
FEEDREADER_DB_PATH |
./data/feedreader.db |
SQLite database path |
FEEDREADER_REFRESH_INTERVAL_HOURS |
3 |
Refresh interval setting used by the scheduler |
FEEDREADER_ITEMS_PER_SOURCE |
20 |
Per-source item count used in source dashboard/health contexts |
FEEDREADER_REQUEST_TIMEOUT_SECONDS |
20 |
Upstream request timeout |
FEEDREADER_USER_AGENT |
feedreader/0.1 |
Outbound fetch user agent |
FEEDREADER_HOST |
0.0.0.0 |
HTTP bind host |
FEEDREADER_PORT |
8080 |
HTTP bind port |
The scheduler runs inside the app process.
Behavior:
- aligned to UTC+7 (
Asia/Ho_Chi_Minh) - runs on the next 3-hour wall-clock boundary
- does not perform an immediate refresh just because the container starts
Manual refresh is also available through the UI and CLI.
Returns service health and per-source refresh status.
Returns feed items for incremental loading.
Query params:
source— optional source filter (hackernews,github,huggingface,alphaxiv)sources— optional comma-separated aggregate source set used when the client wants theAllview scoped to enabled sources (for examplehackernews,github)q— optional case-insensitive search query across title, summary, author, URL host/path, and stored metadatalimit— page sizeoffset— pagination offset
The service stores a cumulative feed history.
Each fetch:
- upserts items by
(source, external_id) - updates refresh state in
sync_state - preserves older items already in the database
The UI/API render items from the full stored set, ordered by article date descending.
Presentation-layer note:
- the source adapters persist raw metadata into
metadata_json - the card-building layer turns that metadata into user-visible summary lines
- current rendered metrics are:
- Hacker News: points and comments
- GitHub: stars, today, forks
- Hugging Face Papers: upvotes
- alphaXiv: likes
- source icons are not embedded in the brief text itself
- the current card layout renders the real source icon inline before the host/domain line
- the search control expands inline in the header
- clicking the search icon focuses the input
- the input renders at
16pxto avoid common iOS Safari auto-zoom behavior - typing is debounced before hitting the API
- closing the search control clears the query and resets the feed
- first-load bootstrap queries, source filter changes, searches,
View more, and manual refresh all show an explicit toast-based loading state - source-filter and search requests that return zero items replace the list with an empty-state message instead of leaving stale cards on screen
View moredisables itself while an append request is in flight and hides itself when the current result set has no further page
- the app shell and previously fetched
GET /api/itemsviews are cached by the service worker for offline reuse - this offline/PWA behavior requires a secure-context origin where service workers are available (for example
localhostor HTTPS); plain HTTP network IP origins such ashttp://100.94.224.102:9002do not get service-worker-based offline reopen support on iOS - when the browser goes offline, a no-wifi indicator appears before the refresh button instead of showing connectivity toasts
- if an offline view has no cached
/api/itemsresponse yet, the list is replaced withOffline and no cached items are available for this view yet. - when the browser comes back online, the no-wifi indicator disappears and the current view is re-fetched silently from
/api/items - reconnect refreshes backend-stored items only; the only UI path that calls
POST /api/refreshremains the manual refresh button
- the configure button opens a dialog that lets the user choose visible sources
- selected sources are stored in
localStorageunderfeedreader.sources - source-specific filters render as real icon-only buttons
Allremains a text button- the source dialog renders real source icons before each source name
- if 2 or more sources are enabled, the filter bar shows:
All- each enabled source
- if exactly 1 source is enabled, the filter bar shows only that source
- the
Allview aggregates only over the enabled source set, not over disabled sources
Potential next improvements:
- more sources (blogs, changelogs, newsletters, papers)
- server-side pagination
- source weighting and ranking controls
- source-specific parsing tests with fixtures
- export/import support
Contributions are welcome.
A good contribution flow:
- fork the repository
- create a branch
- make changes
- run formatting and tests
- open a pull request
Example local verification:
gofmt -w $(find . -name "*.go")
go test ./...The SQLite runtime data directory is intentionally ignored:
data/This keeps the repository focused on source code and assets.
