Crate Structure¶
rpodder is organized as a Cargo workspace with four crates. Dependencies flow one way: server → db → core, and server → feed.
rpodder-server (binary)
├── rpodder-core (domain types, traits, errors)
├── rpodder-db (PostgreSQL + SQLite implementations)
└── rpodder-feed (HTTP client + RSS parser)
rpodder-core¶
Domain types, repository traits, error types, and utilities.
No external dependencies beyond chrono, uuid, serde, url, thiserror. This crate defines the "what" without the "how".
Key files¶
types.rs— all domain structs:User,Device,Podcast,Episode,Subscription,EpisodeAction,Session,SyncGroup,Setting,PodcastList,Chapter,Favoriterepo.rs— 13 async traits:UserRepo,SessionRepo,DeviceRepo,PodcastRepo,EpisodeRepo,SubscriptionRepo,EpisodeActionRepo,SyncGroupRepo,TagRepo,SettingsRepo,PodcastListRepo,ChapterRepo,FavoriteRepoerror.rs—AppErrorenum withNotFound,Conflict,Internalvariantsurl.rs— URL normalization (lowercase host, strip trailing slashes)privacy.rs— heuristic detection of private/token URLs
rpodder-db¶
Database implementations for PostgreSQL and SQLite.
Implements all 13 repository traits from rpodder-core for both backends. Uses sqlx with runtime queries (no compile-time macros).
Key files¶
lib.rs—Dbenum, connection logic, dynamic migration runnerpostgres.rs—PgRepostruct implementing all traitssqlite.rs—SqliteRepostruct implementing all traits, plus unit tests
Pattern¶
Each repo implementation follows the same pattern:
impl repo::UserRepo for PgRepo {
async fn find_by_username(&self, username: &str) -> Result<Option<User>> {
let row: Option<UserRow> = sqlx::query_as(
"SELECT id, username, ... FROM users WHERE LOWER(username) = LOWER($1)",
)
.bind(username)
.fetch_optional(&self.pool)
.await
.map_err(db_err)?;
Ok(row.map(Into::into))
}
}
UserRowis a#[derive(FromRow)]struct matching the SQL columnsFrom<UserRow> for Userconverts from the DB row to the domain type- PostgreSQL uses
$1, $2placeholders; SQLite uses?
rpodder-feed¶
Feed fetching and parsing.
FeedFetcher— HTTP client with conditional GET (ETag, If-Modified-Since), retry with exponential backoff- Uses
feed-rsfor RSS/Atom parsing - Extracts podcast metadata, episodes, and categories
rpodder-server¶
The binary crate — HTTP server, CLI, everything that ties it together.
Key modules¶
main.rs— CLI (clap), server setup, route registrationconfig.rs—AppConfigfrom env vars + TOMLstate.rs—AppStatewithArc<Db>+Arc<AppConfig>middleware/auth.rs— authentication + admin middlewareemail.rs— SMTP sender for activation and password reset emailspodcast_index.rs— Podcast Index API clientfeed_updater.rs— background feed update loopweb_ui.rs— rust-embed handler for serving the SPAroutes/— all HTTP handlers organized by domain
Route modules¶
| Module | Endpoints |
|---|---|
auth.rs |
Login, logout |
admin.rs |
User management, stats, password, HTTPS upgrades, history, /me |
registration.rs |
Public registration |
oauth.rs |
SSO login, callback, info |
devices.rs |
Device CRUD |
subscriptions.rs |
Subscription sync (simple + advanced) |
episodes.rs |
Episode action upload/download |
directory.rs |
Search, toplist, tags, trending, podcast/episode data |
sync.rs |
Sync group management |
settings.rs |
User settings |
lists.rs |
Podcast lists |
chapters.rs |
Chapter marks |
favorites.rs |
Favorites |
health.rs |
Health check, metrics |
Web UI (web/)¶
The Svelte 5 SPA lives in web/ and is built separately:
web/
src/
lib/
api.ts — TypeScript API client
auth.svelte.ts — reactive auth state (runes)
routes/
+layout.svelte — main layout with nav
discover/ — browse, toplist, trending, tags, podcast detail
subscriptions/ — user's subscriptions with HTTPS upgrades
admin/ — admin panel
settings/ — user settings + password change
reset-password/ — password reset flow
login/ — login page
register/ — registration page
history/ — episode action history
devices/ — device management
Built with bun run build → web/dist/, embedded via rust-embed.