Expand description
The axum web layer — server-rendered HTML + a dash of htmx, no SPA.
This module owns the HTTP surface: router builds an axum::Router over
the shared AppState, wiring the store, feed, atproto, and config seams into
a small set of typography-first, dark-mode-ready views rendered with
askama templates (under templates/). Progressive enhancement is a single
vendored htmx script plus a tiny keyboard handler (static/keyboard.js);
every interaction also works as a plain HTML form POST, so the reader is fully
usable with JavaScript disabled.
§HTTP surface
GET /health— liveness + version, astext/plain.GET /about,/standard-site,/stats,/privacy,/terms— static and status pages.GET /manage— feed and folder management.GET /— the reader: a folders/feeds sidebar (from the PDS records layer) plus the main article list. Query params pick the scope (?feed=…/?folder=…/ all) and the view (?view=unread|all|starred).GET /entries/{id}— the clean, distraction-free reader for one entry, with prev/next within the current list.POST /entries/{id}/read— mark an entry read/unread (htmx row swap).POST /entries/{id}/star— star/unstar; writes acommunity.lexicon.rss.savedrecord to the user’s PDS.POST /saved/{rkey}/delete— unsave asavedrecord.POST /read-all— mark-all-read (per feed via?feed=…, else everything).POST /subscriptions— subscribe by URL (autodiscover → PDS record).POST /subscriptions/{rkey}/delete— unsubscribe (delete the PDS record).POST /subscriptions/{rkey}/rename— retitle / move a feed to a folder.POST /folders— create a folder record.POST /folders/{rkey}/rename— rename a folder record.POST /folders/{rkey}/delete— delete a folder record.POST /opml— OPML import (multipart upload or pasted textarea) → bulk subscription records in the PDS.GET /opml/export— OPML export (records → a downloadable document).GET /login+POST /login+/oauth/callback+/logout— the atproto OAuth sign-in flow, on whichever backendFEATHERREADER_REPO_BACKENDselects.GET /oauth/client-metadata.jsonand/oauth/jwks.jsonpublish the Rust client’s identity.POST /account/delete— revoke the session and drop this DID’s local state.GET|POST /beta/redeem— closed-beta invite redemption.GET /claim?t=<token>— the follow→invite bot’s claim link: an opaque token reserving a pre-minted invite code; behaves like a successful/beta/redeem(sets the reserving cookie →/login).POST /bot/claims— headless, shared-secret (X-Bot-Secret) mint of a claim code + token/url for the bot to post. Cap-aware (409 when full).POST /admin/invites,GET /admin/metrics— admin-only invite minting and the backend metrics view.
§Identity — a cookie-resolved atproto session
Per-request identity comes from a signed session cookie (fr_session)
keyed by the logged-in DID, set by oauth_callback and read by
current_session / current_did. For local runs without the sidecar,
Config::dev_did (env FEATHERREADER_DEV_DID) supplies a fallback identity.
All PDS reads and writes route through AppState::repo, which dispatches to
the sidecar or the Rust OAuth client by FEATHERREADER_REPO_BACKEND.