nightshade#
Personal save-for-later and e-reader proxy, backed by atproto for data ownership and Miniflux for RSS polling.
- feed subscriptions and saved URLs live on your atproto PDS as records
(
net.solanaceae.nightshade.feed,net.solanaceae.nightshade.save) - Nightshade syncs feeds two-way between atproto and Miniflux
- the e-reader endpoint fetches saved-URL bodies on demand (no local storage), with a short in-memory TTL cache
- Preact UI for atproto OAuth sign-in, feed management, and saving URLs
Data shape#
atproto holds feed subscriptions and saved URLs (metadata only, no bodies). Miniflux mirrors the feed list and does the actual polling, entry storage, and entry read-state. Nightshade keeps nothing of its own apart from OAuth session files and a small sync snapshot. Kill the process and your data is fine.
Development#
pnpm install
cp .env.example .env # fill in MINIFLUX_URL + MINIFLUX_TOKEN
pnpm dev # server on 8787, Vite on 5173
Leaving NIGHTSHADE_PUBLIC_URL unset runs atproto OAuth in loopback mode
(http://localhost client_id) so you can sign in without a public URL.
Environment#
| var | default | purpose |
|---|---|---|
MINIFLUX_URL |
n/a | required, base URL of your Miniflux instance |
MINIFLUX_TOKEN |
n/a | required (or MINIFLUX_TOKEN_FILE), Miniflux API key |
MINIFLUX_TOKEN_FILE |
n/a | path to file containing just the token value |
NIGHTSHADE_PUBLIC_URL |
n/a | unset for loopback OAuth; set to https://host.tld for production |
NIGHTSHADE_PORT |
8787 |
HTTP port |
NIGHTSHADE_DATA_DIR |
./data |
directory for OAuth state/session files and sync snapshot |
HTTP API#
Auth#
GET /auth/status—{ authenticated, did? }POST /auth/login— body{ handle }, returns{ url }to redirect toGET /auth/callback— OAuth redirect targetPOST /auth/logout— revoke all local sessions
Management (JSON, requires session)#
GET /api/saves[?all=1]— list save recordsPOST /api/saves— body{ url }; creates atproto record, fetches titleDELETE /api/saves/:rkeyPOST /api/saves/:rkey/read//unreadGET /api/feeds— list feed recordsPOST /api/feeds— body{ url, title? }; creates atproto record, reconciles to MinifluxDELETE /api/feeds/:rkey— deletes atproto record, reconciles to MinifluxPOST /api/feeds/refresh— refresh all Miniflux feeds nowPOST /api/sync— trigger a reconciliation pass immediately
E-reader device (plain text, LAN-only, no auth/TLS)#
Save IDs are s<rkey>. RSS entry IDs are the numeric Miniflux entry id.
GET /device/list[?all=1&limit=N]GET /device/item/:id[?page=N]POST /device/item/:id/read
Sync behavior#
Two-way sync between atproto and Miniflux, driven by a last-seen snapshot in
${NIGHTSHADE_DATA_DIR}/sync-state.json. Runs on startup, after every
feed-write through /api/feeds, and on a 5-minute timer.
- Feed added on atproto → subscribed in Miniflux
- Feed added via Miniflux UI or OPML import → record created on atproto
- Feed removed from either side → removed from the other
- Saves don't sync (Miniflux has no saves concept)