Code-path breakdowns 2026.08

Twelve traces.
Real files.

The dossier draws the boundaries. This page walks the code. Every node in every diagram names the file that owns that step, and every claim links to the source on main. Where the implementation disagrees with a decision record, the implementation wins and the disagreement is recorded rather than smoothed over.

Breakdown 01

Writes re-establish trust at their entry point.

Nooklet exposes three HTTP route files. Most mutations travel through sixteen "use server" files across src: fifteen under src/app, plus the layout sign-out action. The built-in queue's authenticated POST handler is the deliberate HTTP exception. Each protected entry point authenticates, validates its input, and delegates to a focused module operation.

TRACE 01 · SERVER ACTION WRITE PATH

From form submission to durable row

  • Primary path
  • Short-circuit
  • Return edge
CLIENT Form submit connections/connection-form.tsx EDGE Redirect gate, not authorization src/proxy.ts SERVER ACTION auth() then role check settings/connections/actions.ts NO SESSION Typed error, no write action-state.ts BOUNDARY SCHEMA Zod parse of the FormData service-connections/schemas/ INVALID INPUT Field errors to the form useActionState ABUSE CONTROL consumeRateLimit src/lib/security/rate-limit.ts OVER LIMIT formatRetryAfter message rate_limits, fixed window MODULE WORKFLOW Orchestration, the public surface service-connections/workflows/ REPOSITORY Transaction owner for this write service-connections/repositories/ PERSISTENCE Drizzle over SQLite in WAL mode src/lib/database/client.ts CACHE INVALIDATION revalidatePath next/cache server re-render of the route

A form submission reaches src/proxy.ts, which redirects unauthenticated requests but performs no authorization. The server action then calls auth() and checks the role, parses the FormData with a Zod schema, and consumes a rate limit. Each of those three steps can short-circuit to a typed error state without writing anything. On success the action calls a module workflow, which calls the repository that owns this transaction through Drizzle. The action finishes with revalidatePath, and the server re-renders the affected route.

Trace 01. The proxy redirects; it does not authorize. A server action is a directly callable POST endpoint, so protected action files re-establish identity rather than trusting that the request passed through the matcher. Login and bootstrap use their own pre-session validation and rate limits; the queue POST authenticates and delegates its validated control request to a workflow.

Breakdown 02

Server-rendered reads do not make an HTTP hop.

A server component imports a module query as an ordinary function and calls it during render. There is no fetch, no serialization boundary, and no endpoint in between. The three route files that do exist serve authentication callbacks, the container health probe, and the caller-scoped queue API used for polling and controls.

TRACE 02 · SERVER COMPONENT READ PATH

Render-time query, no endpoint

SERVER COMPONENT dynamic = force-dynamic (workspace)/library/page.tsx IDENTITY await auth() inside the component src/auth.ts MODULE QUERY A plain function call media-library/queries/ THE ONLY ROUTE HANDLERS Three narrow HTTP files api/auth/[...nextauth] api/health api/downloads/queue PERSISTENCE ensureDatabaseReady, then select src/lib/database/client.ts RESPONSE Streamed HTML, no client fetch React Server Components

A server component marked force-dynamic awaits auth(), then calls a module query as an ordinary imported function. The query calls ensureDatabaseReady and runs a Drizzle select in the same process, and the component streams HTML back. Alongside the spine, an aside notes the only three route files in the application: the Auth.js catch-all, the health probe, and the authenticated built-in queue API. Server-rendered page data stays on the direct query path; the queue is the explicit client-polled exception.

Trace 02. Fourteen production query files call ensureDatabaseReady() directly rather than going through a repository. That is an accepted variance from the documented layering, not an accident: reads may shape a result for one screen without a repository round trip, while most writes use repository-owned transaction seams.

Breakdown 03

A module publishes three folders and hides four.

commands/, queries/, and workflows/ are the public surface. Repositories, adapters, schemas, and types are internal, reached only through that surface. Folders are created when real code lands, so most modules carry a subset rather than the full template.

TRACE 03 · MODULE ANATOMY

