This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in node_modules/next/dist/docs/ (resolved from this file's directory; in monorepos the next package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by next dev — verify at node_modules/next/dist/server/lib/generate-agent-files.js. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
These are the persistent engineering instructions for any AI agent (Claude, Codex, or otherwise) working in this repository. They are operational, not exhaustive — for product and architectural knowledge, read the docs listed below before making non-trivial changes.
- docs/PRODUCT.md — what TunarrTube is, who it serves, core user journeys, terminology.
- docs/ARCHITECTURE.md — the architecture that actually exists: structure, data flow, integrations, persistence, build/deploy.
- docs/DECISIONS.md — decisions that are explicit or strongly evidenced in the code, and open questions that aren't.
- README.md — user-facing setup, workflow, and troubleshooting; kept in sync with actual behavior and a reliable source of truth for intended behavior.
app/— Next.js App Router: pages (app/**/page.tsx, mostly async Server Components reading Prisma directly) and API route handlers (app/api/**/route.ts).components/— client ("use client") UI components. They call theapp/api/**routes withfetchand hold local state; there is no client-side state library.lib/— the actual application: one directory per domain service (sources,downloads,jobs,tunarr,cache,playback,settings,metadata,thumbnails,youtube,system,logging,db), plusapi.ts(response helpers) andvalidation.ts(zod schemas). Route handlers are thin: parse input with zod, call alib/function, wrap the result withok()/toErrorResponse().prisma/—schema.prisma(SQLite) and committed migrations.storage/— local media and thumbnails (dev default; overridable by env/Settings).tests/— vitest unit/integration tests, run against real modules withfetch/child_processstubbed viavi.stubGlobal.
- Service layer owns behavior; routes stay thin. Business logic, Prisma calls, and side effects belong in
lib/<domain>/service.ts. A new API route should mostly parse (zod), call a service function, and returnok(...)/toErrorResponse(...). - Errors are
AppError. Thrownew AppError(code, message, httpStatus, details?)(fromlib/api.ts) for any expected failure;toErrorResponseturns it into{ error: { code, message, details } }. Don't invent ad-hoc error shapes. - Validate at the boundary. Every route that accepts a body validates it with a schema in
lib/validation.tsbefore calling into a service. Add new input shapes there, not inline in the route. - BigInt fields must go through
serialize().fileSizecolumns are PrismaBigInt; any response including them must be wrapped inserialize()(fromlib/api.ts) orJSON.stringifywill throw. - Never write outside the configured media root. Use
assertWithinDirectory(lib/settings/service.ts) for any path built from user- or DB-derived segments before touching the filesystem. - Downloads/writes are temp-then-rename. Follow the existing pattern in
lib/downloads/service.ts(write to a temp path,renameinto place) so a crash or failed job never leaves a half-written file recorded as complete. - Sanitize before logging. Route error messages, process output, and
writeLogcalls throughsanitizeLogValue(lib/logging/service.ts) — it redacts signed YouTube/Googlevideo URLs and--cookiesflags. Never log raw yt-dlp stderr or stream URLs. - Background work goes through the job queue. Don't spawn
yt-dlp/FFmpeg or call the Tunarr API directly from a request handler for anything that can be slow; enqueue aJobviaenqueueUniqueJob(lib/sources/service.ts) and letlib/jobs/runner.tsprocess it. CallkickWorker()after enqueueing so the job runs promptly instead of waiting for the next poll. - This app assumes a single running instance. The job worker and scheduler use in-process
globalThisstate with no distributed locking (seelib/jobs/runner.ts,lib/jobs/scheduler.ts). Do not add code that assumes multiple replicas share work safely against the same SQLite file. - TunarrTube never deletes a previously completed download just because the source video disappeared upstream. Sync marks memberships
missing, it doesn't deleteSourceVideo/media. Preserve this guarantee in any change tosyncSourceor download logic. - The MVP only accepts public HTTPS YouTube URLs on the hosts allow-listed in
lib/youtube/url.ts. Don't add cookie/auth-based extraction without discussing the security implications (credentials would need to reach a background job process).
npm run dev # next dev (predev runs prisma generate + migrate deploy)
npm run build # next build --webpack (prebuild runs prisma generate)
npm start # next start (prestart runs prisma generate + migrate deploy)
npm test # vitest run
npm run test:watch # vitest
npm run typecheck # tsc --noEmit
npm run db:migrate # prisma migrate dev (schema development only)Run npm test and npm run typecheck before considering a change done. There are no component/UI tests in this repo — tests/ covers lib/ logic only.
Edit prisma/schema.prisma, then run npm run db:migrate to generate a migration under prisma/migrations/. Commit the generated SQL. Do not hand-edit committed migration files.
If a change alters routing, data flow, an external integration, the schema, or the build/deploy model, update docs/ARCHITECTURE.md in the same change. If it changes a user-facing workflow, update README.md and, if relevant, docs/PRODUCT.md. Don't let these drift from the code.
If the task already names the files/symbols to touch, SKIP vexp. Otherwise one
run_pipeline({ "task": "..." }) returns ranked pivot files with line ranges and
blast radius. Do NOT open files one by one to find your way around - every extra
tool call costs a turn. Call it again ONLY when the task moves to a new area.
get_skeleton for files to understand, not edit. verify_done before calling a
multi-file task complete, then RUN the tests it names.
Anchor the task on real identifiers (ClassName, functionName) or file paths:
run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })
vexp runs entirely on this machine, index in .vexp/;
run_pipeline transmits nothing to any external service.
On status: "degraded" or 0 pivots the index is still building - use your own tools.
For literal string sweeps use your native search - do NOT route text sweeps through vexp.
Repo SOURCE only: logs, dist/, node_modules/ and files outside the repo are NOT indexed.