Build Feedbin
YESreplaces $7/mosaves $84/yrback to the verdict
A feed reader whose real work is being a well-behaved HTTP client at scale: conditional requests so unchanged feeds cost nothing, backoff that respects what servers ask for, parsing feeds that violate their own specifications, and read state that survives a feed rewriting its own entries.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the fetcher and the application. Either is fine, since the work is HTTP and text.
Get it Your package manager, or nvm and rbenv respectively.
Verify
The runtime version prints and a HTTP client library installs - installfree locally
Why Entries, revisions and read state with correct unread counts want a relational database with real indexes.
Get it Docker or your package manager. open ↗
Verify
psql connects and full text search returns a result - have readyfree
Why Every parsing decision in phase 1 is driven by how real feeds actually misbehave, and no synthetic fixture reproduces that.
Get it Collect feeds you actually read, plus deliberately a few from old software with undeclared encodings or invalid dates. Save the raw bytes as fixtures.
Verify
Twenty raw feeds are saved and at least three fail strict validation - installfree
Why Conditional requests, rate limiting and backoff cannot be tested against real servers without being exactly the nuisance this build is trying not to be.
Get it Any small server that can return validators, not-modified responses, error statuses and explicit wait instructions on demand.
Verify
It can return a not-modified response and a rate-limited response with a wait instruction - have readyfree
Why Phase 5 needs a real hub, since the verification handshake and signature format are where implementations differ from the specification.
Get it Find a feed advertising a hub, or run an open source hub locally.
Verify
A subscription is established and a push is received
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- feeds(id, url, site_url, title, etag, last_modified, last_fetched_at, next_fetch_at, failure_count, hub_url, disabled_at) - entries(id, feed_id, guid, url, title, content, published_at, content_hash, first_seen_at, updated_at) - revisions(entry_id, ordinal, content_hash, content, at) · because publishers edit articles after posting - subscriptions(user_id, feed_id, folder, created_at) - reads(user_id, entry_id, read_at, source) · source is manual or auto - fetches(feed_id, at, status, bytes, duration_ms, outcome) · outcome is not_modified, updated, error or rate_limited - The fetch log is what tells you whether you are a good citizen, and it is worth keeping even though it is the largest table.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_URLsecret | required | postgres://localhost:5432/reader | Feeds, entries and read state. |
USER_AGENT | required | MyReader/1.0 (+https://example.com/bot) | Identifies your client and gives a route to reach you. Anonymous clients get blocked. |
MIN_FETCH_INTERVAL_MINUTES | required | 15 | The floor between fetches of one feed, regardless of what the schedule computes. |
MAX_FAILURES_BEFORE_DISABLE | required | 20 | After this a feed is disabled and surfaced. Retrying a dead site forever helps nobody. |
CALLBACK_BASE_URL | optional | https://reader.example.com/websub | Phase 5 only. Where hubs deliver pushes. Must be reachable from outside. |
ENTRY_BODY_RETENTION_DAYS | required | 365 | Bodies are trimmed after this. Identities and read state are kept indefinitely. |
The build, in order
Parsing feeds that do not follow the rules
Real feeds from real sites parse into entries.
Real feeds violate their specifications. Refusing them is refusing the web.
Files
src/parse.tsFeed identifier, then link, then content hash. Record which was used.
Files
src/identity.tssrc/identity.test.tsIncluding broken ones. Synthetic feeds are always better formed than real ones.
Files
fixtures/feeds/
done when · tick each as it passeswatch out- Deriving an entry's identity from its title or its content means a publisher fixing a typo creates what looks like a new entry, and the user sees the same article again. Prefer the feed's own identifier, fall back to the link, and only then to a content hash, recording which was used so the behaviour is explicable.
Fetching without being a nuisance
An unchanged feed costs one small request and no bandwidth.
Both kinds a server may give you, sent back on the next request.
Files
src/fetch.tsA user agent with a name and a way to reach you. Anonymous crawlers get blocked.
Files
src/fetch.tsThe not-modified ratio is how you know whether your client behaves.
Files
src/fetches.ts
done when · tick each as it passeswatch out- Downloading the full feed every poll is how a reader gets blocked, and at any scale it is a meaningful cost imposed on someone else's server for nothing. Store the validators the server gives you and send them back, and treat a high ratio of full downloads to not-modified responses as a defect in your client rather than in the feeds.
Scheduling and backing off
Feeds are polled at a sensible rate and problems reduce it rather than continuing.
It outranks your own schedule. Ignoring it is how you get blocked permanently.
Files
src/schedule.tsJitter is what stops every feed retrying at the same instant.
Files
src/backoff.tssrc/backoff.test.tsAnd tell the user. Silent dead feeds are how a reader quietly stops working.
Files
src/health.ts
done when · tick each as it passeswatch out- Retrying a failing feed on the same schedule forever turns one dead site into thousands of pointless requests, and retrying every feed on the same clock produces a thundering burst at the top of every hour. Back off with jitter, honour an explicit wait instruction over your own schedule, and stop entirely after persistent failure.
Reading, and read state that stays correct
What you have read stays read, even when publishers edit their articles.
The version you read should still exist after the publisher edits it.
Files
src/revisions.tsSo an edit updates rather than resurrecting.
Files
src/reads.tsA full recount at ten thousand entries is felt every time.
Files
src/counts.ts
done when · tick each as it passeswatch out- Overwriting an entry when its content changes loses the version you read and, if the identity is content-derived, marks it unread again. Store a revision and keep the read state attached to the identity, so an edited article shows as updated rather than as new.
Push, for feeds that offer it
Feeds advertising a hub arrive instantly and stop being polled.
The verification handshake is what proves you asked for it.
Files
src/websub.tsAn unauthenticated push is content injection into a user's reader.
Files
src/websub.tssrc/websub.test.tsA lapsed subscription must degrade to polling, not to silence.
Files
src/schedule.ts
done when · tick each as it passeswatch out- Trusting an unauthenticated push means anyone who learns your callback address can inject entries into a user's reader, which is a content injection vector with your name on it. Verify the signature where the hub provides one, and confirm pushed content against the feed before trusting it.
Search, import and getting outproduct builder
You can find an article from a year ago and take your subscriptions elsewhere.
Feed and date, combined with text. A year of entries should search instantly.
Files
src/search.tsBoth directions, with folders. A reader you cannot leave is one to avoid.
Files
src/opml.tsRead state is small and its loss resurrects old articles.
Files
src/retention.ts
done when · tick each as it passeswatch out- Deleting old entries to control database size also deletes the read state attached to them, so a feed that still lists a two-year-old article will present it as unread the next time it is fetched. Trim the body and keep the identity and read state, which are small.
That is the whole plan for Feedbin. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Newsletter ingestion by email address.
- Article extraction for summary-only feeds.
- Mobile and offline clients.
- Feed fetching as a distributed system.
- publisher-specific import reliability
- citation graph scale
- team libraries and institutional access
- resilient polling of malformed and rate-limited feeds
- licensed scholarly metadata
- Filters that hide entries matching a rule, which is what makes a high-volume feed usable
- Saved searches as feeds of their own
- A shared read state API, once the counts are provably correct
Need the files? The project pack on the verdict page hands your agent the whole brief · more rss & research.