The import allowlist, and where it bends

  • Allowed import
  • Forbidden import
CALLER Server action or server component src/app/ PUBLIC SURFACE WRITE, ONE STEP commands/ validate, write, audit READ SEAM queries/ shaped for a screen ORCHESTRATION workflows/ multi-phase, ordered INTERNAL DATABASE repositories/ only write path OUTBOUND adapters/ vendor calls VALIDATION schemas/ Zod at the edge SHAPES types/ inferred rows ENFORCED PUBLIC SURFACE CROSS-MODULE CONSUMERS Import target public.ts facades repositories and adapters stay internal REPOSITORY CHECK boundaries:check rejects bypasses production modules are scanned

A caller in src/app imports from a module's public surface: commands, queries, or workflows. Inside the module boundary sit four internal folders that callers must not import directly: repositories, adapters, schemas, and types. A forbidden-import edge is drawn piercing the module boundary straight into repositories. Below the boundary, target modules expose explicit public.ts facades and a repository validator rejects production cross-module imports into repository or adapter internals.

Trace 03. Cross-module production code imports target module facades instead of repository or adapter internals. npm run boundaries:check makes that rule executable; server actions still enqueue durable work through the jobs module's public API instead of doing filesystem work in the request process.

Breakdown 04

A workflow orchestrator only names the order.

The canonical shape is a folder of phase files behind a thin index.ts that awaits each one in sequence and branches nowhere. Indexer search is the cleanest example in the repository: eight phases, thirty-three lines, no conditionals.

TRACE 04 · WORKFLOW PHASE PATTERN

searchIndexersWorkflow, phase by phase

request-validation indexer-selection credential-resolution indexer-execution normalization filtering-scoring persistence audit HOW CONSISTENTLY IT IS APPLIED THE EXEMPLAR 8 phase files, 33-line index indexers/workflows/search-indexers/ SAME SHAPE, LARGER 13 phase/helper files downloads/import-completed-downloads/ NOT SPLIT AT ALL One file, no phase folder downloads/workflows/season-fulfillment.ts

The searchIndexersWorkflow orchestrator awaits eight phases in order: request-validation, indexer-selection, credential-resolution, indexer-execution, normalization, filtering-scoring, persistence, and audit. Beside it, three examples of how consistently the pattern is applied across the repository: search-indexers with eight phase files behind a thirty-three line index, import-completed-downloads with thirteen phase/helper files in the same shape, and season-fulfillment which is a single file with no phase folder at all.

Trace 04. Treat the phase folder as an intent, not a guarantee. The orchestrator carrying no business logic is what makes a workflow testable — the sibling index.test.ts mocks each phase and asserts only ordering and propagation. Where a workflow has not been split, that test cannot exist in the same form.

Breakdown 05

One application container, one supervisor, two long-lived children.

A wedged bind mount blocks in the kernel and cannot be cancelled from JavaScript. Enough blocked calls exhaust the process-wide filesystem thread pool and page rendering stops. The answer is process isolation: filesystem work never runs in the web runtime. The YouTube proof-of-origin provider is a separate internal Compose sidecar and is outside this application-container supervision tree.

TRACE 05 · SUPERVISION TREE

Startup order and exit policy

  • Spawns
  • Fatal exit
  • Recoverable exit
PID 1 tini -g -- Dockerfile ENTRYPOINT SUPERVISOR container-supervisor.mjs forces NODE_ENV=production first ONE SHOT, AWAITED worker.cjs --migrate-only migrations finish before any child starts WEB server.js Next.js standalone never touches a mount WORKER worker.cjs jobs, engine, imports owns all filesystem work DISPOSABLE PROBE --refresh-storage-snapshots killed at a fixed deadline the only scheduled statfs WEB EXITS The whole container shuts down beginShutdown, 10s SIGKILL ceiling WORKER EXITS Restart with exponential backoff 1s doubling to a 30s cap

