# Architecture Decision Records

## ADR-001 — Unified Next.js app + separate worker (2026-08-03)
**Decision:** Single Next.js 16 application (App Router, route handlers) with
a standalone BullMQ worker process, instead of a separate NestJS API.
**Why:** One deployable and one type system; SSR dashboards need the data
layer anyway; business logic lives in `src/lib` so a later API split stays
cheap. Worker isolation keeps crawls off the request path.

## ADR-002 — Prisma 6, not Prisma 7 (2026-08-03)
**Decision:** Pin `prisma@6` / `@prisma/client@6`.
**Why:** Prisma 7 landed with a new generator/driver-adapter architecture
that is still churning. v6 is the stable line with the classic
`prisma-client-js` generator and needs no driver adapters. Revisit once v7
settles.

## ADR-003 — Custom session auth instead of Auth.js (2026-08-03)
**Decision:** Hand-rolled session auth: argon2id hashes, 256-bit tokens
stored as SHA-256, HttpOnly/SameSite=Lax/Secure cookies, DB-backed sessions.
**Why:** Auth.js treats credentials logins as second-class (JWT-only, no DB
sessions) and complicates email-verification/reset flows. Our flows need
lockout, session revocation on reset, and audit hooks — simpler and more
auditable first-party. OAuth (Google/Microsoft) will be added via the
`Account` model with standard OAuth libraries.

## ADR-004 — RBAC in code, custom roles in DB later (2026-08-03)
**Decision:** Role→action sets live in `src/lib/auth/rbac.ts`; `Role` /
`Permission` tables exist but are unused for now.
**Why:** Six fixed roles are testable as a pure function. DB-driven custom
roles are an agency-tier feature; the schema keeps the door open.

## ADR-005 — SSRF guard as the single fetch path (2026-08-03)
**Decision:** All fetches of user-supplied URLs go through
`safeFetch`/`assertSafeUrl` (manual redirects, DNS pre-validation, byte/time
caps). Plain `fetch()` on user URLs is banned by convention and review.
**Why:** The crawler is the platform's biggest attack surface; one hardened
choke point beats scattered checks.

## ADR-006 — Fail-fast queue producer (2026-08-03)
**Decision:** `enqueueCrawl` races a 5 s timeout; on failure the crawl row is
marked FAILED with "queue unavailable".
**Why:** BullMQ producers hang indefinitely when Redis is down
(`maxRetriesPerRequest: null`); a hidden hang violates the "failures must be
visible" rule.

## ADR-007 — In-memory rate limiting for now (2026-08-03)
**Decision:** Fixed-window in-memory limiter with a minimal interface.
**Why:** Single-instance deployments don't need Redis for this; the
interface is designed so a Redis store can replace the Map when we scale
horizontally (tracked in TASKS.md).

## ADR-008 — Outbound email address (2026-08-03)
**Decision:** Default `MAIL_FROM=seo@northwestcar.group`; canonical web URL
remains `https://seo.northwestcar.group`.
**Why:** Owner supplied "seo@northwestcar.group" mid-build; it is an email
address, so it became the sender identity. Both values are env-configurable
if intended differently.

## ADR-009 — pgbouncer=true against pooled local Postgres (2026-08-03)
**Decision:** Document `?pgbouncer=true` for `prisma dev`'s local server and
any transaction-mode pooler.
**Why:** Observed `prepared statement "s0" already exists` — the pooled
endpoint can't share prepared statements; the flag disables them.
