Skip to main content

Module config

Module config 

Source
Expand description

Runtime configuration for the FeatherReader server.

Everything is env-driven with a sane default for every knob, so a bare ./featherreader boots and works — no config file required (the “trivial to self-host” promise). The environment variables all share the FEATHERREADER_* prefix:

VariableDefaultMeaning
FEATHERREADER_BIND127.0.0.1:8080host:port the HTTP server binds.
FEATHERREADER_DBfeatherreader.dbPath to the SQLite cache file.
FEATHERREADER_PUBLIC_URLhttp://localhost:8080Externally-reachable base URL (OAuth callback + client metadata).
FEATHERREADER_ALLOWED_DIDS(empty = open)Comma-separated login allow-list of atproto DIDs.
FEATHERREADER_POLL_INTERVAL3600 (1h)Default per-feed poll interval, in seconds. At least 1; 0 is refused at startup.
FEATHERREADER_PUBLICATION_READ_DEADLINE_SECS30The longest one standard.site publication read may take before it is a failure. Under Fly’s 45 s kill_timeout, so the read in flight at shutdown can finish. Must be at least 1.
FEATHERREADER_STARTUP_DELAY_SECSunsetShortens every background loop’s delay before its FIRST tick (30/45/60/75/90 s, and 5 min for the relay probe). A ceiling: a larger value changes nothing and says so in the log. For dev loops and integration runs; production wants the built-in values. Read in scheduler.rs, listed here because this table is where an operator looks.
FEATHERREADER_RETENTION_HARD_DAYS180Absolute ceiling: entries older than this go regardless of starred/unread. The bound that keeps one reader’s pins from filling a shared cache and stalling the poller. 0 removes the ceiling — the ONLY bound on pinned entries, so 0 here means the cache is unbounded. Must be STRICTLY GREATER than the window below, or 0: a ceiling inside the window would delete the rows the window spares, so it cannot be applied, and startup REFUSES the pair rather than silently running unbounded.
FEATHERREADER_RETENTION_DAYS14Evict READ, UNSTARRED entries older than this. Starred and unread entries survive this window but not the hard ceiling above. 0 disables this rolling window ONLY; the ceiling still applies. Set BOTH to 0 for no eviction at all.
FEATHERREADER_PUBLICATION_RETENTION_DAYS3650Absolute ceiling for entries of a kind the rolling window does not apply to — a standard.site publication. Publications are bounded by COUNT (max_entries_per_feed) instead of by age, because measurement says a 14-day window stores NOTHING from a real publication: the newest documents on three of them were 109 to 241 days old. This is the “not immortal” backstop, not the space bound. 0 disables it.
FEATHERREADER_PROXY_IMAGESfalseProxy feed images so reader IPs aren’t leaked to feed hosts.
FEATHERREADER_TRUSTED_IP_HEADER(unset)Trusted reverse-proxy header for the real client IP (e.g. Fly-Client-IP, CF-Connecting-IP). Unset trusts the socket peer only.
FEATHERREADER_MAX_SUBS_PER_DID500Per-DID subscription cap.
FEATHERREADER_MAX_FEEDS10000Global distinct-feed ceiling.
FEATHERREADER_MAX_ENTRIES_PER_FEED2000Per-feed retained-entry cap (newest N).
FEATHERREADER_DB_SIZE_WATERMARK_BYTES2 GiBAbove this the poller stops fetching new content (0 disables).
FEATHERREADER_RESOLVER_HOSThttps://bsky.socialatproto handle-resolver base (com.atproto.identity.resolveHandle) the pre-handshake beta gate uses to honor an existing seat on a cookie-less login.
FEATHERREADER_BOT_SECRET(unset = /bot/claims disabled)Shared bearer secret (X-Bot-Secret) gating the headless follow→invite bot’s mint endpoint POST /bot/claims. Unset ⇒ endpoint returns 503. MUST be set (strong) on a production-like instance if the bot is used.
FEATHERREADER_CLAIM_TTL_SECS1209600 (14 days)TTL for a bot-minted claim invite code — long, since the claim link is delivered asynchronously (a public skeet).
FEATHERREADER_RELAY_HOSTSrelay1.us-west.bsky.network,relay1.us-east.bsky.networkRelays queried for the network adoption count. Bare hosts or full URLs. Setting it to the empty string names no relays and so disables the probe (unset ⇒ the defaults above; the two are deliberately distinguished).
FEATHERREADER_ADOPTION_INTERVAL_SECS86400 (24h)Adoption-probe cadence (±10% jitter). 0 disables the probe.
FEATHERREADER_SHOW_ADOPTIONfalseRender the one-line adoption fact on /about.

The cutover switch and the Rust-native OAuth client it selects:

VariableDefaultMeaning
FEATHERREADER_REPO_BACKENDsidecarWhich implementation serves com.atproto.repo.*: sidecar or rust. An unrecognised value FAILS startup rather than defaulting, since a silent fallback would make every side-by-side measurement a comparison of the sidecar with itself. The container entrypoint reads the same variable to install the matching Caddy OAuth routing — the two cannot share /oauth/callback, so they must agree.
FEATHERREADER_STANDARD_SITEfalseWhether an at://…/site.standard.publication/… subscription may be stored — pasted into the subscribe form (a handle is resolved to its DID), imported via OPML, or written by another client. A stored publication is polled whatever this says: the flag gates storage, not reading (see feed::FeedKind).
FEATHERREADER_OAUTH_KEY_PATHoauth-signing-key.jsonThe client’s ES256 signing key, encrypted at rest in the SAME format the sidecar writes so one file serves both and a rollback finds what it expects.
FEATHERREADER_OAUTH_ENCRYPTION_KEY(unset = plaintext)At-rest encryption for the signing key and stored sessions. Generate it, do not choose it — openssl rand -hex 32. The value is stretched with a single SHA-256 (pinned for byte-compatibility with the sidecar’s format), so its entropy is the ceiling, and the adversary this protects against is someone holding a volume snapshot with all the time in the world.
FEATHERREADER_PLC_DIRECTORYhttps://plc.directoryDirectory used to resolve did:plc documents.
FEATHERREADER_OAUTH_SCOPEatproto transition:genericScope requested at login. Part of the dev client_id, so changing it changes the client’s identity in dev.

The atproto OAuth sidecar (@atproto/oauth-client-node) is configured with a second small block — the base URL the Rust server reaches it on and the shared secret gating its internal API (see SidecarConfig):

VariableDefaultMeaning
SIDECAR_PUBLIC_URLhttp://127.0.0.1:8081Public base URL of the OAuth sidecar (its browser-facing /login, plus the OAuth client_id/redirect_uri).
SIDECAR_INTERNAL_URL(= SIDECAR_PUBLIC_URL)Loopback base URL the Rust server reaches the sidecar’s /internal/* API on. Defaults to the public URL for single-URL local dev.
SIDECAR_INTERNAL_SECRET(dev fallback)Shared X-Internal-Secret for the sidecar’s /internal/* API.
FEATHERREADER_COOKIE_SECRET(dev fallback)HMAC key used to sign the session cookie.
FEATHERREADER_DEV_DID(unset)When set, a request with no session cookie acts as this DID (local runs without the sidecar).

FEATHERREADER_BIND also accepts the design’s FEATHERREADER_ADDR spelling as a fallback for compatibility.

Structs§

Config
Fully-resolved server configuration, materialized once at startup.
OauthConfig
Wiring for the Rust-native OAuth client.
SidecarConfig
Configuration for the atproto OAuth sidecar (@atproto/oauth-client-node).