Conventions
Read this before writing any code in this repo. Several people and agents work in parallel; these rules are what keep the result one codebase.
Ground rules
Section titled “Ground rules”- Clean room. Upstream projects (Sonarr, Radarr, Prowlarr, Bazarr, Seerr, in
../reference/) are behaviour references. Read them to learn what happens; write how yourself. Never copy code, regexes, SQL or comments from them. Test inputs and expected values are data and may be used as fixtures. - TypeScript strict, ESM, Node 24.
tsconfig.base.jsonis the law (exactOptionalPropertyTypes,noUncheckedIndexedAccess). Noanywithout a comment saying why. - One package per concern, one module per feature. A
packages/*library has zero knowledge of the database or HTTP; it is pure logic + external-service clients, testable in isolation. Anapps/api/src/modules/*module owns its tables, routes, services and jobs and talks to packages. - Contract first. Every request/response shape lives in
packages/shared/src/<area>.tsas a Zod schema with an inferred type, exported frompackages/shared/src/index.ts. The api validates with it; the web parses with it. Never define an API shape anywhere else. - Tests are Vitest, colocated as
*.test.ts, run withpnpm test. Pure packages aim for fixture-driven tests; external clients are tested against recorded or hand-written response fixtures infixtures/, never live services. - No new dependencies without a reason in the PR/commit message. Prefer Node built-ins
(
fetch,node:crypto,node:fs/promises). Approved so far: fastify (+cors, static, websocket), fastify-type-provider-zod, zod, drizzle-orm, better-sqlite3, croner, pino, fast-xml-parser (indexers), react, @tanstack/react-query, vite. - Dates are ISO-8601 strings across the API and DB (
textcolumns). Durations are milliseconds as integers. Sizes are bytes as integers. - Logging through the pino logger passed in context; never
console.*in api code. - Errors: throw typed errors (
class XError extends Errorwith acode) from packages; modules map them to HTTP in one place.
Module layout (apps/api/src/modules/<name>/)
Section titled “Module layout (apps/api/src/modules/<name>/)”schema.ts drizzle tables for this module (re-exported by src/db/schema.ts)service.ts business logic; takes `AppContext` + typed inputs; no Fastify typesroutes.ts Fastify routes under /api/v1/<name>; thin: validate → service → replyjobs.ts scheduler.register(...) calls, if anyindex.ts export const <name>Module: AppModule = (ctx) => async (app) => { routes; jobs }*.test.tsA module never imports another module’s schema.ts directly; it imports from
src/db/schema.ts (the aggregate) so cross-module joins are visible in one place.
Package layout (packages/<name>/)
Section titled “Package layout (packages/<name>/)”package.json name "@arclight/<name>", type module, exports "./src/index.ts"src/index.ts public API onlysrc/** implementationfixtures/** test dataPackages depend on @arclight/shared for domain types when they are part of the API
contract; otherwise they define their own types and shared re-exports what the API needs.
Naming
Section titled “Naming”- Files:
kebab-case.ts. Types/classes:PascalCase. Functions/vars:camelCase. DB columns:snake_casein SQL,camelCasein Drizzle property names. - Series/episode terms follow TV convention:
series,season,episode,absoluteEpisode,airDate. Movie terms:movie,year,edition. Both sharetitle,sortTitle,cleanTitle,monitored,qualityProfileId,rootFolderPath,path,tags. - A “release” is a candidate download found at an indexer. A “grab” sends it to a download client. An “import” moves a completed download into the library. “Wanted” is monitored and missing or below cutoff.
Quality vocabulary
Section titled “Quality vocabulary”Quality definitions are an ordered enum shared by parser, profiles and decision engine:
see packages/shared/src/quality.ts. Use its ids and names everywhere; do not invent
quality strings.
Commit messages
Section titled “Commit messages”Imperative subject ≤ 72 chars, body explains why, ends with the Co-Authored-By line the session provides.