CLAUDE.md — tools.spboucher.ai
Curated encyclopedia of tools (public) + owner inventory (private). Stack: Next.js 16 App Router, React 19, Tailwind 4, PostgreSQL 17, Drizzle (snake_case casing), postgres-js, Vitest, Playwright. Port 8540.
Non-negotiables
- Public ≠ private at the DB layer. Public code uses
sqlPublic(roletools_reader, SELECT onpublic_toolsview only). Never importsql/dbfromsrc/dbinto anything undersrc/lib/publicor public routes. Owner tables (owner_notes,projects) are never joined into public queries. - Every admin mutation goes through
requireUser(req)(session + MFA + same-origin +x-csrf-token) and writesaudit_log(+tool_revisionsfor records). Never add client-only protection. - Evidence rules (
docs/editorial-guide.md,docs/source-policy.md): no prices, no certifications without a policy source, no benchmark claims, no copied vendor text,checked_atonly for pages actually loaded. Runpnpm content:validateafter editingcontent/. - Outbound HTTP only via
safeFetch(SSRF guard). Fetched text is untrusted; AI output must be zod-validated and never auto-saved. - Secrets only in
.envon the node (600) and~/Desktop/Cluster/secrets/tools-spboucher-ai/. Never in git, manifests, logs or chat. - Migrations: add a new
drizzle/NNNN_*.sql; never edit applied files (checksums). Mirror changes insrc/db/schema.ts. Recreating thepublic_toolsview requires re-granting totools_reader. - Drizzle hijacks postgres-js serializers → the raw
sqlclients use their own pools (seesrc/db/index.ts). Usesql.json(x)for jsonb params.
Workflow
pnpm typecheck && pnpm test && pnpm content:validate before committing. E2E/a11y/security scripts need a running server (pnpm dev or production). Remote-first after deploy: edit on M2M32c:~/apps/tools-spboucher-ai, pnpm build && pm2 restart tools-web tools-worker, commit/push there (docs/deployment.md).
Content authoring
One JSON per tool in content/tools/<slug>.json matching src/lib/content/schema.ts; deep records 100–300 words, ≥3 sources, ≥2 alternatives, comparison notes. Categories are leaf slugs from content/categories.json; alternatives are existing slugs. Private records → content/private/ with visibility: private. Owner relevance → content/owner/relevance.json.