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
- GitHub repo titles are normalized to canonical
owner/repoform from the repo URL path - Hugging Face cards show upvotes
- alphaXiv cards show likes
- published/fetched dates are formatted in the browser locale while preserving the stored UTC calendar date
- 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
- reader settings dialog for theme, density, and source visibility
- 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 indicator that shows a no-wifi icon while offline and silently refreshes the current view when the browser reconnects
- Scheduled refresh every 1 hour on wall-clock boundaries in UTC+7
- Manual refresh from the header re-fetches the current feed view from backend stored items only; it does not re-fetch upstream sources
- 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 1-hour clock boundaries by default
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 host-native builds
- or Docker for containerized development/testing and image-based runs
go run ./cmd/feedreader serve --host 127.0.0.1 --port 8080Then open:
http://127.0.0.1:8080
To keep local dev data separate from the default SQLite file:
FEEDREADER_DB_PATH="$(pwd)/tmp/feedreader-dev.db" go run ./cmd/feedreader serve --host 127.0.0.1 --port 8080Use this when you do not want to install Go on the host.
docker run --rm -p 18080:8080 -v "$PWD":/src -w /src golang:1.24-bookworm go run ./cmd/feedreader serve --host 0.0.0.0 --port 8080Then open:
http://127.0.0.1:18080
To keep containerized dev data separate from the default SQLite file:
docker run --rm -p 18080:8080 -e FEEDREADER_DB_PATH=/src/tmp/feedreader-dev.db -v "$PWD":/src -w /src golang:1.24-bookworm go run ./cmd/feedreader serve --host 0.0.0.0 --port 8080Host-native:
gofmt -w $(find . -name "*.go")
go test ./...Dockerized Go toolchain:
docker run --rm -v "$PWD":/src -w /src golang:1.24-bookworm go test ./...Host-native:
go run ./cmd/feedreader fetchDockerized Go toolchain:
docker run --rm -v "$PWD":/src -w /src golang:1.24-bookworm go run ./cmd/feedreader fetchdocker build -t feedreader .GitHub Actions release tags also publish a multi-arch image to GHCR:
docker pull ghcr.io/boringcode-dev/feedreader:latestdocker 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 |
1 |
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 N-hour wall-clock boundary based on
FEEDREADER_REFRESH_INTERVAL_HOURS(default: 1 hour) - does not perform an immediate refresh just because the container starts
On-demand refresh is available through:
- the header refresh button for re-fetching the current backend-stored feed view
- the CLI and
POST /api/refreshfor triggering an immediate upstream source refresh
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
Triggers an immediate upstream refresh across all sources and returns per-source outcomes.
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 and only triggers the search API once the query reaches at least 2 characters
- closing the search control clears the query and resets the feed only when an active query exists
- closing an empty visible search box just hides the control and does not refetch
/api/items
- first-load bootstrap queries, source filter changes, searches,
View more, and header refresh all show an explicit toast-based loading state - source-filter changes use the generic loading toast text
Loading feed… - 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 as `http://100.94.224.102:9[...] - when the browser goes offline, a no-wifi indicator appears in the header action row 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; it does not trigger upstream source refetches
- the configure button opens a
Reader settingsdialog - theme is configured in the dialog instead of a dedicated header toggle
- the dialog sections are ordered as:
ThemeUI densitySources
- theme and density options are shown in a 2-column layout to reduce dialog height
- clicking the dialog backdrop closes it
- background page scrolling is locked while the dialog is open
- selected sources are stored in
localStorageunderfeedreader.sources - selected density is stored in
localStorageunderfeedreader.uiDensity - selected theme is stored in
localStorageunderfeedreader.theme - source-specific filters render as real icon-only buttons
Allremains a text button- the source dialog renders real source icons before each source name
- density options are:
Comfortable(default)Compact
- 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 ./...Example Dockerized verification:
docker run --rm -v "$PWD":/src -w /src golang:1.24-bookworm go test ./...- CI runs on pull requests and
mainpushes. - CI checks
gofmtformatting andgo test ./.... - CD publishes
ghcr.io/boringcode-dev/feedreaderonv*.*.*tag pushes. - Published release images include
linux/amd64andlinux/arm64variants and update thelatesttag.
For security concerns, please email hi@boringcode.dev instead of using the issue tracker. See SECURITY.md for more details.
This project is licensed under the MIT License — see the LICENSE file for details.
The SQLite runtime data directory is intentionally ignored:
data/This keeps the repository focused on source code and assets.