tini is PID 1 and starts container-supervisor.mjs, which forces NODE_ENV to production and then runs the worker bundle once with --migrate-only, awaiting its exit so migrations complete before any long-lived process opens SQLite. It then spawns two long-lived children: the Next.js web server and the background worker. Separate storage-probe children are disposable; each performs one bounded refresh and exits. If the web process exits, the supervisor shuts the whole container down. If the worker exits or its persisted heartbeat stops advancing, it is restarted with exponential backoff starting at one second and capped at thirty seconds. Normal termination first asks the worker to drain its active pass.

Trace 05. The asymmetry is the design. A worker failure is contained and recoverable, so it backs off and restarts, and the failure counter resets to one once the worker has survived sixty seconds. A web failure is not recoverable in place, so the container exits and the orchestrator restarts it cleanly rather than serving a half-dead runtime. A watchdog also recycles a child whose heartbeat is stale for 120 seconds by default; SIGTERM drains healthy in-flight work before the ten-second kill ceiling.

Breakdown 06

Filesystem lanes run alone; network lanes run together.

Every fifteen seconds the worker runs one pass. The three lanes that touch the filesystem execute serially and first, then maintenance runs by itself, and only then do the four network and AI lanes run concurrently.

TRACE 06 · WORKER PASS ORDER

One tick, seven job lanes plus maintenance

TIMER Every 15 seconds, one pass src/lib/jobs/worker.ts SERIAL 1 OF 3 download-import filesystem SERIAL 2 OF 3 media-title-delete filesystem SERIAL 3 OF 3 media-library-scan filesystem ALONE, NO OVERLAP runMaintenancePass() retention, engine, imports, season plans PARALLEL watch-history allSettled PARALLEL recommendation allSettled PARALLEL missing-content allSettled PARALLEL metadata-refresh allSettled

A fifteen second timer starts one worker pass. Three filesystem lanes run serially in order: download-import, then media-title-delete, then media-library-scan. Maintenance then runs alone with no other lane overlapping it. Finally four network and AI lanes run concurrently through Promise.allSettled: watch-history-sync, recommendation-run, missing-content-search, and metadata-refresh.

Trace 06. The three filesystem lanes are also excluded from heartbeat-driven progress: only real completion advances the worker's progress timestamp for those types. A responsive JavaScript timer must never be able to report a healthy worker while a scan is blocked on a wedged mount.

TRACE 06B · CLAIM AND LEASE

Ownership is proved by the write, not a lock

STEP 1 SELECT due candidates nextRunAt reached, and no live lease STEP 2 Conditional UPDATE re-asserts the same predicate in the WHERE STEP 3 changes > 0 wins then a 30s heartbeat extends a 5 minute lease

Claiming a job selects due candidates whose lease is free, then issues a conditional UPDATE that re-asserts the same predicate in its WHERE clause. If that update reports more than zero changed rows, this worker owns the job. A heartbeat every thirty seconds then extends the five minute lease for as long as the job runs.

Trace 06B. There is no lock table and no advisory lock. Two workers racing the same row both pass the SELECT, but only one UPDATE matches the predicate, so only one sees a non-zero change count. A crashed worker needs no cleanup: its lease simply expires and the row becomes claimable again.

Breakdown 07

The download engine drains one release at a time.

Admission happens in the web process against a persisted capacity snapshot. Everything after that runs in the worker: a single drain loop claims one queued row, fetches every segment over a pool of NNTP connections, and writes decoded payloads straight into the assembling file at their byte offsets.

TRACE 07 · ENGINE RUNNER LOOP

Claim, transfer, and classify the outcome

  • Primary path
  • Defer and retry
  • Loop
PEEK Next queued row, by priority download-engine/queue/engine-repository.ts LIVE CAPACITY Real statfs, for this one release download-engine/runtime/live-capacity.ts NOT ENOUGH ROOM Mark waiting, defer this one smaller releases still run CLAIM Compare and swap to fetching expectedStates guard LOST THE RACE Defer, take the next row no lock table needed FRESH START Clear the work directory no per-segment resume yet TRANSFER downloadNzb, pooled NNTP yEnc decoded at its byte offset VERDICT CASCADE Transport before content only content may blocklist FINALIZE Repair, extract, move out download-engine/finalize/ drain until no candidate remains

