A tiny, fast, Cloudflare-native feed reader for engineering and research signals.
Server-rendered UI · D1 storage · Scheduled refresh · Cloudflare Workers
- Multi-source feed aggregation
- Hacker News
- GitHub Trending
- Hugging Face Trending Papers
- alphaXiv Explore
- Persistent edge storage with Cloudflare D1
- 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
- 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
- Cloudflare-native deployment with Workers, D1, Cron Triggers, and Workers Static Assets
feedreader is designed for people who want a small, understandable reader instead of a large feed platform.
It optimizes for:
- simple operations
- low infrastructure overhead
- straightforward data ownership
- easy extension when adding more sources
- TypeScript
- Cloudflare Workers
- Web Standard APIs
linkedom
- Server-rendered HTML
- Vanilla JavaScript
- Plain CSS
- Cloudflare D1
- Cloudflare Workers
- Cron Triggers
- Workers Static Assets
At a high level:
- source adapters fetch upstream content
- items are upserted into D1 by
(source, external_id) - the Worker reads stored items ordered by article date descending
- the cron trigger refreshes hourly on wall-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
core/ platform-agnostic domain logic, source adapters,
refresh orchestration, SSR rendering
platforms/cloudflare/ Worker entrypoint, D1 repository, migrations,
wrangler config
web-static/ CSS, JS, icons, manifest, service worker
docs/assets/ README screenshots and supporting images
- Node.js 22+
- npm
- a Cloudflare account for remote deployment
npm install
npm run db:migrate:local
npm run devThen open:
http://127.0.0.1:8788
npm run typecheck
npm testnpm install
wrangler login
wrangler d1 create feedreader
# copy the returned database_id into platforms/cloudflare/wrangler.toml
wrangler secret put REFRESH_SECRET --config platforms/cloudflare/wrangler.toml
npm run db:migrate:remotenpm run deployIf you are reproducing this deployment in another Cloudflare account, bind the Worker to a custom domain in Cloudflare or use the generated *.workers.dev hostname.
Bindings configured in platforms/cloudflare/wrangler.toml:
DB— D1 database bindingASSETS— Workers Static Assets binding forweb-static/SELF— service binding back to the same Worker for per-source refresh fan-out
Runtime configuration:
| Variable | Default | Description |
|---|---|---|
FEEDREADER_ITEMS_PER_SOURCE |
20 |
Per-source item count used in source dashboard/health contexts |
FEEDREADER_USER_AGENT |
feedreader/0.1 |
Outbound fetch user agent |
REFRESH_SECRET |
required secret | Shared secret for internal per-source refresh fan-out routes |
The scheduler runs through a Cloudflare Cron Trigger.
Behavior:
- runs hourly via
0 * * * * - does not perform an immediate refresh just because a deploy completed
- fans out one fresh Worker invocation per source to stay within free-tier CPU budgets
On-demand refresh is available through:
- the header refresh button for re-fetching the current backend-stored feed view
POST /api/refreshfor triggering an immediate upstream source refresh across all sources
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.
Internal fan-out route used by the Worker itself. It requires the X-Refresh-Secret header and is not intended for external callers.
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) - 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 - 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
- more source-specific parsing fixtures
- export/import support
Contributions are welcome.
A good contribution flow:
- fork the repository
- create a branch
- make changes
- run verification locally
- open a pull request
Example local verification:
npm run typecheck
npm test
npm run db:migrate:local- CI runs on pull requests and
mainpushes. - CI checks type safety, unit tests, and a local D1 migration smoke run.
- Deploy runs on
mainpushes and manual dispatch. - Deploy validates Cloudflare credentials, re-runs verification, applies remote D1 migrations, and deploys the Worker.
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.
Generated runtime artifacts are intentionally ignored:
node_modules/
.wrangler/
.dev.vars
.envrc
*.logThis keeps the repository focused on source code, static assets, and documentation.

