Next.js is pinned to 16.2.10; APIs and file conventions may differ from training data. Before changing Next-specific behavior, read the relevant guide under node_modules/next/dist/docs/ and heed deprecations.
- Use Node
24.xandpnpm@10.26.1; CI enables pnpm with Corepack and installs withpnpm install --frozen-lockfile. - CI quality order is
pnpm lint->pnpm typecheck->pnpm test->pnpm test:performance-contracts->pnpm test:railway-template->pnpm build. - Focused unit tests:
pnpm vitest run path/to/file.test.ts(vitest.config.tsincludes**/*.test.{ts,tsx}and usesjsdom). - E2E:
pnpm test:e2estartspnpm db:migrate:startup && pnpm dev --hostname 127.0.0.1 --port ${PLAYWRIGHT_PORT:-3100}and needs Postgres; setSTACKRAY_E2E_USE_SYSTEM_CHROME=trueto use system Chrome like CI. - DB-backed smoke tests need Postgres and startup migrations first:
pnpm test:scan-pipeline-smokeexercises fake scanners plushttp/intel/browserworkers;pnpm test:railway-templatevalidates the Railway service template. pnpm buildrunsnext buildand thenscripts/copy-next-standalone-assets.ts;pnpm startalways runspnpm db:migrate:startupbefore.next/standalone/server.js.pnpm dev:localis the default local stack: it creates.env.localif missing, chooses per-worktree ports, starts Postgres/MinIO, runs migrations, seedsadmin@stackray.local/StackrayDev123!, then runs host Next.js plus Docker workers.pnpm dev:local:downstops this worktree's Docker services without deleting volumes;pnpm dev:local:wipedeletes local Postgres/MinIO volumes.
.env.localtakes precedence over.env, and setup scripts never overwrite an existing.env.local; check it is not pointing at Railway before worker or migration testing.- Before launching
pnpm dev:local,pnpm dev, or local hands-on QA servers, check tmux for an existing Stackray dev session. In the main~/projects/stackraycheckout, reuse the existing session if one is running; in a separate worktree, start that worktree's own tmux session withpnpm dev:localso it gets isolated ports, Compose project, and volumes. - Local app runs on the host; Postgres and MinIO run in Docker; worker containers provide
httpx,nuclei,subfinder, nuclei templates, and browser/screenshot Linux dependencies. - First local stack defaults: app
http://localhost:3000, Postgres127.0.0.1:5432, MinIO API127.0.0.1:9000, MinIO console127.0.0.1:9001withminioadmin/minioadmin. - Worker roles are split by
STACKRAY_WORKER_ROLE:httphandleshttp_probe/run_scan,intelhandles subfinder/nuclei/ip/finalize/schedules,browserhandles headless/browser fallback, andallhandles every task. - Worker production images are intentionally slim and built by
worker/Dockerfile; when changing worker startup commands, runtime imports, scripts, or assets, verify the Dockerfile copies every required file into/app.
- Stackray intentionally uses HTTP/JSON plus SSE, not
tRPC,oRPC, or GraphQL. The web UI, agent API, and workers share persisted records as the source of truth. - API/UI entrypoints live under
app/; product route handlers call services inlib/server/**; shared payload schemas live inlib/contracts/**; worker orchestration lives inworker/**. drizzle/schema.tsis the database source of truth;lib/db/schema.tsis only the app-facing re-export.- Product-resource API routes should accept either Better Auth session cookies or bearer API keys via
requireSessionOrBearerActor; account/admin/API-key/user-product-state routes should stay session-only viarequireAppSession.lib/session/route-auth-boundaries.test.tsenforces this split. - Queue Graphile work through
enqueueGraphileJob; scan creation queueshttp_probe, and worker task selection is role-based rather than one monolithic worker path. - Scan progress is SSE from persisted
scan_events; results must be written before being streamed.
scripts/startup-migrate.tsis the canonical runtime migration path. Keep it: it loads.env.local/.env, takes a Postgres advisory lock, applies checked-in Drizzle migrations, then runs Graphile Worker migrations.- Normal schema changes: edit
drizzle/schema.ts, runpnpm db:generate, runpnpm db:migrate:startupagainst local Postgres, test withpnpm dev:local, and commit the generated SQL plusdrizzle/migrations/meta/*. - Do not rewrite the checked-in
0000_*baseline for normal schema evolution. Current migration history already includes incremental0001+files; append the next migration. - Avoid manually editing generated Drizzle artifacts (
drizzle/migrations/*.sql,drizzle/migrations/meta/*) during normal work. If generated SQL looks wrong, fix the schema/source and regenerate. - A full migration-history reset is exceptional and intended only for fresh/reset databases and template cutovers. If any database has applied an older migration lineage, do not reset checked-in history without an explicit reconciliation plan.
scripts/startup-migrate.test.tsintentionally checks_journal.json, SQL files, snapshots, the0000_*baseline, and important historical migrations.
- Scanner binaries are not npm deps. Pins live in
worker/scanner-pins.jsonand are mirrored intoworker/Dockerfileandworker/Dockerfile.dev; update withpnpm scanners:updateorpnpm scanners:update:patch, not by hand. - The worker image builds from the
CarlosCommits/httpxfork plus pinnednuclei,subfinder, and nuclei-template refs. CI only builds the scanner image when scanner-impacting files changed. - For technology detection, keep layers separate: detection rules in
lib/server/scans/custom-wappalyzer-fingerprints.json, display metadata inlib/server/scans/custom-technology-metadata.json, generated upstream catalog inlib/server/scans/generated/wappalyzer-catalog.json, and repo-local nuclei templates underworker/nuclei-templates/registered inworker/nuclei.ts. - Never hand-edit
lib/server/scans/generated/wappalyzer-catalog.json; refresh it withpnpm wappalyzer:update-catalogand inspect catalog changes withpnpm wappalyzer:diff-catalog -- [base] [head] --limit N. - Custom Wappalyzer fingerprints are passed to
httpxwith-cffin primary-td, screenshot-td, and runtime-tdhpaths. Modern SPA evidence may require the pinnedhttpxfork, not just a JSON fingerprint. - Register worker-used Nuclei templates in
NUCLEI_TEMPLATE_DEFINITIONS; repo-local templates userepoLocal: trueand run by-tpath, while upstream templates run by-idunlessNUCLEI_TEMPLATES_DIRis configured. - TXT DNS fallback rules should stay YAML-backed in nuclei templates; do not add duplicate hardcoded TXT signature constants in TypeScript.
- Treat every server-rendered page and route handler as a query plan. Before adding data dependencies, identify which reads are required for first paint, which are independent, and which can be deferred or streamed.
- Drizzle queries, authentication lookups, and other non-
fetchreads that may be repeated during one render should use Reactcache()for request-scoped deduplication. Prefer stable primitive cache arguments. Do not use request caching as a substitute for efficient SQL or indexes, and never cache mutations, job enqueueing, or other side effects. - Run independent read-only operations concurrently with
Promise.allonly after verifying independence, transaction behavior, ordering, rate limits, and side effects. Keep genuinely dependent operations sequential. - List and search endpoints must filter, order, and paginate in SQL before hydrating rows in Node.js. Do not load an unbounded table or result inventory and then sort, filter, deduplicate, or paginate it in application code. If derived-data filtering requires an exception, document it and keep the default path bounded.
- Every list endpoint must enforce a maximum page size. Fetch at most
limit + 1rows when determining whether another page exists. - Nonessential aggregates, filter-option inventories, exports, and secondary panels should not block initial route rendering. Load them on interaction or behind a Suspense/loading boundary where appropriate.
- New queries over growing tables must be reviewed against their filter, join, and ordering columns. Add a generated Drizzle migration for a supporting index when existing indexes do not match the access pattern.
- Performance-sensitive route loaders should have tests covering query bounds, pagination behavior, request-level deduplication, and safe parallel fan-out. Avoid tests that only verify rendered output while permitting unbounded data access.
- Treat folder
index.tsfiles as public external entrypoints only. Do not add barrel exports for module-internal helpers. - For external scan artifacts, favicons, Wappalyzer icons, screenshots, and prototype previews, do not blindly replace raw
<img>withnext/imageunless the source is internal, proxied/cached, or explicitly configured. - Use
flatMapfor simple map-and-filter transformations where each input returns either[]or[value]; use onefor...ofloop when partitioning one array into multiple outputs. - Use
toSorted()for immutable sorting instead of[...items].sort(...). - Use
SetorMapfor repeated membership checks or lookups inside loops. Keep array order semantics explicit when changing deduplication code. - Avoid array indexes as React keys unless the index represents a fixed spatial position. Prefer stable domain identifiers or stable value keys.
- Avoid numeric
&&rendering in JSX. Use explicit boolean conditions so0cannot accidentally render as text or disappear as a valid value. - Use deterministic date/time formatting for server-rendered text; volatile timestamps belong client-side or behind explicit locale/time-zone formatting.
- Event listeners opened in effects should use named callbacks and remove each listener in cleanup before closing the underlying resource.
- Do not export Next.js
metadatafrom a client component. If metadata matters for a client-only page, split the route into a server page/wrapper plus a client component. - For scan artifacts, favicons, Wappalyzer icons, screenshots, and prototype previews that can come from arbitrary external domains, do not blindly replace raw
<img>tags withnext/image. Usenext/imageonly when the source is internal, proxied/cached through our domain, or safely covered by explicit image configuration and sizing. - Export image capture: iOS/WebKit rasterizes html-to-image's SVG
foreignObjectin a detached document anddecode()resolves before inner raster images finish decoding there, so a single canvas draw leaves the scan screenshot/favicon blank while inline SVG icons render (WebKit bug 219770). Keep export copy/download flows routed throughcaptureExportPngBlob/captureExportPngDataUrlincomponents/shared/image-export.ts, which redraw the same SVG image multiple times on iOS before reading the canvas. - For dead code cleanup, prefer removing
exportfrom module-internal symbols before deleting code. Keep contract schemas, view-model shape types, and behavior-oriented worker/session helpers unless a focused API/entrypoint review proves they are not intentional surface area. - When Tailwind width and height utilities use the same value, prefer
size-*.