The engine runner peeks the next queued row by priority, then checks live capacity with a real statfs for that single release; if there is not enough room it marks the row waiting and defers it so smaller releases can still run. It claims the row by compare and swap into the fetching state, losing the race means deferring to the next row. It clears the work directory for a fresh start, transfers every segment over pooled NNTP connections writing each decoded yEnc part at its byte offset, classifies the outcome with transport failures checked before content failures, and finalizes. The loop then repeats until no candidate remains.

Trace 07. The verdict order is load-bearing. A transport-exhausted transfer is checked before an unrecoverable one because only a content verdict may blocklist a release — reporting a broken news server as a damaged release would send the caller hunting through every other candidate for the same episode. Exhausted transfers are parked as paused rather than failed, because a terminal state discards the stored NZB and only a paused row can resume.

TRACE 07B · FINALIZATION

Eight steps from raw payload to importable media

STEP 1 Deobfuscate by magic bytes STEP 2 par2 verify repair if needed STEP 3 Inspect archives quota checked first STEP 4 Staged extract isolated staging dir STEP 5 Verify and merge path guards STEP 6 Rescue media unnamed payloads STEP 7 Remove artifacts par2 volumes STEP 8 Move to output one sequential copy

Finalization runs eight steps in order: deobfuscate files by magic bytes, verify and repair with par2, inspect archives against an extraction quota before extracting anything, extract into an isolated staging directory, verify and merge the staged tree with path traversal guards, rescue unnamed media, remove par2 artifacts, and finally move the work directory to the output directory in one sequential copy.

Trace 07B. Step 8 is the only time data crosses from the engine work directory onto the operator's download mount, and it is deliberately one sequential pass. That split is why two directory environment variables exist rather than one: random heavy I/O stays on the container volume, and the bind mount is touched once.

Breakdown 08

Seven states are declared; all are reachable.

The engineDownloadStates enum now contains only states written by the runner, and every transition is a compare-and-swap that names the states it expects to find.

TRACE 08 · ENGINE STATE MACHINE

Transitions that actually exist

  • Transition
  • Failure
  • Resumable
queued fetching repairing extracting completed paused failed

A row starts queued and moves to fetching. From fetching it branches to either repairing or extracting, and both converge on completed. Fetching can also move to paused, either by user intent or because the transfer was parked, and paused returns to queued. Failure from an active state moves to failed. Assembly itself happens while the row is fetching.

Trace 08. The former dead assembling value has been removed because yEnc parts are assembled in place during fetching. paused still carries two distinct meanings: a user pressed pause, or the engine parked a transfer whose server broke. Both are resumable, which is exactly why parking is not modelled as a failure.

Breakdown 09

Fifty tables in one SQLite file.

One schema file declares every table. Most entity rows use text identifiers and millisecond timestamps; relationship tables use composite or parent keys, and foreign keys declare explicit delete behavior. Service credentials live in dedicated side tables instead of parent rows.

TRACE 09 · SCHEMA BY DOMAIN

Which module owns which tables

IDENTITY AND ACCESS · 5 users preferences audit_events rate_limits auth_sessions MEDIA LIBRARY · 9 media_libraries media_library_paths media_titles media_title_external_ids tv_seasons tv_episodes media_files media_scan_runs media_request_attempts DOWNLOADS · 7 download_clients download_fulfillments download_fulfillment_episodes download_requests download_queue_items download_import_runs download_imported_files INDEXERS · 6 indexers indexer_media_categories indexer_secrets indexer_search_runs indexer_search_results indexer_search_result_secrets RECOMMENDATIONS · 6 recommendation_runs recommendation_items recommendation_run_metrics recommendation_item_states recommendation_feedback recommendation_item_timeline_events WATCH HISTORY · 3 watch_history_sources watch_history_sync_runs watch_history_items PLATFORM · 8 service_connections service_secrets storage_snapshots jobs notification_channels notification_channel_events notification_dispatch_audit instance_configuration DOWNLOAD ENGINE · 1 engine_downloads YOUTUBE · 5 youtube_sources youtube_source_selections youtube_videos youtube_source_videos youtube_downloads REVOCABLE JWT SESSIONS Encrypted browser token auth_sessions rejects stale cookies after sign-out

