Personal save-for-later and Miniflux e-reader proxy for Xteink X4 (wip)
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

TypeScript 89.1%
Nix 6.5%
CSS 4.0%
HTML 0.4%
11 1 0

Clone this repository

https://tangled.org/pdewey.com/nightshade https://tangled.org/did:plc:hm5f3dnm6jdhrc55qp2npdja/nightshade
git@tangled.org:pdewey.com/nightshade git@tangled.org:did:plc:hm5f3dnm6jdhrc55qp2npdja/nightshade

For self-hosted knots, clone URLs may differ based on your setup.

Download tar.gz
README.md

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 to
  • GET /auth/callback — OAuth redirect target
  • POST /auth/logout — revoke all local sessions

Management (JSON, requires session)#

  • GET /api/saves[?all=1] — list save records
  • POST /api/saves — body { url }; creates atproto record, fetches title
  • DELETE /api/saves/:rkey
  • POST /api/saves/:rkey/read / /unread
  • GET /api/feeds — list feed records
  • POST /api/feeds — body { url, title? }; creates atproto record, reconciles to Miniflux
  • DELETE /api/feeds/:rkey — deletes atproto record, reconciles to Miniflux
  • POST /api/feeds/refresh — refresh all Miniflux feeds now
  • POST /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)