This file is the authoritative technical reference for agentic coding assistants working on ShadowBrain. Follow these guidelines exactly.
# Start dev server
pnpm dev
# Build for production
pnpm build
# Run all tests
pnpm test
# Run tests in watch mode
pnpm test:watch
# Lint
pnpm lint
# Format all code
pnpm format
# Type check
pnpm typecheck
# Run the full verify chain (lint → typecheck → build → test → knip)
# This is the local "all green" bar for opening a PR.
pnpm verifyPolicy: When fixing a CodeQL / code-scanning alert (taint-flow queries like
request-forgery, sql-injection, prototype-pollution, etc.), verify the
fix with the local CodeQL analyzer before claiming success. Do not reason
about what the analyzer models as a sanitizer/sink — run the query and get
ground truth. For ordinary bugs, lint, typecheck, or feature work, use
pnpm test / typecheck / lint instead — CodeQL is heavy (~30 s DB build
- query eval) and wasteful for non-CodeQL problems.
Usage:
# Default: run js/request-forgery query
./scripts/codeql-scan.sh
# Explicit rule alias
./scripts/codeql-scan.sh request-forgery
# Full javascript-code-scanning suite (87 queries, what GitHub Actions runs)
./scripts/codeql-scan.sh full
# Arbitrary query/suite file
./scripts/codeql-scan.sh path/to/Query.qlExit codes: 0 = no alerts (clean) · 2 = alerts found · 1 = setup/usage error.
Bootstrap install (one-time, if ~/.local/share/codeql-cli/codeql is missing):
mkdir -p ~/.local/share/codeql-cli
curl -sL -o /tmp/codeql.tar.gz \
https://github.com/github/codeql-action/releases/latest/download/codeql-bundle-linux64.tar.gz
tar xzf /tmp/codeql.tar.gz -C ~/.local/share/codeql-cli
# -> creates ~/.local/share/codeql-cli/codeql/codeqlThe script prints these instructions automatically if the CLI is absent.
The local scan script (
scripts/codeql-scan.sh) is set up by #61 — Security: CI guardrails (CodeQL, lint CI, Renovate, SECURITY.md). Until that issue lands, the script path above is the contract the implementation must create (exit codes, usage, bootstrap instructions must all match this section).
ShadowBrain is a single Next.js (App Router) application — not a monorepo.
src/
├── app/ # Next.js App Router pages and API routes
├── db/ # Database layer (better-sqlite3 + schema)
│ ├── schema.ts # Table definitions
│ ├── migrations/ # SQL migration files
│ └── index.ts # Query helpers
├── lib/ # Utilities (auth, storage, ai prompts)
├── components/ # React components (shadcn/ui based)
├── hooks/ # Custom React hooks
└── test/ # Test setup and helpers
- Use
@/alias for src:import { getItem } from "@/db/index" - Type-only imports:
import type { ContentItem } from "@/db/schema" - Order: builtins → external → internal → types
- Strict mode enabled
- Use
typekeyword (unions), notenum - Never use
as any— create type guards - Schema types inferred from table definitions
- For Zod schemas, use
z.infer<typeof schema>for the TypeScript type; do not duplicate the shape as a hand-written interface
- Files: kebab-case (
error-handler.ts), PascalCase for components - Functions: camelCase
- Types/Interfaces: PascalCase
- Constants:
UPPER_SNAKE_CASEfor global values (API base URLs, retry limits, error codes).camelCasefor config objects, theme values, and module-level defaults.
- Semicolons: yes
- Quotes: double
- Trailing commas: es5
- Print width: 80
- Indent: 2 spaces (no tabs)
- Arrow parens: always
- End of line: lf
package.json carries the app's major.minor.patch version. The project is
pre-1.0 (0.x.y) — major is not yet, so API stability is not promised.
Bump the version in package.json on the same branch as the change when it is
relevant:
-
patch (
0.2.x → 0.2.y) — bug fixes, internal refactors, dependency bumps, and other changes with no user-visible behavior change. -
minor (
0.x.0) — any new user-facing feature, new API endpoint, new UI surface, or other meaningful capability that adds something a user can see or call. -
major (
x.0.0) — reserved for the first stable release. Do not bump major until the API surface and data model are considered stable. -
Patch the README badge too.
package.jsonand the shields.io version badge inREADME.md(the[]line in the header) must be bumped in lockstep — a version bump that leaves the badge stale is a half-applied change.
When in doubt, bump minor — under 0.x it is cheap to add more, and the
version is meant to be a rough signal of how much capability has landed, not
a contract.
- Use
better-sqlite3synchronously for reads, async wrappers for API routes - Write migrations as SQL files in
src/db/migrations/ - Don't edit applied migrations — add a new one. The migration history is the audit trail (especially under the App Security Baseline: it records every schema change).
- Open connections briefly; close after each operation
- WAL mode enabled for concurrent access
content_items carries two independent visibility flags — is_hidden
and is_private — both defaulting to 0 (visible). The read helpers
in src/db/repositories/content-items.ts and src/db/search.ts take
includeHidden / includePrivate options (both default to false)
and hide any row whose flag is set without the matching opt-in. A row
with both flags set requires both opt-ins to be returned.
The opt-ins are gated behind authentication at the route layer
(requireAuthenticated(request) from src/lib/auth/guard.ts): the
admin can opt in via ?include_hidden=1 / ?include_private=1 on
GET /api/items, GET /api/items/[id], GET /api/search,
PATCH /api/items/[id], and DELETE /api/items/[id]. The body of
POST /api/items and PATCH /api/items/[id] accepts an
is_hidden / is_private field (admin-only). An unauthenticated
request always sees the strict default and the route returns 401 —
the opt-in cannot be used to bypass auth.
- Use Zod schemas for all API input validation
- Define schemas alongside route handlers, not in a separate file
- Return proper HTTP status codes
- Log detailed errors server-side; return generic messages to clients
- Security failures (401, 403, 429) must return generic messages to the client; the specific reason is logged server-side and to
audit_logs— never echo internal paths, DB errors, or stack traces - See
docs/superpowers/specs/2026-06-19-app-security-baseline-design.md§Error Handling for the full policy
- Required for auth endpoints (
/api/auth/login) — ≈5 attempts / 15 min / IP. - Required for all other API routes — ≈120 req / min / IP.
- Required for non-API routes — ≈600 req / min / IP.
- Implemented in
src/lib/rate-limit.ts(in-memory token bucket per IP) by #56 — Security: global rate limiting. The proxy (src/proxy.ts) is the enforcement layer: it picks the right bucket per path, consumes a token, and returns 429 +Retry-Afterbefore the request reaches the route handler. Route handlers do not consume tokens of their own — the login route only callsresetLoginRateLimit(ip)on a successful login so a legitimate user is not penalised for typos. - Reads the real client IP from the configured trusted proxy header. The header name is the
TRUSTED_PROXY_HEADERenv var (defaultX-Forwarded-For; nginx typically sets it viaproxy_set_header X-Forwarded-For $remote_addr;). When the configured header is missing, the IP falls back to"unknown"and every request lands in the same bucket — that is a deployment problem (no trusted proxy), not a code bug. - Returns
429withRetry-Afteron exceed. The 429 response is generic (no internal paths, no DB errors) and carries the same security response headers (CSP, HSTS, …) as every other code path; see the App Security Baseline design spec §Error Handling.
The session-auth foundation lives in src/lib/auth/ and is enforced at two layers:
- Proxy (
src/proxy.ts) — Next.js's renamedmiddleware(Next 16+). Protects every route except/loginand/api/auth/*; checks the CSRF origin on state-changing methods; redirects unauthenticated browser navigations to/login?from=…and returns 401 for unauthenticated API calls. Sliding renewal re-signs the cookie on activity soSESSION_MAX_AGEalso acts as an inactivity timeout. - Route guard (
src/lib/auth/guard.ts) —requireAuthenticated(request)is called at the top of every protected route handler as a defense-in-depth check; a unit test that invokes the handler without going through the proxy still fails closed.
Auth library modules: session.ts (HMAC-signed cookies, clamping, sliding renewal), password.ts (bcrypt + OWASP ASVS V3.2.2 constant-time login via a precomputed dummy hash), csrf.ts (origin/referer check, constant-time compare), exempt-paths.ts (exact-pathname matching — no suffix/prefix), audit.ts (auth event log to audit_logs), client-ip.ts, constants.ts.
Required env vars: ADMIN_USERNAME, ADMIN_PASSWORD_HASH (bcrypt hash, cost ≥ 10), SESSION_SECRET (min 32 chars, used to sign cookies). SESSION_MAX_AGE is optional (ms; clamped to [1h, 30d]; default 24h). TRUSTED_PROXY_HEADER is optional (default X-Forwarded-For; the rate limiter and audit log read the client IP from this header — see ### Rate Limiting above and the App Security Baseline design spec §5). Generate the password hash with pnpm hash:password — a hidden-prompt script in scripts/hash-password.ts that reuses the app's bcryptjs and BCRYPT_COST so the hash is guaranteed to verify against the login route.
Test helper: authedRequest(url, init) in src/db/test-utils.ts signs a session cookie using the test SESSION_SECRET, so existing route tests can call the protected handlers directly.
- Use
Promise.all()for parallel independent operations - Avoid N+1 queries — batch where possible
- All URL-fetch endpoints (bookmark auto-fetch, image capture) MUST use
validateFetchUrlfromsrc/lib/ssrf.tsbefore making any HTTP request. - The validator blocks private / loopback / link-local IP ranges, rejects
non-http(s) schemes, and returns a
safeLookupcallback that re-validates the IP at connect time to prevent DNS rebinding. - See the App Security Baseline design spec (docs/superpowers/specs/2026-06-19-app-security-baseline-design.md §7) for the full policy.
- Use shadcn/ui components (built on Tailwind CSS)
- Dark mode is the default theme
- TanStack Query for server state if complexity warrants it, otherwise fetch + SWR
The pre-PR walkthrough lives in docs/agents/review-checklist.md. Walk through it before opening a PR.
The end-to-end flow for taking work to a green PR lives in docs/agents/pr-workflow.md.
Issues are tracked in GitHub Issues. See docs/agents/issue-tracker.md.
Standard triage labels: needs-triage, needs-info, ready-for-agent, ready-for-human, wontfix. See docs/agents/triage-labels.md.
Single-context: one CONTEXT.md at repo root. See docs/agents/domain.md.