Fifty tables grouped by owning domain: five for identity and access, nine for the media library, seven for downloads, six for indexers, six for recommendations, three for watch history, eight platform tables covering stable instance ownership, service connections, storage snapshots, jobs and notifications, one for the download engine, and five for YouTube sources, selections, videos, membership, and downloads. Auth.js keeps encrypted JWTs in browser cookies while each token must also match an unexpired auth_sessions row and the user's current monotonic authentication generation.

Trace 09. The jwt callback requires the live user, matching authentication generation, and a per-login auth_sessions row. Sign-out deletes that row before clearing the cookie, so a late response cannot make an old token valid again. Disabling an account or writing a password advances the generation and removes active rows in the same transaction, including logins still pending issuance. Tokens missing any validity claim fail closed and require a fresh sign-in. The engine_downloads row also carries the full NZB payload, which is what lets a restart re-run a transfer without returning to the indexer.

Breakdown 10

The web process never calls statfs.

A hung mount blocks in the kernel and takes the filesystem thread pool with it. So the request path reads a persisted snapshot instead, refreshed on a schedule by a disposable child process that can be killed at a deadline.

TRACE 10 · CAPACITY POLICY

Who is allowed to touch a bind mount

  • Request path
  • Snapshot read
WEB PROCESS Enqueue an NZB enqueue-nzb-download.ts no syscall on a mount READ, DO NOT PROBE readDownloadCapacity reads storage_snapshots two rows, work and output FRESHNESS GATE Older than 2 minutes refuse with storage_unavailable reserve 512 MiB, size times 2 WORKER PROCESSES · THE ONLY PLACE statfs RUNS DISPOSABLE CHILD Storage probe worker.cjs with a flag killed at a fixed deadline THE BLOCKING CALL statfs on both roots refresh-storage-snapshots.ts may hang; the parent kills it PERSISTED RESULT storage_snapshots reachable, writable, free every page reads this table ENGINE RUNNER inspectLiveEngineCapacity a real statfs, immediately before claiming the snapshot can be two minutes stale PER RELEASE, NOT GLOBAL Can this one run right now too large means defer, not refuse smaller releases keep draining

In the web process, enqueuing an NZB calls readDownloadCapacity, which reads two rows from the storage_snapshots table rather than probing the filesystem, and refuses the request if the snapshot is older than two minutes. Inside the worker boundary, a disposable probe child runs the only scheduled statfs against both storage roots and writes the result back to storage_snapshots, which the web process then reads. Separately the engine runner performs a real live statfs immediately before claiming a row, asking only whether this one release can run now, so an oversized release is deferred rather than blocking every smaller one.

Trace 10. Two checks exist because they answer different questions. Admission asks “is there plausibly room, according to something we already know” and must never block. The runner asks “is there room for this exact release, right now” and is allowed to block, because it is in the process that is permitted to.

Breakdown 11

Four boundaries, each enforced at the point of use.

Stored credentials, outbound requests, filesystem paths, and abuse controls each have a dedicated module in src/lib/security/. None of them are middleware; every one is called by the code that is about to do the dangerous thing.

TRACE 11 · SECURITY BOUNDARIES

Where each guard sits

STORED CREDENTIALS KEY DERIVATION HKDF-SHA256 from the box key falls back to AUTH_SECRET ENVELOPE AES-256-GCM, versioned previous keys allow rotation OUTBOUND REQUESTS RESOLVE, THEN JUDGE assertOutboundHostAllowed classifies the resolved address PINNED DISPATCH createPinnedLookup the socket cannot re-resolve FILESYSTEM PATHS CONTAINMENT realpath, then compare roots APPROVED_MEDIA_ROOTS ISOLATED VARIANT Canonicalize in a child process a wedged mount times out instead ABUSE CONTROLS FIXED WINDOW consumeRateLimit counters live in rate_limits CLIENT IDENTITY Headers trusted only on request TRUST_PROXY_HEADERS gates it

