https://github.com/tjiddy/narratorr-requests
Overseerr-style audiobook request manager — a contract-first sidecar to narratorr
https://github.com/tjiddy/narratorr-requests
audiobooks fastify narratorr overseerr react self-hosted typescript
Last synced: 5 days ago
JSON representation
Overseerr-style audiobook request manager — a contract-first sidecar to narratorr
- Host: GitHub
- URL: https://github.com/tjiddy/narratorr-requests
- Owner: tjiddy
- Created: 2026-06-13T21:11:14.000Z (22 days ago)
- Default Branch: main
- Last Pushed: 2026-06-25T06:12:27.000Z (11 days ago)
- Last Synced: 2026-06-25T06:14:57.368Z (11 days ago)
- Topics: audiobooks, fastify, narratorr, overseerr, react, self-hosted, typescript
- Language: TypeScript
- Homepage: https://docs.narratorr.dev/narratorr-requests/
- Size: 1.06 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# narratorr-requests
An [Overseerr](https://overseerr.dev/)-style request manager for
[Narratorr](https://github.com/tjiddy/narratorr) (Sonarr/Radarr for audiobooks). Family and friends sign in
(local email/password or any OIDC provider — Plex bridge, Authelia, Authentik, Google, …),
search/browse audiobooks, and request them. New users land in an **approval queue** until an
admin lets them in; an admin then approves each request (admins and auto-approve users skip the queue); approved
requests hand off to Narratorr's `search → download → import` pipeline, and the requester tracks
progress to **available** on their **My requests** page. Notifications are admin-facing — a new
request to review, a new signup awaiting approval — not requester-facing.
It is a **plug-in sidecar** that talks to Narratorr only over its public `/api/v1` HTTP
surface (API key). It has no other coupling. The Narratorr connection and the notifiers
(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
boot — not via environment variables — and stored encrypted in the DB, so spinning it up
takes a minimal env (a session secret + auth) and the rest is point-and-click.
## Contract-first
The `/api/v1` surface this app consumes is vendored as Zod schemas under
[`src/shared/schemas/v1/`](src/shared/schemas/v1/) (+ [`book.ts`](src/shared/schemas/book.ts)),
mirroring Narratorr's own schema layout so the contract lifts cleanly upstream. The matching
**MSW handlers** (`src/server/mocks/narratorr-v1.ts`) are a **test fixture** — they back the
contract tests; there is no mock runtime mode.
See [`CLAUDE.md`](CLAUDE.md) for the architecture, conventions, and the contract/auth model.
## Run it (local dev)
```bash
pnpm install
cp .env.example .env # AUTH_BYPASS=1 is the default — seeds a dev admin
pnpm db:migrate # create/upgrade the sqlite db
pnpm dev # server :3000, client :5173
```
Open http://localhost:5173, then go to **Settings** and enter a Narratorr URL + API key
(use **Test** to verify) to enable search and requests. Add a notifier the same way — pick a
type, choose which events it fires on, and **Test** before saving. Until Narratorr is
configured, search/requests return a "not connected yet" notice.
## Auth
Authentication (who you are) is **pluggable**; authorization (who may request) is the in-app
**approval queue**. The two are independent.
- **Methods** — enable either or both:
- **Local** email/password (`LOCAL_AUTH=true`, the default). Email is the login + contact;
the first signup becomes admin.
- **OIDC** — list provider ids in `OIDC_PROVIDERS` and configure each with `OIDC__*`
(issuer / client id / secret / redirect uri / label, plus optional claim overrides). Plex
(via the bridge), Authelia, Authentik, Keycloak, Pocket-ID, Google, … are all just provider
instances. Each gets a login button and a callback at `/api/auth/oidc//callback`.
- **Approval queue** — a new user can sign in but lands `pending` and can't search or request
until an admin approves them on the **Users** page (reject is durable). The **first** user in
any method becomes admin + active automatically. For a public/open IdP, set
`BOOTSTRAP_ADMIN=:` to claim admin deterministically (this
disables first-user-auto-admin so a stranger can't grab it by signing in first).
- **Dev** — `AUTH_BYPASS=1` seeds a dev admin and makes every request that admin, so it's
refused in production and on non-loopback binds.
- Behind a reverse proxy, set `TRUSTED_PROXIES` (same var name as narratorr) so the real client IP is used for the login
rate-limit. See [`.env.example`](.env.example) for copy-paste presets.
> Single-instance note: OIDC login state and the auth rate-limiter are in-memory, so running
> multiple replicas needs sticky sessions (a shared store is a future enhancement).
## Settings (in-app)
The admin **Settings** page configures, and stores encrypted at rest:
- **Narratorr connection** — base URL + API key (the lifeline; required for search/requests).
- **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.
- **Public URL** — used to deep-link notifications back to the request queue.
Secrets are never returned to the browser (the form shows `•••••••• (unchanged)`). The at-rest key
comes from `SETTINGS_KEY`, or is derived from `SESSION_SECRET` when that's unset — see the
caveat in [`.env.docker.example`](.env.docker.example).
## Docker
A single container runs everything (Fastify API + the built SPA), with the libSQL file on a
named volume and migrations applied on boot.
**Smoke test (boots into the admin UI, zero config):**
```bash
docker compose up --build # http://localhost:3000
```
Out of the box this runs in **AUTH_BYPASS** mode (seeded dev admin) so you can confirm the
image runs and serves the UI. `/api/health` reports readiness (it pings the DB and reports
`narratorrConfigured`), and the container has a matching `HEALTHCHECK`. Configure Narratorr in
Settings to actually request anything.
**Real deployment:**
```bash
cp .env.docker.example .env # fill SESSION_SECRET (+ SETTINGS_KEY), pick an auth method
# (LOCAL_AUTH and/or OIDC_*), set NODE_ENV=production, AUTH_BYPASS=0
docker compose up -d --build
# then open the app, sign in (first user = admin), and configure Narratorr + notifications in Settings
```
In production the app **refuses to start** without a `SESSION_SECRET`, refuses `AUTH_BYPASS`
entirely, and requires at least one auth method (`LOCAL_AUTH` or an OIDC provider) — so a
misconfigured deployment fails fast rather than exposing an open admin. The build is a 3-stage
`node:24-slim` image that runs as the non-root `node` user; the DB persists in the
`narratorr-requests-data` volume (`/data`).
> **Upgrading:** migrations apply automatically on boot and are **forward-only** (no
> down-migrations). Before upgrading a populated instance, **snapshot the
> `narratorr-requests-data` volume**; to roll back, restore that snapshot and redeploy the
> previous image (an older image can't run against a newer schema).
### Published images
CI (`.github/workflows/docker.yml`) builds a multi-arch (amd64/arm64) image and pushes to
**Docker Hub `narratorr/narratorr-requests`** and **GHCR `ghcr.io/tjiddy/narratorr-requests`**:
- **Release** — push a semver tag on `main` (`git tag v1.0.0 && git push origin v1.0.0`) → `:latest`,
`:1.0.0`, `:1.0` + a GitHub Release.
- **Bleeding edge** — every push to `develop` (or a manual workflow run) → `:develop`.
Quality gates (lint/typecheck/test/build) run first, and the pushed image is smoke-tested before
the job succeeds. Requires repo secrets **`DOCKERHUB_USERNAME`** and **`DOCKERHUB_TOKEN`**.
## Verify
```bash
pnpm verify # lint + test + typecheck + build
```
## Contributing
Issues and PRs welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) (branch off `develop`) and the
[Code of Conduct](CODE_OF_CONDUCT.md). Found a security issue? See [SECURITY.md](SECURITY.md) —
please report privately, not as a public issue.
## License
[GPL-3.0](LICENSE) © Todd Johnson