Works through TODO.md from the 2026-09-12 over-engineering audit. Drops the entries.read/flagged/position columns (migrate() removes them from older databases), migrate_opml_children, the legacy interval_mins key, the contrib/ systemd units, test-only Db wrappers, a duplicate token generator, redundant logbuf visitors, unused page state and CSS, and the infer, dirs and tokio-stream dependencies. The icon is served once as /icon.png instead of inlined four times, taking about 94 KB off the two pages. The adoption's subscription half was not dead: it gives a fresh install's first admin the config's feeds. It stays as adopt_catalogue, now tested. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TAC7sLVqfKmY6rsTLXzNgk
5.4 KiB
Configuration
One TOML file, read at startup and re-read whenever the web UI writes to it — most changes take
effect without a restart. Default location $XDG_CONFIG_HOME/ipx/config.toml
(~/.config/ipx/config.toml), overridden with --config or $IPX_CONFIG.
| What | Where | Override |
|---|---|---|
| Config | ~/.config/ipx/config.toml |
--config, $IPX_CONFIG |
| Database | ~/.local/share/ipx/state.db |
$IPX_DATA_DIR |
| Control socket | $XDG_RUNTIME_DIR/ipx.sock |
[general] socket |
| Downloads | [general] download_dir |
— |
~ is expanded in paths. The database is SQLite in WAL mode; back it up by copying state.db
while the daemon is stopped, or with sqlite3 state.db .backup.
[general]
[general]
download_dir = "~/Podcasts"
socket = "/run/user/1000/ipx.sock"
schedule = "every 1h" # "every 30m", "every 4h", "2d", "90" (minutes)
organize = "feed" # "feed" | "date"
max_total_gb = 50 # 0 = unlimited
max_age_days = 30 # 0 = keep forever
max_new_per_check = 3 # per feed, per scan. 0 = unlimited
media_types = ["audio", "video"]
schedule— how often feeds are re-checked. A feed's own<ttl>still wins when it asks to be polled less often, and a per-feedscheduleoverrides both. Admin-only from the UI.organize—feedfiles downloads under the feed's folder;dateunderYYYY-MM-DD.max_total_gb— the reaper deletes to get back under this, oldest first, keeping a 50 MB pad. Kept items are never deleted, and a file only counts as read once every subscriber has read it.0disables it entirely.max_age_days— items older than this with no file on disk are pruned from the database. Kept ones stay.0disables it.max_new_per_check— the cap that stops a new subscription pulling a whole back catalogue.0means unlimited, which is rarely what you want: subscribing to an OPML of 80 feeds with no cap fetched 216 files and 22 GB in one scan.media_types— top-level MIME types taken automatically. Anything else is still listed and can be fetched by hand; blog feeds put each article's header image in an<enclosure>, and without this the disk fills with artwork. Empty takes everything.
[torrent]
[torrent]
enabled = true
seed_ratio = 1.0 # stop seeding at this ratio ...
seed_time_mins = 60 # ... or after this long, whichever comes first
port_range = "6881-6889"
stall_mins = 30 # give up on a torrent making no progress
A .torrent body is handed to the torrent path whatever MIME type it was advertised as. Torrents
run on their own tasks (two at a time) so a slow swarm never blocks a scan.
[web]
[web]
enabled = true
bind = "0.0.0.0:8099" # 127.0.0.1:8080 by default
token = "" # generated and saved on first run
trusted_header = "" # e.g. "Cf-Access-Authenticated-User-Email"
trusted_proxies = ["127.0.0.1", "::1"]
auto_create_users = true
session_days = 30
token— the shared secret, which signs in as the admin.?token=…sets a cookie, so you paste it once per browser. It is what the Docker healthcheck and any scripts use.trusted_header— a header naming the signed-in user, set by whatever fronts ipx. Empty disables that path. See sso.md.trusted_proxies— addresses allowed to assert that header, and the entire security boundary for it. Name the proxy, never a subnet.auto_create_users— create an account the first time the proxy vouches for a new name.session_days— sign a session out after this long without a request.
It is plain HTTP. On a LAN bind everything crosses the network in the clear — and a feed URL can itself carry a credential. Put TLS in front of it if that matters.
[feeds.<id>]
The table key is the feed id: stable, human-readable, and used in paths and the API. ipx add
derives it from the feed title.
[feeds.atp]
url = "https://atp.fm/rss"
folder = "Accidental Tech Podcast" # default: the feed title
schedule = "every 6h" # overrides [general] for this feed
media_types = ["audio"] # overrides [general] for this feed
username = "ray" # HTTP basic auth
password_env = "IPX_ATP_PASS" # preferred over a literal `password`
With more than one account, keywords, auto_download, allow_explicit and
max_new_per_check live on each person's subscription in the database, not here — the values in
config.toml are the fallback for a feed nobody has claimed. The keys above describe the feed itself
and are the same for everyone. See users.md.
Feeds derived from a subscribed OPML are not written here: the OPML is the source of truth and they are re-derived on every scan. Editing one in the UI promotes it to a real config entry.
Environment
| Variable | Effect |
|---|---|
IPX_CONFIG |
Config file path |
IPX_DATA_DIR |
Directory holding state.db |
IPX_LOG |
What reaches stderr (ipx=debug, ipx::scan=debug, …) |
IPX_UI_LOG |
What the in-process log buffer captures for the UI's Log view |
http_proxy / https_proxy |
Honoured for feed and enclosure fetches |