Skip to content
This repository was archived by the owner on Jun 22, 2026. It is now read-only.

boringcode-dev/feedreader

Repository files navigation

feedreader

A tiny, fast, self-hosted feed reader for engineering and research signals.

Server-rendered UI · SQLite storage · Scheduled refresh · Docker-friendly · Private by default

Go 1.24 SQLite Docker ready Server rendered UI License: MIT


Screenshot

feedreader home screen


Features

  • 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 more appends 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/repo form 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, All stays 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/items responses 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

Why feedreader?

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

Tech stack

Backend

  • Go
  • net/http
  • html/template
  • modernc.org/sqlite
  • goquery

Frontend

  • Server-rendered HTML
  • Vanilla JavaScript
  • Plain CSS

Storage

  • SQLite

Deployment

  • Docker
  • Reverse proxy compatible

Architecture

At a high level:

  1. source adapters fetch upstream content
  2. items are upserted into SQLite by (source, external_id)
  3. the web app reads stored items ordered by article date descending
  4. 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

Project structure

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

Getting started

Prerequisites

  • Go 1.24+ for host-native builds
  • or Docker for containerized development/testing and image-based runs

Run locally with Go

go run ./cmd/feedreader serve --host 127.0.0.1 --port 8080

Then 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 8080

Run locally with Dockerized Go

Use 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 8080

Then 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 8080

Run tests

Host-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 ./...

On-demand refresh

Host-native:

go run ./cmd/feedreader fetch

Dockerized Go toolchain:

docker run --rm -v "$PWD":/src -w /src golang:1.24-bookworm go run ./cmd/feedreader fetch

Docker image build

docker build -t feedreader .

GitHub Actions release tags also publish a multi-arch image to GHCR:

docker pull ghcr.io/boringcode-dev/feedreader:latest

Docker image run

docker run --rm -p 8080:8080 -v $(pwd)/data:/data feedreader

Then open:

  • http://127.0.0.1:8080

Configuration

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

Scheduling

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/refresh for triggering an immediate upstream source refresh

API

GET /healthz

Returns service health and per-source refresh status.

GET /api/items

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 the All view scoped to enabled sources (for example hackernews,github)
  • q — optional case-insensitive search query across title, summary, author, URL host/path, and stored metadata
  • limit — page size
  • offset — pagination offset

POST /api/refresh

Triggers an immediate upstream refresh across all sources and returns per-source outcomes.


Data model

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

UI behavior

Search

  • the search control expands inline in the header
  • clicking the search icon focuses the input
  • the input renders at 16px to 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

Loading and empty states

  • 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 more disables itself while an append request is in flight and hides itself when the current result set has no further page

Offline and connectivity

  • the app shell and previously fetched GET /api/items views 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 localhost or 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/items response yet, the list is replaced with Offline 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

Reader settings dialog

  • the configure button opens a Reader settings dialog
  • theme is configured in the dialog instead of a dedicated header toggle
  • the dialog sections are ordered as:
    • Theme
    • UI density
    • Sources
  • 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 localStorage under feedreader.sources
  • selected density is stored in localStorage under feedreader.uiDensity
  • selected theme is stored in localStorage under feedreader.theme
  • source-specific filters render as real icon-only buttons
  • All remains 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 All view aggregates only over the enabled source set, not over disabled sources

Roadmap

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

Contributing

Contributions are welcome.

A good contribution flow:

  1. fork the repository
  2. create a branch
  3. make changes
  4. run formatting and tests
  5. 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/CD

  • CI runs on pull requests and main pushes.
  • CI checks gofmt formatting and go test ./....
  • CD publishes ghcr.io/boringcode-dev/feedreader on v*.*.* tag pushes.
  • Published release images include linux/amd64 and linux/arm64 variants and update the latest tag.

Security

For security concerns, please email hi@boringcode.dev instead of using the issue tracker. See SECURITY.md for more details.


License

This project is licensed under the MIT License — see the LICENSE file for details.


Repository hygiene

The SQLite runtime data directory is intentionally ignored:

data/

This keeps the repository focused on source code and assets.

About

A tiny, fast, self-hosted feed reader for engineering and research signals.

Resources

License

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors