A self-hosted, headless RSS sync backend on Cloudflare Workers + D1.
HeadRSS is built with these principles in mind:
- Decoupled: feed fetching and sync are separate. The fetcher runs on your machine, the sync server on Cloudflare, connected only by HTTP.
- Extensible: the fetch pipeline is open to customization (e.g. full-text extraction) without touching the sync server.
- Data ownership: your subscriptions, read state, and content stay in your own database.
- Client freedom: you can use any Google Reader-compatible app (Reeder, NetNewsWire, etc.) on any platform.
- Google Reader API for RSS client sync
- Native REST API with OpenAPI spec for web frontends
- OPML import/export
- Authenticated feed support (Basic Auth, Bearer token, custom headers), encrypted at rest
- Multi-user with per-device app passwords
- Automatic exponential backoff for failing feeds
- Separate CLI fetcher that runs on your machine via cron
- CLI for admin and subscription management
- Runs on Cloudflare free tier for personal use
HeadRSS consists of two components:
- Worker: Cloudflare Workers + D1. Handles API requests and stores all data (feeds, items, users, read state).
- CLI (
headrss): runs on your machine. Works as:- Fetcher: fetches RSS/Atom feeds on a cron schedule and pushes items to the Worker.
- Admin/user client: manages users, feeds, subscriptions, folders, and OPML.
┌─────────────────────────────────┐ ┌──────────────────────────────────────┐
│ Server │ │ Cloudflare (Worker + D1) │
│ │ │ │
│ CLI (headrss): │ │ API endpoints: │
│ headrss feed fetch │ │ /api/google/* ← RSS clients │
│ ├─ GET /admin/feeds ─────┼────▶│ /api/native/* ← web frontend │
│ ├─ fetch RSS/Atom feeds │ │ /ingest/* ← fetcher push │
│ └─ POST /ingest/items ─────┼────▶│ /admin/* ← admin/fetcher │
│ │ │ │ │
│ headrss feed purge ─────┼────▶│ ▼ │
│ │ │ D1 (SQLite) │
│ headrss admin user/feed/opml │ │ │
│ headrss subscription/folder │ │ │
│ │ │ │
└─────────────────────────────────┘ └──────────────────────────────────────┘
See Architecture for details.
| Package | Description |
|---|---|
@headrss/core |
Domain types, port interfaces, commands, queries, Zod schemas |
@headrss/adapter-d1 |
D1 implementation of EntryStore + CredentialStore |
@headrss/adapter-greader |
Google Reader protocol adapter (Hono sub-app) |
@headrss/adapter-api |
Native REST API adapter (Hono sub-app, OpenAPI) |
@headrss/worker |
Cloudflare Worker entry point — wires everything together |
@headrss/cli |
CLI tool for admin + feed fetching (Bun runtime) |
pnpm install
pnpm build# Create D1 database
cd packages/worker
npx wrangler d1 create headrss
# Copy wrangler.toml.example and fill in your database_id
cp wrangler.toml.example wrangler.toml
# Apply migrations
npx wrangler d1 migrations apply headrss --local # local dev
npx wrangler d1 migrations apply headrss --remote # production
# Set secrets
npx wrangler secret put TOKEN_KEY
npx wrangler secret put CREDENTIAL_KEY
npx wrangler secret put INGEST_API_KEY
npx wrangler secret put FETCH_API_KEY
npx wrangler secret put ADMIN_API_KEY
# Deploy
npx wrangler deployAll configuration is via environment variables:
# Required
export HEADRSS_URL="https://headrss.your-domain.com"
# Admin operations
export ADMIN_API_KEY="your-admin-key"
# Feed fetching
export FETCH_API_KEY="your-fetch-key"
export INGEST_API_KEY="your-ingest-key"
# Per-user operations (or use `headrss login`)
export HEADRSS_USER="your-username"
export HEADRSS_PASSWORD="your-app-password"Optional tuning:
| Variable | Default | Description |
|---|---|---|
FETCH_CONCURRENCY |
8 |
Max parallel feed fetches |
FETCH_INTERVAL |
900 |
Seconds between fetches per feed |
FETCH_TIMEOUT |
30 |
HTTP request timeout (seconds) |
RETENTION_DAYS |
90 |
Item retention for purge |
LOG_LEVEL |
info |
debug, info, warn, error |
See Setup Guide and Configuration for details.
# Create a user
headrss admin user add alice
# Create an app password (displays the plaintext password once)
headrss admin password add <userId> --label "CLI"
# Login (caches token locally)
headrss login# Import from OPML file (as logged-in user)
headrss subscription import subscriptions.opml
# Export subscriptions as OPML
headrss subscription export -o subscriptions.opml
# Or add individually
headrss subscription add https://example.com/feed.xml
headrss subscription add https://example.com/feed.xml --folder Technology# Fetch all due feeds (one-shot, for cron)
headrss feed fetch
# Dry run — show what would be fetched
headrss feed fetch --dry-run
# Purge old items (keeps starred and explicitly unread)
headrss feed purge*/15 * * * * headrss feed fetch
0 3 * * * headrss feed purgeheadrss subscription list
headrss subscription add <url>
headrss subscription rm <id>
headrss subscription import <file>
headrss subscription export [-o <file>]
headrss folder list
headrss folder add <name>
headrss folder rm <id>headrss admin user list
headrss admin user add <username>
headrss admin user rm <id>
headrss admin password list <userId>
headrss admin password add <userId>
headrss admin password rm <id>
headrss admin feed list
headrss admin feed rm <id>
headrss admin opml export <userId>
headrss admin opml import <file> --user <id>See CLI Reference for details.
Point your RSS client at the Worker URL with the Google Reader base path:
https://headrss.your-domain.com/api/google
Login with your username and an app password. Tested with:
- Reeder (iOS/Mac)
- NetNewsWire (Mac/iOS)
| Path | Auth | Description |
|---|---|---|
/api/google/* |
ClientLogin token | Google Reader-compatible API |
/api/native/v0/* |
Bearer token | Native REST API (OpenAPI) |
/ingest/* |
INGEST_API_KEY |
Feed item ingestion |
/admin/* |
FETCH_API_KEY / ADMIN_API_KEY |
System administration |
/health |
None | Health check |
/api/openapi.json |
None | OpenAPI spec |
See API Reference and Authentication for details.
# Install dependencies
pnpm install
# Build all packages
pnpm build
# Run tests
pnpm test
# Type check
pnpm typecheck
# Lint
pnpm lint
# Format
pnpm format
# Local dev server
cd packages/worker && npx wrangler dev
# Run CLI in development
bun packages/cli/src/index.ts --help# Build
cd packages/cli
bun build --compile src/index.ts --outfile headrss
# Use it directly
./headrss feed fetch
# Or, copy to bin
cp headrss ~/.local/bin/MIT