Repository navigation
Architecture
Applies to the current
mainimplementation. Last source review: 2026-08-19.
Nooklet has one supervised application container with separate Node.js processes for the Next.js web server and background worker. The default Compose deployment also starts an internal YouTube proof-of-origin provider sidecar. SQLite is the durable system of record and the cross-process coordination boundary; media and download files remain on operator-controlled filesystems. This separation keeps the UI available when an unhealthy Docker Desktop bind mount blocks a worker-side filesystem call.
For repository metrics and deeper implementation evidence, see the engineering dossier. For the decisions that shaped this design, see Architecture Decisions.
flowchart LR
Person["User browser"]
Proxy["TLS reverse proxy\noperator managed"]
App["Nooklet web\nNext.js request process"]
Worker["Nooklet worker\njobs + downloads + imports"]
DB[("SQLite\n/app/data/nooklet.db")]
Stage["Download work + completed staging\nDOWNLOAD_ENGINE_WORK_DIR / DOWNLOAD_ENGINE_DIR"]
YouTubeWork["YouTube work\nYOUTUBE_WORK_DIR"]
Media["Movie, TV, and YouTube libraries\napproved roots"]
AI["OpenAI-compatible provider"]
Metadata["TMDB / TVDB"]
History["Plex / Tautulli / Trakt"]
Indexers["Newznab indexers"]
News["Usenet server"]
YouTube["Public YouTube"]
Pot["Internal PO-token provider"]
Notify["Discord / Apprise / webhook"]
Person --> Proxy --> App
App <--> DB
Worker <--> DB
Worker <--> Stage
Worker <--> YouTubeWork
Worker <--> Media
App --> AI
App --> Metadata
App --> History
App --> Indexers
Worker --> AI
Worker --> Metadata
Worker --> History
Worker --> Indexers
Worker --> News
Worker --> YouTube
Worker --> Pot
Worker --> Notify
The web and worker do not use a direct in-process or HTTP control channel: web requests persist state and job/control intent in SQLite, and the worker claims it there. Both processes can call remote services for the workflows they own. The reverse proxy is not included in the shipped container. Operators exposing Nooklet beyond loopback are responsible for TLS, ingress restrictions, and correct proxy-header handling. See Security Model.
flowchart TD
UI["App Router pages and components"]
Boundary["Server actions and API route handlers"]
Domain["Domain commands, queries, and workflows"]
Ports["Repositories and typed adapter capabilities"]
DB[("Drizzle + SQLite")]
External["Remote services"]
Files["Approved filesystems"]
Worker["Job worker and download runner"]
UI --> Boundary
Boundary --> Domain
Worker --> Domain
Domain --> Ports
Ports --> DB
Ports --> External
Ports --> Files
The architectural rule is that UI code delegates work through a server action or route boundary and does not call vendor adapters directly. Workflows own orchestration, validation, persistence, and failure semantics. Cross-module production code must not import another module's repositories/ or adapters/ internals; modules with a public.ts facade expose narrow capabilities there. npm run boundaries:check enforces that focused private-folder rule, but does not prove every possible module cycle or internal boundary. The governing intent is recorded in ADR-0001.
sequenceDiagram
actor User
participant UI as App Router UI
participant Boundary as Server action / route
participant Auth as Auth and validation
participant Workflow as Domain workflow
participant Data as SQLite / durable job intent
participant Adapter as External adapter
User->>UI: Submit a task
UI->>Boundary: Typed task input
Boundary->>Auth: Authenticate and authorize
Auth-->>Boundary: User and policy context
Boundary->>Workflow: Validated command
Workflow->>Data: Read or persist workflow state
opt Remote capability required
Workflow->>Adapter: Typed provider request
Adapter-->>Workflow: Validated response
end
Workflow->>Data: Persist outcome and audit data
Workflow-->>Boundary: Domain result
Boundary-->>UI: User-safe result
Most application behavior uses Next.js server actions. The stable HTTP surface is intentionally small and documented in HTTP API.
A season request is modeled above the physical transfer lifecycle. download_fulfillments records the user outcome and active strategy; download_requests records each pack or episode release attempted to reach it.
flowchart LR
UI["Season request"] --> Plan["Durable fulfillment"]
Plan --> Pack["Season-pack search"]
Pack --> Attempt["Physical download request"]
Attempt --> Import["Import + coverage reconciliation"]
Import -->|Complete| Done["Succeeded"]
Import -->|Missing coverage| Episodes["Per-episode fallback"]
Pack -->|No usable pack| Episodes
Pack -->|Pack larger than staging filesystem| Episodes
Episodes --> Child["Independent episode attempts"]
Child --> Import
Worker["15-second worker"] -->|Resume nextAttemptAt| Plan
Plan -->|Storage/configuration failure| Blocked["Blocked with corrective message"]
The fulfillment owns plan-scoped release exclusions, an eight-probe search-pass ceiling, three submitted transfers per pack or episode cycle, retry timing, and grouped Activity presentation. download_requests.submittedAt keeps preflight rejects out of the transfer budget while retaining them as exclusions. Release failures and packs proven larger than the staging filesystem can advance to another pack or episode fallback. Capacity held by active downloads waits without consuming the release; current free-space/mount, destination, downloader, credential, and compatible-indexer failures block fan-out with a corrective action. A successful pack is reconciled against current monitored, aired episode coverage instead of being assumed complete.
This boundary keeps user intent separate from acquisition evidence and makes recovery restart-safe without adding an external queue service. See ADR-0003 and Downloads and Import.
- Next.js App Router supplies React Server Components, server actions, and route handlers.
- Auth.js uses a credentials provider, encrypted JWT cookies, and an
auth_sessionsvalidity registry that provides revocable, absolute-lifetime 24-hour sessions. - Drizzle maps the normalized schema to SQLite. Migrations run during database readiness initialization.
- A Node supervisor runs database migration once, then starts distinct web and worker children. Next.js instrumentation deliberately does not load the worker graph in the production web child.
- The persisted job worker polls every 15 seconds. Its pass state is written atomically beside SQLite so the web process can evaluate progress without importing worker code.
- The built-in downloader uses a worker-process async runner and an NNTP connection pool for the active transfer. Control intent and speed are persisted because process-local signals cannot cross the web/worker boundary.
- The YouTube module owns discovery, source synchronization, a durable one-at-a-time download runner, yt-dlp/ffmpeg subprocesses, restart-safe work under
YOUTUBE_WORK_DIR, and containment-checked publication into approved YouTube library paths. - The default Compose deployment starts a separate, internal
youtube-pot-providerservice. It is not published to the host; yt-dlp in the application container reaches it over the Compose network. - Storage capacity is sampled by a disposable probe child. Pages read SQLite snapshots and never probe bind mounts directly.
-
tiniis PID 1 in the Docker image; the supervisor forwards termination and applies bounded child shutdown.
Primary sources: instrumentation, database client, job worker, engine runner, YouTube runner, Compose services, and Dockerfile.
The physical modules under src/modules are:
| Module | Primary responsibility |
|---|---|
admin |
Administrative queries and operational views |
discover |
TMDB-backed discovery rails and title search |
download-engine |
NZB parsing, NNTP transfer, repair, extraction, and engine queue |
downloads |
Durable season fulfillment, request association, enqueue, built-in import, retry, and cancellation reconciliation |
identity-access |
Login, authorization, and first-admin bootstrap |
indexers |
Indexer configuration, search, normalization, and protected result storage |
instance-config |
Persisted stable ownership for shared instance configuration |
jobs |
Persisted schedules, claims, leases, and job history |
media-library |
Libraries, folders, titles, episodes, scans, monitoring, and release selection |
notifications |
Channel configuration, dispatch, and delivery audit |
preferences |
Per-user recommendation and history defaults |
readiness |
Capability-level setup and health evaluation |
recommendations |
AI runs, enrichment, history, feedback, and analytics |
service-connections |
External connection configuration, verification, and encrypted secrets |
storage |
Persisted staging/destination snapshots and isolated capacity inspection |
users |
Accounts, roles, password hashing, and recovery state |
watch-history |
Manual, Plex, Tautulli, and Trakt history synchronization |
youtube |
Public discovery, monitors, source sync, durable transfer, cookie access, and safe library publication |
credential-vault and metadata appear in older design documents as conceptual ownership areas, but they are not physical module directories in the current tree. Secret handling is implemented across src/lib/security, service-connections, and indexer repositories; metadata adapters currently live primarily in service-connections and media workflows.
flowchart TB
Host["Docker host"]
subgraph Container["Nooklet container"]
Tini["tini"] --> Supervisor["Node.js supervisor"]
Supervisor --> Web["Next.js web process"]
Supervisor --> Jobs["Background worker process"]
Supervisor --> Probe["Disposable storage probe"]
Jobs --> Engine["Download-engine runner"]
Jobs --> YouTubeRunner["YouTube runner + yt-dlp / ffmpeg"]
end
Provider["youtube-pot-provider sidecar"]
Volume[("nooklet-data volume")]
DownloadMount["Host download folder"]
MediaMount["Host media folders"]
Host --> Container
Web <--> Volume
Jobs <--> Volume
Probe <--> Volume
Engine <--> DownloadMount
YouTubeRunner <--> Volume
YouTubeRunner --> Provider
YouTubeRunner <--> MediaMount
Jobs <--> MediaMount
Probe <--> MediaMount
The shipped Compose configuration persists /app/data in a named volume, publishes only application port 42021 on loopback by default, and keeps the provider sidecar internal to the Compose network. Media and download bind mounts are operator configuration. See Storage and Path Mapping.
- One supervised application container and its internal provider sidecar are the supported Compose topology. The web and worker are separate OS processes, not horizontally scalable replicas.
- SQLite and durable control rows coordinate the two children. Process-local import locks remain worker-only implementation details and are not distributed coordination primitives.
- A process restart reclaims persisted jobs through leases, but an interrupted native download is parked. Explicit Resume restarts it from its stored NZB rather than resuming individual segments.
- Season recovery schedules and renewable per-plan work leases survive restart in SQLite. The maintenance loop is serialized inside the single supported worker process.
- The worker drains an active pass during normal termination. A stale persisted heartbeat degrades health and is logged once, but does not terminate the worker; only an actual exit is restarted. Explicit shutdown retains a bounded force-kill ceiling.
- Shared service, indexer, library, and path configuration resolves through one persisted instance owner and does not change merely because that user's role or enabled state changes.
- Media-mount failures are contained from ordinary web navigation, but Nooklet cannot repair a damaged host disk or cancel a filesystem syscall already in uninterruptible kernel sleep.
- A single Usenet service connection is currently resolved. Multi-server priority and block-account scheduling described in ADR-0002 are not implemented.
- Current authentication is local credentials only. Trakt accepts an OAuth access token as connection data, but Nooklet does not expose Trakt as an Auth.js sign-in provider.
- Jellyfin is not a current service-connection or watch-history source despite references in older planning documents.
- App Router source
- Domain modules
- YouTube module
- Database schema
- Architecture principles
- Durable season fulfillment decision
- Filesystem isolation decision
- Project structure note
- Engineering dossier
Related: Data and Background Jobs | Downloads and Import | Security Model
Nooklet repository · Features · User guide · Engineering dossier · Issues · Private security advisory
Documentation last reviewed against main: August 19, 2026.
Nooklet
Start here
- Getting started
- Guided Docker setup
- Docker installation
- Native installation (advanced)
- First-time setup
Configure
Use Nooklet
- Discover and recommendations
- Library and requests
- YouTube monitoring
- Downloads and import
- Watch history
- Automation and notifications
Operate
- Health and diagnostics
- Backup, restore, upgrades
- Accounts and users
- Troubleshooting
- Multi-instance boundaries
Engineering