Skip to main content

Config

Struct Config 

Source
pub struct Config {
Show 29 fields pub bind: SocketAddr, pub db_path: PathBuf, pub public_url: String, pub allowed_dids: Vec<String>, pub poll_interval: Duration, pub publication_read_deadline: Duration, pub retention_days: u32, pub retention_hard_days: u32, pub publication_retention_days: u32, pub proxy_images: bool, pub beta_cap: i64, pub trusted_ip_header: Option<String>, pub max_subs_per_did: i64, pub max_feeds_global: i64, pub max_entries_per_feed: i64, pub db_size_watermark_bytes: i64, pub sidecar: SidecarConfig, pub oauth: OauthConfig, pub cookie_secret: String, pub dev_did: Option<String>, pub repo_backend: Backend, pub standard_site: bool, pub resolver_base: String, pub bot_secret: Option<String>, pub claim_ttl_secs: i64, pub relay_hosts: Vec<String>, pub relay_host_errors: Vec<String>, pub adoption_interval: Duration, pub show_adoption: bool,
}
Expand description

Fully-resolved server configuration, materialized once at startup.

Fields§

§bind: SocketAddr

The socket address the HTTP server binds to.

§db_path: PathBuf

Filesystem path to the SQLite cache/database file.

§public_url: String

The externally-reachable base URL (used to build the atproto OAuth callback and client-metadata URLs). No trailing slash.

§allowed_dids: Vec<String>

Optional login allow-list of atproto DIDs. Empty means the instance is open to any atproto identity that can log in.

§poll_interval: Duration

The default per-feed poll interval.

§publication_read_deadline: Duration

The longest one standard.site publication read may take, start to finish. A read is otherwise bounded only per request (FETCH_TIMEOUT x MAX_LIST_PAGES), which is hours against a repo that pages slowly.

§retention_days: u32

Cache eviction window, in days: a READ, UNSTARRED entry older than this is dropped from the local cache. Starred and still-unread entries are kept past this window — but NOT indefinitely: see retention_hard_days, which is the bound. The PDS holds the reader’s choices, and the entry CONTENT lives only here and at the origin feed, which usually serves just its last few dozen items.

Two weeks by default. The cache exists to render a feed list quickly, not to archive the web.

§retention_hard_days: u32

Absolute cache ceiling, in days. Entries older than this are dropped REGARDLESS of starred or unread state.

This is the bound, and sparing would remove it without one. “Mark unread” is a one-click control and entries is shared across every reader, so an unbounded exception lets one person pin rows permanently — and because the poller stops entirely once the database crosses db_size_watermark_bytes, with the retention DELETE as its only release valve, those pins could stop polling for everyone.

Losing a starred entry here is survivable: the saved record stays in the reader’s PDS and renders as a link.

§publication_retention_days: u32

Absolute ceiling, in days, for entries of a kind the rolling window does not apply to — a standard.site publication today. 0 disables it.

Ten years, and the reason is that age is the wrong policy here at all. Measured on 2026-09-27, reading three real publications through standard_site::fetch: the newest document Standard.site offered was 131 days old, Annotated’s 109 (oldest 373), minus listens’ 241. Under the 14-day window every one of them stored zero rows — a successful poll and an empty feed. Long-form publishing is not news-paced, so a publication is bounded by COUNT (max_entries_per_feed, the newest N plus up to N starred) rather than by age.

This number is therefore not a space bound; the per-feed trim is. It is the guarantee that “not aged out” does not become “immortal”: the trim only runs when a poll stores something, so entries of a feed nobody polls any more would otherwise never be reaped. Ten years is longer than the protocol itself, so it cannot truncate an archive that exists today, while still being a real bound rather than none.

Per-publication retention on the reader’s own PDS will choose inside this ceiling; the instance’s number stays the upper bound.

§proxy_images: bool

Whether to proxy feed images through the server (privacy vs. bandwidth).

§beta_cap: i64

Closed-beta seat cap: the maximum number of DIDs that may hold beta access at once (redeeming an invite fails with CapacityFull past this). From FEATHERREADER_BETA_CAP, default 100.

§trusted_ip_header: Option<String>

The reverse-proxy header the rate limiter TRUSTS for the real client IP, e.g. Fly-Client-IP (bare Fly) or CF-Connecting-IP (Cloudflare). When set, ONLY this header is consulted — never the spoofable multi-hop X-Forwarded-For chain — and it falls back to the socket peer if the header is absent/unparseable. Unset (the default) trusts the socket peer only, which is correct for a direct bind with no proxy in front. From FEATHERREADER_TRUSTED_IP_HEADER.

§max_subs_per_did: i64

Per-DID subscription cap. A DID may hold at most this many subscriptions; add_subscription rejects over it and import_opml trims to it. Bounds the storage/poller blast radius of one account on a small box. From FEATHERREADER_MAX_SUBS_PER_DID, default 500.

§max_feeds_global: i64

Global ceiling on distinct feeds in the shared cache. A new feed is refused once the feeds table holds this many rows (existing feeds still poll). From FEATHERREADER_MAX_FEEDS, default 10_000.

§max_entries_per_feed: i64

Cap on how many entries are retained per feed on insert — the newest N by published date; older rows are pruned in the same transaction so one firehose feed can’t fill the disk. From FEATHERREADER_MAX_ENTRIES_PER_FEED, default 2_000.

§db_size_watermark_bytes: i64

DB-size watermark, in bytes. Above it the background poller stops fetching new content (and logs an alert) so the $3.50 box can’t be filled to a crash. 0 disables the watermark. From FEATHERREADER_DB_SIZE_WATERMARK_BYTES, default 2 GiB.

