Files
ipx/docs/configuration.md
rays 0945f3a9f4 Remove ipx copy-db (#85)
It was the one-off copy from SQLite to Postgres (#18), run once on 2026-09-18. Production has run
on Postgres since; rolling back needs only the old state.db, which is kept, not this command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:15:51 +00:00

8.1 KiB

Configuration

Two places. config.toml holds what ipx needs before it reaches its database, and what decides who gets in: where things are (download_dir, socket, organize), [torrent] and [web]. The database holds the catalogue of feeds ([feeds.<id>] below) and the server settings the admin page edits (schedule, max_total_gb, max_age_days, max_new_per_check, media_types). Change those in the web UI, or with ipx add, ipx rm and ipx import; they take effect without a restart.

The first time ipx meets a database that holds no catalogue, it takes the feeds and those settings from config.toml, then rewrites config.toml without them, keeping the original beside it as config.toml.pre-database. After that, feeds or those settings written into config.toml are ignored, with a warning in the log saying so. The sections below describe them as they were written in config.toml, which is still how a fresh install begins.

config.toml's default location is $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 unless IPX_DATABASE_URL names a Postgres database instead. Back SQLite up by copying state.db while the daemon is stopped, or with sqlite3 state.db .backup; back Postgres up with pg_dump.

[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, max_total_gb, max_age_days, max_new_per_check and media_types move into the database as described above; download_dir, socket and organize stay in config.toml.

  • 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-feed schedule overrides both. Admin-only from the UI.
  • organize — feed files downloads under the feed's folder; date under YYYY-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. 0 disables it entirely.
  • max_age_days — items older than this with no file on disk are pruned from the database. Kept ones stay. 0 disables it.
  • max_new_per_check — the cap that stops a new subscription pulling a whole back catalogue. 0 means 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"]
access_team       = ""                    # e.g. "<team>.cloudflareaccess.com"
access_aud        = ""                    # the Access application's AUD tag
auto_create_users = true
sign_out_url      = ""                    # e.g. "/cdn-cgi/access/logout"
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.
  • access_team, access_aud — with both set, a request through the proxy also has to carry the Cf-Access-Jwt-Assertion Cloudflare Access signed for this application, and the name comes from that token instead of the header. See sso.md.
  • auto_create_users — create an account the first time the proxy vouches for a new name.
  • sign_out_url — where Sign out sends someone the proxy signed in: the proxy's own sign-out, /cdn-cgi/access/logout behind Cloudflare Access. Empty sends them to the sign-in page, where the proxy signs them straight back in.
  • 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>]

Kept in the database once ipx has moved them in: a feed's settings are changed in the web UI, and feeds come and go with ipx add, ipx rm and ipx import. 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
category           = "Technology"                # the Directory's, if the feed names none
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 in the catalogue: the OPML is the source of truth and they are re-derived on every scan. Editing one in the UI promotes it to a catalogue entry.

Environment

Variable Effect
IPX_CONFIG Config file path
IPX_DATA_DIR Directory holding state.db
IPX_DATABASE_URL A postgres://user:password@host:port/database URL: use that database instead of state.db
IPX_TEST_DATABASE_URL For cargo test: run the database tests on this Postgres database too, each in a schema of its own
IPX_LOG What reaches stderr (ipx=debug, ipx::scan=debug, …)
IPX_LOG_FORMAT json for one JSON object a line, with each request's and event's fields as its own (for Loki and the like); text otherwise
IPX_UI_LOG What the in-process log buffer captures for the UI's Log view
OTEL_EXPORTER_OTLP_ENDPOINT An OTLP/HTTP collector, such as Tempo at http://host:4318: the daemon sends it traces of scans, downloads and web requests. The other OTEL_EXPORTER_OTLP_* variables apply too
http_proxy / https_proxy Honoured for feed and enclosure fetches