Share settings across devices (digest server)¶
The iOS app has three connection modes (Settings → Connection):
| Mode | Where config + removed papers live | Synced? | Needs a server? |
|---|---|---|---|
| On this device (default) | on the phone | no | no |
| Server | on the digest server | yes: every device pointing at the same server shares them | yes |
| Demo | in memory (sample papers) | no | no |
To share keywords, authors, feeds, weights and removed papers between devices
(iPhone, iPad, a Mac), run the digest server (server/, a FastAPI app around
the same arxiv_digest.py engine as the CLI and GUI) somewhere all of them can
reach, and switch each device to Server.
No server? Copy one file instead
In On this device mode nothing is synced, but you can move the config by hand as a single JSON file, both ways (see Copy the config file).
1. Requirements¶
- Python 3.12+ and uv (installs into your home directory, no root needed):
- The whole repository, not just
server/: the server imports the engine (arxiv_digest.py) from the repo root.
2. Start the server¶
Check it from the same machine:
- Without an access token the server only answers this machine (it checks
the caller and the
Hostheader). Other devices need a token, see step 4. - The first past-week load fetches from arXiv (~20–40 s); later loads come from the server's 1 h cache.
- Stop it with
Ctrl-C. Your config and removed papers are kept inserver/digest.db(SQLite), so restarting doesn't lose them.
3. Optional: start from your GUI settings¶
Point the server at a saved GUI profile; it is used as the config for a user who has none stored yet:
DIGEST_DEFAULT_CONFIG_PATH=~/.arxiv_scraper/profiles/<name>.json \
uv run uvicorn app.main:app --host 127.0.0.1 --port 8000
This seeds the server once. Edits made later in the app are stored on the server and don't change the profile file.
4. Make it reachable from your devices¶
First create an access token (a shared secret every device sends) and start the server with it. Keep it private, like a password:
export DIGEST_ACCESS_TOKEN="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"
echo "$DIGEST_ACCESS_TOKEN" # copy this into each device (step 5)
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000
With a token set, every request except /health must send it
(Authorization: Bearer <token>); without it the server answers 401. Then
pick how devices reach the server:
| Setup | Server command | URL to enter on each device |
|---|---|---|
| Same Wi-Fi at home (Mac or Linux box) | --host 0.0.0.0 |
http://<server-LAN-IP>:8000 |
| From anywhere, private: Tailscale on the server and every device | --host 0.0.0.0 |
http://<server-tailscale-IP>:8000 |
| Remote Linux machine, Mac only: SSH tunnel | --host 127.0.0.1 |
on the Mac: ssh -N -L 8000:127.0.0.1:8000 you@host, then http://127.0.0.1:8000 |
Find the LAN IP with ipconfig getifaddr en0 (Mac) or hostname -I (Linux).
If a device can't connect, open http://<url>/health in its browser first;
firewalls and guest/eduroam networks that isolate devices are the usual cause.
One shared user, plain HTTP
local mode has a single shared user and no accounts; the access token is
the only lock, and plain http:// doesn't encrypt it. Use it on networks you
trust (home Wi-Fi, Tailscale, an SSH tunnel), never directly on the public
internet. See Going public.
What the server guards against:
- Other devices without the token:
401(or403when no token is configured). - Web pages you visit: no CORS headers by default, and the localhost-only
check rejects foreign
Hostnames, so a page can't read or change your config throughhttp://127.0.0.1:8000(including DNS rebinding). - Fetching arbitrary URLs: feed URLs must be on
arxiv.org; a config with other hosts, or more than 50 feeds, is rejected (422). - Remote mode with the default JWT secret: the server refuses to start.
5. Connect each device¶
iPhone / iPad: Settings → Connection → Server → enter the URL from step 4 and the access token → Connect. The status line reads Connected · local mode; a wrong token shows Missing or wrong access token. The token is stored in the iOS Keychain. Allow Local Network access when iOS asks (needed for LAN addresses).
Web GUI (arxiv-gui, two-way): sidebar → 🔄 Sync with server → enter
the URL and the access token → Connect. The first time it asks once whose
config both sides should use: Use server's or Upload mine. After that:
- every GUI session starts from the server's config and removed papers; ⬇ Download refreshes them;
- ⬆ Save to server, saving a profile, and Write project config upload the config; removing or restoring a paper goes to the server straight away;
- if the server can't be reached the GUI keeps working from its local copy
(
arxiv_config.json, which thearxiv-digestCLI also reads) and shows Offline. Changes made meanwhile (config, removed and restored papers) go to the server with the next save or the next GUI start, before anything is downloaded; ⬇ Download instead drops them in favour of the server's.
The address and token are stored in ~/.arxiv_scraper/sync.json (readable only
by you); ARXIV_DIGEST_SERVER / ARXIV_DIGEST_TOKEN override them. Stop
syncing goes back to local-only.
Named profiles stay on your computer: loading one and saving it sends it to the server as the shared config.
Mac app (optional): on Apple-silicon Macs the iOS app also runs as a Mac
app. In Xcode, open ios/ArxivDigest.xcodeproj, choose the destination My Mac
(Designed for iPad), run it, and connect it to the same URL. (Not yet tested
on this project; report problems.)
Now a keyword added on the phone, a removal on the Mac, or a preset loaded on either is saved to the server and shows on the other device after its next load (pull to refresh).
6. Keep it running¶
A template unit is in deploy/arxiv-digest.service. Fill in <youruser>
and the checkout path, then:
sudo cp deploy/arxiv-digest.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now arxiv-digest
systemctl status arxiv-digest
Without root but with user lingering enabled (loginctl enable-linger $USER,
which some distributions allow without sudo), copy the unit to
~/.config/systemd/user/, remove its User= line, and use
systemctl --user enable --now arxiv-digest.
Check first that long-running processes are allowed on the machine.
tmux new -s digest
cd ~/arxiv-digest/server && uv run uvicorn app.main:app --host 127.0.0.1 --port 8000
# detach: Ctrl-b then d · reattach: tmux attach -t digest
tmux sessions don't survive a reboot; start it again afterwards.
Run the step 2 command in a Terminal window, or in a
tmux/screen session. The Mac must be awake for other devices to sync.
Updating:
cd ~/arxiv-digest && git pull
cd server && uv sync
# then restart: Ctrl-C and rerun, or `sudo systemctl restart arxiv-digest`
7. Back up¶
Everything is in server/digest.db. Copy it while the server is stopped, e.g.
cp server/digest.db ~/digest-backup-$(date +%F).db.
Copy the config file (without a server)¶
For On this device mode, or to move settings once: the config is a single JSON file with the same format everywhere.
Mac → iPhone
- GUI → Profiles tab → Export next to a saved profile (save the current
config as a profile first if needed). This downloads
<name>.json. - AirDrop the file to the iPhone and choose arXiv Digest, or save it to Files and use the app's Settings → Config file → Import config….
- The app opens Config with the imported settings; tap Save.
iPhone → Mac
- App → Settings → Config file → Export config → AirDrop to the Mac
(it arrives in Downloads as
arxiv-digest-config.json). - GUI → Profiles tab → Import profile from JSON → pick the file.
- Save it as a profile, or Write project config (and, if you sync, ⬆ Save to server).
An import replaces the whole config (keywords, authors, feeds, weights, colors) and isn't kept until you save. Removed papers aren't part of the file.
Reference¶
Server settings (environment variables)¶
| Variable | Default | Meaning |
|---|---|---|
DIGEST_MODE |
local |
local: one shared user, SQLite, access token (or this machine only). remote: JWT login, multi-user |
DIGEST_DATABASE_URL |
sqlite:///./digest.db |
Database (Postgres for remote) |
DIGEST_DEFAULT_CONFIG_PATH |
– | GUI profile used for users without a stored config |
DIGEST_JWT_SECRET |
dev-secret-change-me |
Change in remote mode |
DIGEST_ACCESS_TOKEN |
– | Local mode: token every device must send; unset = this machine only |
DIGEST_CORS_ORIGINS |
[] (none) |
Browser origins allowed to call the API; leave empty unless a web front end needs it |
ZOTERO_API_KEY, ZOTERO_LIBRARY_ID |
– | Enable Save to Zotero from the app via the Zotero Web API |
Without the Zotero variables the app offers Share to Zotero (the iOS share sheet hands the paper to the Zotero app), and everything else works.
Going public (remote mode)¶
export DIGEST_MODE=remote
export DIGEST_JWT_SECRET="$(openssl rand -hex 32)"
export DIGEST_DATABASE_URL="postgresql+psycopg://user:pass@host/db"
export DIGEST_CORS_ORIGINS='["https://yourapp.example"]'
Users register with POST /auth/register and log in with POST /auth/token.
The iOS app has no login screen yet, so remote mode isn't usable from the
app today. Put HTTPS in front (e.g. Caddy) before exposing it, and expect arXiv
to rate-limit a busy shared server; the server caches fetches for an hour.