Four security boundaries. Stored credentials derive a key with HKDF-SHA256 from the secret box key, falling back to AUTH_SECRET, and seal values in a versioned AES-256-GCM envelope that supports key rotation. Outbound requests resolve the hostname first and classify the resolved address, then pin those addresses into the dispatcher so the socket cannot re-resolve. Filesystem paths are canonicalized with realpath and compared against approved roots, with an isolated variant that canonicalizes in a child process so a wedged mount times out. Abuse controls use fixed-window counters in the rate_limits table, and forwarding headers are trusted only when explicitly enabled.

Trace 11. The SSRF guard is resolution-time, not string-time. A hostname that looks perfectly public but resolves to a link-local or metadata address is rejected, and because the resolved addresses are pinned into the dispatcher, a second lookup cannot return something different after the check passed. The allowlist re-permits ordinary private ranges host by host, which a self-hosted application genuinely needs.

Breakdown 12

Every repository opens the database the same way.

There is no exported connection singleton. Each caller invokes ensureDatabaseReady(), which is cheap after the first call and carries the migration gate, the pragma set, and a compatibility assertion with it.

TRACE 12 · DATABASE BOOTSTRAP

From module import to a usable connection

MODULE LOAD env parsed eagerly, at import src/lib/env.ts CONSEQUENCE Build needs a placeholder Dockerfile injects AUTH_SECRET ENTRY POINT ensureDatabaseReady() called by every repository CACHE PROBE State on globalThis survives dev hot reload PRAGMAS WAL, NORMAL, foreign keys on busy_timeout 5000 MIGRATION GATE Journal compared as a string migrate only when it differs THE SIGNATURE drizzle/meta/_journal.json any byte change re-runs migrate REPAIR, THEN ASSERT Add drifted columns, then verify fails loudly at open, not at query

The env module parses its schema eagerly at import, which is why the Dockerfile injects a throwaway AUTH_SECRET at build time. Every repository calls ensureDatabaseReady, which probes cached state stored on globalThis so it survives development hot reload, opens the connection and applies four pragmas including WAL journaling and foreign keys, compares the raw contents of drizzle/meta/_journal.json against the cached signature and runs migrations only when they differ, then applies compatibility repairs for drifted columns and asserts the schema before returning.

Trace 12. The migration gate is a raw string comparison of the journal file, not a version number or checksum — cheap to evaluate, and it re-runs migrations whenever that file changes by any byte. The compatibility repair afterwards exists because a column once arrived outside the migration sequence; the hard assert that follows means an unrepairable database fails at open with a clear message rather than at some later query.

Appendix

How the reported drift was resolved.

Each previously reported mismatch was reviewed against the implementation. The rows remain as a decision log: one dead state was corrected in code, while intentional constraints and ownership choices were made explicit in the ADRs.

Disposition of previously reported architecture drift
Reviewed item Disposition Resolution
credential-vault as a physical module Accepted architecture ADR-0001 now defines it as a responsibility: shared crypto lives in src/lib/security/, while each domain owns its secret records and policy.
metadata as a physical module Accepted architecture ADR-0001 now records metadata as a capability exposed by service-connections, not a placeholder directory.
Durable engine_segments rows Accepted constraint ADR-0002 now specifies in-memory segment scheduling, aggregate persisted progress, and safe whole-transfer restart from the stored NZB.
assembling engine state Rectified in code The dead enum value and dependent branches were removed; an upgrade migration normalizes any historical row before the narrowed state model is used.
Repositories and adapters are module-internal Not a drift ADR-0001 and the structure guide scope this to production cross-module imports; boundaries:check enforces exactly that invariant.
Database access outside repositories Accepted architecture ADR-0001 now permits query-owned read projections and focused workflow/configuration/maintenance transactions while keeping reusable persistence and ordinary writes in repositories.
Multiple prioritized Usenet servers Accepted constraint ADR-0002 now fixes the current topology at one canonical instance-wide server; primary/block-account failover remains a possible future decision.