§sidecar: SidecarConfig

The atproto OAuth sidecar wiring (base URL + shared internal secret).

§oauth: OauthConfig

The Rust-native OAuth client’s own wiring. Read whatever the backend, so a misconfiguration is caught at startup rather than at the moment the switch is thrown.

§cookie_secret: String

HMAC key used to sign the session cookie. In production this MUST be set (FEATHERREADER_COOKIE_SECRET); a stable dev fallback is used otherwise so local runs work without configuration.

§dev_did: Option<String>

Optional dev-only DID: when set, a request with no valid session cookie is served as this DID (local runs without the OAuth sidecar). Unset in a real deployment — no session then means “logged out”.

§repo_backend: Backend

Which repo implementation serves com.atproto.repo.* — the cutover switch. Defaults to the sidecar, so deploying the Rust client changes nothing until this is set deliberately.

§standard_site: bool

Whether an at:// standard.site publication subscription may be stored — pasted into the subscribe form (a handle is resolved to its DID first), imported via OPML, or written by another client. From FEATHERREADER_STANDARD_SITE, default off.

This flag does not gate polling. Since 0.4.0 a stored publication row is polled like any feed, flag on or off: the flag decides what may be stored, and a row already stored is read. Which at:// rows are publications, and which are Unsupported and never polled, is decided once by crate::feed::FeedKind::of.

§resolver_base: String

Base URL of the atproto handle resolver (com.atproto.identity.resolveHandle), no trailing slash. Used by the pre-handshake beta gate to turn a submitted handle into a DID so an existing seat can be honored on a cookie-less first login. Defaults to crate::atproto::DEFAULT_RESOLVER_HOST. From FEATHERREADER_RESOLVER_HOST.

§bot_secret: Option<String>

Shared bearer secret gating the headless bot mint endpoint (POST /bot/claims), sent by the follow→invite bot as X-Bot-Secret. When empty the endpoint is DISABLED (503) — a bot can’t mint. Like the cookie/sidecar secrets it MUST be set on a production-like instance (fail-loud at boot); on a loopback/dev instance it stays unset so /bot/claims is simply off until an operator opts in. From FEATHERREADER_BOT_SECRET.

§claim_ttl_secs: i64

TTL (seconds) for a claim invite code minted by POST /bot/claims. The bot delivers the claim link asynchronously (a public skeet), so this is a generous window — the admin-mint browser flow’s 30-minute TTL would expire before the follower ever taps the link. From FEATHERREADER_CLAIM_TTL_SECS, default 14 days.

§relay_hosts: Vec<String>

Relay bases queried for the network adoption count, as normalized origin URLs (scheme + host, no trailing slash) — the fetch layer is handed something crate::net can scheme-allow-list rather than being asked to guess. An empty list disables the probe, and FEATHERREADER_RELAY_HOSTS= (present, empty) is how an operator asks for exactly that — distinct from leaving the variable unset, which takes the defaults. Bare hostnames are accepted and normalized to https://….

§relay_host_errors: Vec<String>

Entries of FEATHERREADER_RELAY_HOSTS that were rejected as unusable, in "value" (reason) form. Parsing happens before init_tracing, so these are carried here and warned about by the adoption probe task instead of being lost — or, as they were previously, aborting boot.

§adoption_interval: Duration

How often the adoption probe runs. Duration::ZERO (the env value 0) DISABLES it. From FEATHERREADER_ADOPTION_INTERVAL_SECS, default 24 h.

§show_adoption: bool

Render the one-line adoption fact on /about. Default false: a count of 1 reads as a status claim rather than a fact, and the honest home for it today is the log. From FEATHERREADER_SHOW_ADOPTION.

Implementations§

Source§

impl Config

Source

pub fn retention_for(&self, kind: FeedKind) -> (u32, u32)

The retention window that applies to entries of kind, as (days, hard_days) for crate::store::prune_old_entries.

One home for the policy, because it has two halves that must agree. The sweep decides what to DELETE; standard_site::ingest_floor decides what is even worth STORING, and it is written to mirror the sweep. If the two disagree, the store gains rows the sweep deletes and the next poll re-inserts — the resurrection cycle, which costs a reader their read state once per window, forever. Both sides read this.

An RSS feed gets the rolling window and the hard ceiling. A publication gets no rolling window and the archive ceiling instead: measured, a 14-day window stored zero rows from every real publication tried, because their newest documents were 109 to 241 days old. See Config::publication_retention_days and feed::FeedKind::AGED.

Returning 0 for a publication’s window is load-bearing rather than incidental: prune_old_entries honours a ceiling when days <= 0, and ingest_floor falls through to the ceiling on the same condition.

Source

pub fn from_env() -> Result<Self>

Build a Config from the process environment, falling back to the defaults above for anything unset. Returns an error only when a present variable fails to parse — an unset variable is never an error.

Source

pub fn did_allowed(&self, did: &str) -> bool

Whether the given atproto DID is permitted to log in. When no allow-list is configured the instance is open, so every DID is allowed.

Source

pub fn admin_seed_dids(&self) -> &[String]

The admin-bootstrap seed for the closed-beta gate: the DIDs that get a beta_access seat automatically (via crate::store::ensure_seed) so a fresh instance always has at least the operator(s) inside the gate and able to mint invite codes.

Reuses ALLOWED_DIDS as the seed source — the same “these are the people I trust on this instance” concept — so operators don’t configure the list twice. Returns a borrowed slice (empty when the instance is open / no allow-list is set, in which case there is nothing to seed).

Trait Implementations§

Source§

impl Clone for Config

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Config

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for Config

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
Source§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more