# nightshade Personal save-for-later and e-reader proxy, backed by [atproto](https://atproto.com/) for data ownership and [Miniflux](https://miniflux.app/) 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 ```sh 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`. 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)