{"id":51306266,"url":"https://github.com/tjiddy/narratorr-requests","last_synced_at":"2026-07-01T00:00:56.848Z","repository":{"id":365151650,"uuid":"1268711921","full_name":"tjiddy/narratorr-requests","owner":"tjiddy","description":"Overseerr-style audiobook request manager — a contract-first sidecar to narratorr","archived":false,"fork":false,"pushed_at":"2026-06-25T06:12:27.000Z","size":1116,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-25T06:14:57.368Z","etag":null,"topics":["audiobooks","fastify","narratorr","overseerr","react","self-hosted","typescript"],"latest_commit_sha":null,"homepage":"https://docs.narratorr.dev/narratorr-requests/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tjiddy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-13T21:11:14.000Z","updated_at":"2026-06-25T03:47:38.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/tjiddy/narratorr-requests","commit_stats":null,"previous_names":["tjiddy/narratorr-request","tjiddy/narratorr-requests"],"tags_count":18,"template":false,"template_full_name":null,"purl":"pkg:github/tjiddy/narratorr-requests","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tjiddy%2Fnarratorr-requests","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tjiddy%2Fnarratorr-requests/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tjiddy%2Fnarratorr-requests/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tjiddy%2Fnarratorr-requests/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tjiddy","download_url":"https://codeload.github.com/tjiddy/narratorr-requests/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tjiddy%2Fnarratorr-requests/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34987610,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-30T02:00:05.919Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["audiobooks","fastify","narratorr","overseerr","react","self-hosted","typescript"],"created_at":"2026-07-01T00:00:51.638Z","updated_at":"2026-07-01T00:00:56.831Z","avatar_url":"https://github.com/tjiddy.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# narratorr-requests\n\nAn [Overseerr](https://overseerr.dev/)-style request manager for\n[Narratorr](https://github.com/tjiddy/narratorr) (Sonarr/Radarr for audiobooks). Family and friends sign in\n(local email/password or any OIDC provider — Plex bridge, Authelia, Authentik, Google, …),\nsearch/browse audiobooks, and request them. New users land in an **approval queue** until an\nadmin lets them in; an admin then approves each request (admins and auto-approve users skip the queue); approved\nrequests hand off to Narratorr's `search → download → import` pipeline, and the requester tracks\nprogress to **available** on their **My requests** page. Notifications are admin-facing — a new\nrequest to review, a new signup awaiting approval — not requester-facing.\n\nIt is a **plug-in sidecar** that talks to Narratorr only over its public `/api/v1` HTTP\nsurface (API key). It has no other coupling. The Narratorr connection and the notifiers\n(a list of ntfy / email / webhook destinations, each routed to the events it fires on) are configured in the in-app **Settings** page after first\nboot — not via environment variables — and stored encrypted in the DB, so spinning it up\ntakes a minimal env (a session secret + auth) and the rest is point-and-click.\n\n## Contract-first\n\nThe `/api/v1` surface this app consumes is vendored as Zod schemas under\n[`src/shared/schemas/v1/`](src/shared/schemas/v1/) (+ [`book.ts`](src/shared/schemas/book.ts)),\nmirroring Narratorr's own schema layout so the contract lifts cleanly upstream. The matching\n**MSW handlers** (`src/server/mocks/narratorr-v1.ts`) are a **test fixture** — they back the\ncontract tests; there is no mock runtime mode.\n\nSee [`CLAUDE.md`](CLAUDE.md) for the architecture, conventions, and the contract/auth model.\n\n## Run it (local dev)\n\n```bash\npnpm install\ncp .env.example .env          # AUTH_BYPASS=1 is the default — seeds a dev admin\npnpm db:migrate               # create/upgrade the sqlite db\npnpm dev                      # server :3000, client :5173\n```\n\nOpen http://localhost:5173, then go to **Settings** and enter a Narratorr URL + API key\n(use **Test** to verify) to enable search and requests. Add a notifier the same way — pick a\ntype, choose which events it fires on, and **Test** before saving. Until Narratorr is\nconfigured, search/requests return a \"not connected yet\" notice.\n\n## Auth\n\nAuthentication (who you are) is **pluggable**; authorization (who may request) is the in-app\n**approval queue**. The two are independent.\n\n- **Methods** — enable either or both:\n  - **Local** email/password (`LOCAL_AUTH=true`, the default). Email is the login + contact;\n    the first signup becomes admin.\n  - **OIDC** — list provider ids in `OIDC_PROVIDERS` and configure each with `OIDC_\u003cID\u003e_*`\n    (issuer / client id / secret / redirect uri / label, plus optional claim overrides). Plex\n    (via the bridge), Authelia, Authentik, Keycloak, Pocket-ID, Google, … are all just provider\n    instances. Each gets a login button and a callback at `/api/auth/oidc/\u003cid\u003e/callback`.\n- **Approval queue** — a new user can sign in but lands `pending` and can't search or request\n  until an admin approves them on the **Users** page (reject is durable). The **first** user in\n  any method becomes admin + active automatically. For a public/open IdP, set\n  `BOOTSTRAP_ADMIN=\u003cprovider\u003e:\u003csubjectOrUsername\u003e` to claim admin deterministically (this\n  disables first-user-auto-admin so a stranger can't grab it by signing in first).\n- **Dev** — `AUTH_BYPASS=1` seeds a dev admin and makes every request that admin, so it's\n  refused in production and on non-loopback binds.\n- Behind a reverse proxy, set `TRUSTED_PROXIES` (same var name as narratorr) so the real client IP is used for the login\n  rate-limit. See [`.env.example`](.env.example) for copy-paste presets.\n\n\u003e Single-instance note: OIDC login state and the auth rate-limiter are in-memory, so running\n\u003e multiple replicas needs sticky sessions (a shared store is a future enhancement).\n\n## Settings (in-app)\n\nThe admin **Settings** page configures, and stores encrypted at rest:\n\n- **Narratorr connection** — base URL + API key (the lifeline; required for search/requests).\n- **Notifiers** — a list of destinations (ntfy, email/SMTP, generic/Discord webhook). Each has a name, an enabled toggle, the events it fires on (new request / new signup), and a **Test** button — add as many as you like, including several of the same type routed to different events.\n- **Public URL** — used to deep-link notifications back to the request queue.\n\nSecrets are never returned to the browser (the form shows `•••••••• (unchanged)`). The at-rest key\ncomes from `SETTINGS_KEY`, or is derived from `SESSION_SECRET` when that's unset — see the\ncaveat in [`.env.docker.example`](.env.docker.example).\n\n## Docker\n\nA single container runs everything (Fastify API + the built SPA), with the libSQL file on a\nnamed volume and migrations applied on boot.\n\n**Smoke test (boots into the admin UI, zero config):**\n\n```bash\ndocker compose up --build      # http://localhost:3000\n```\n\nOut of the box this runs in **AUTH_BYPASS** mode (seeded dev admin) so you can confirm the\nimage runs and serves the UI. `/api/health` reports readiness (it pings the DB and reports\n`narratorrConfigured`), and the container has a matching `HEALTHCHECK`. Configure Narratorr in\nSettings to actually request anything.\n\n**Real deployment:**\n\n```bash\ncp .env.docker.example .env     # fill SESSION_SECRET (+ SETTINGS_KEY), pick an auth method\n#                                 (LOCAL_AUTH and/or OIDC_*), set NODE_ENV=production, AUTH_BYPASS=0\ndocker compose up -d --build\n# then open the app, sign in (first user = admin), and configure Narratorr + notifications in Settings\n```\n\nIn production the app **refuses to start** without a `SESSION_SECRET`, refuses `AUTH_BYPASS`\nentirely, and requires at least one auth method (`LOCAL_AUTH` or an OIDC provider) — so a\nmisconfigured deployment fails fast rather than exposing an open admin. The build is a 3-stage\n`node:24-slim` image that runs as the non-root `node` user; the DB persists in the\n`narratorr-requests-data` volume (`/data`).\n\n\u003e **Upgrading:** migrations apply automatically on boot and are **forward-only** (no\n\u003e down-migrations). Before upgrading a populated instance, **snapshot the\n\u003e `narratorr-requests-data` volume**; to roll back, restore that snapshot and redeploy the\n\u003e previous image (an older image can't run against a newer schema).\n\n### Published images\n\nCI (`.github/workflows/docker.yml`) builds a multi-arch (amd64/arm64) image and pushes to\n**Docker Hub `narratorr/narratorr-requests`** and **GHCR `ghcr.io/tjiddy/narratorr-requests`**:\n\n- **Release** — push a semver tag on `main` (`git tag v1.0.0 \u0026\u0026 git push origin v1.0.0`) → `:latest`,\n  `:1.0.0`, `:1.0` + a GitHub Release.\n- **Bleeding edge** — every push to `develop` (or a manual workflow run) → `:develop`.\n\nQuality gates (lint/typecheck/test/build) run first, and the pushed image is smoke-tested before\nthe job succeeds. Requires repo secrets **`DOCKERHUB_USERNAME`** and **`DOCKERHUB_TOKEN`**.\n\n## Verify\n\n```bash\npnpm verify   # lint + test + typecheck + build\n```\n\n## Contributing\n\nIssues and PRs welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) (branch off `develop`) and the\n[Code of Conduct](CODE_OF_CONDUCT.md). Found a security issue? See [SECURITY.md](SECURITY.md) —\nplease report privately, not as a public issue.\n\n## License\n\n[GPL-3.0](LICENSE) © Todd Johnson\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftjiddy%2Fnarratorr-requests","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftjiddy%2Fnarratorr-requests","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftjiddy%2Fnarratorr-requests/lists"}