Developer conventions — Note SPBoucher AI
Read this before touching the code. Stack: Next.js 16 (App Router, React 19, TypeScript strict), Tailwind v4 (design tokens in src/app/globals.css: bg, bg-elev, bg-sunk, fg, fg-muted, fg-faint, line, accent, ai, danger, success, warning), Drizzle ORM + PostgreSQL 17 + pgvector (src/db/schema.ts, snake_case casing), pg-boss worker (src/worker), official openai + @anthropic-ai/sdk behind src/lib/ai.
Hard rules
- Do not edit
src/db/schema.tsand do not add migrations. Every table you need already exists (see schema). Flexible data goes in the existingjsonbcolumns (pages.properties,database_properties.config,database_rows.values,database_views.config,ai_executions.*,settings,dashboard_layouts.widgets,experiments.params/metrics,references.fields). - Do not add npm dependencies (no
pnpm add). Installed and available: next, react, drizzle-orm, postgres, pg-boss, zod, openai, @anthropic-ai/sdk, @tiptap/* (core, react, starter-kit, list, table, image, mention, mathematics, details, code-block-lowlight, drag-handle-react, file-handler, bubble-menu, extensions), lowlight, katex, motion, @tanstack/react-query, zustand, react-force-graph-2d, pdf-parse, mammoth, papaparse, turndown, marked, jszip, bibtex-parse-js, nanoid, sanitize-html, react-markdown + remark-gfm/remark-math/rehype-katex, @dnd-kit/, cmdk, lucide-react, date-fns, qrcode, otplib, @simplewebauthn/, @radix-ui/* (dialog, dropdown-menu, popover, tooltip, select, switch, tabs, scroll-area, checkbox, slot, separator, toast, context-menu, avatar), class-variance-authority, clsx, tailwind-merge, tippy.js, pino, dotenv. - Never run
pnpm build, never start anything on port 8480 (a production server is running there). Typecheck withnpx tsc --noEmit. To try your feature in a browser usepnpm exec next dev -p <your port>with the.envin the project root (it already points to the real database through an SSH tunnel on 127.0.0.1:15432; ifnc -z 127.0.0.1 15432fails, runssh -f -N -L 15432:127.0.0.1:5432 M4M64c). Login: usernamesimon, password = the value ofOWNER_INITIAL_PASSWORDin~/Desktop/Cluster/secrets/note-spboucher-ai/.env(read it with grep; never print it in your report). The worker is also running (/tmp/note-worker.log); you can run a second worker withnpx tsx src/worker/index.tsif you change handlers (stop it when done). - Only touch the files/directories assigned to you; do not reformat other files. New routes go in
src/app/(app)/<route>/page.tsx(server component that callsrequireUserPage()+getWorkspace()then renders a client component). - Secrets: never log API keys. AI calls go through
src/lib/aionly (router → providers); record usage withrecordUsage. - UI quality bar: minimalist, dense, premium; reuse
src/components/ui/*primitives (Button,Input,Dialog,Popover,Menu*,Tabs*,Badge,Switch,Tip,Empty,Spinner,toast). Responsive: every screen must work on a 390px-wide phone (stack columns, horizontal scroll for wide tables, touch targets ≥ 40px) and on desktop. Dark mode via tokens only (no hard-coded colours). Keyboard: Escape closes, Enter confirms.
Server-side helpers
handler(({ req, user, ws, params }) => Response)fromsrc/lib/api.tswraps a route handler with auth + workspace + error mapping;parseBody(req, zodSchema);HttpError(status, msg);uuidzod helper. Example:src/app/api/pages/[id]/route.ts.db,schemafrom@/db;audit(userId, action, target?, detail?)from@/lib/audit.- Pages service
src/lib/pages/service.ts(createPage,updatePage,getPage,listTree,softDeletePage, …). Pages are the universal container:type= page | database | research; database rows are pages withdatabase_idset (row page content = the row's body). - Search:
hybridSearch(wsId, q, { limit, pageIds, sourceTypes }),indexSource(...)insrc/lib/search/index.ts; chunking inchunk.ts; embeddings inembed.ts. - AI:
route({ task, tools?, schema?, vision?, estTokens?, preferred? })fromsrc/lib/ai/router.tsreturns{ model, provider, effort, reason }; thencallAI({ model: model.id, provider, system, messages, jsonSchema?, tools?, effort })(non-streaming) orstreamAI(...)(async generator of{type:"text"|"tool_call"|"done"|"error"}) fromsrc/lib/ai/providers.ts;sse(generator)fromsrc/lib/ai/chat.tsturns a generator into an SSE Response. Task classes: chat | quick | write | reason | deep | code | extract | longctx | agent | judge. Prompts:src/lib/ai/prompts.ts(INJECTION_GUARDmust be appended to any system prompt that sees workspace content; wrap workspace content in<workspace>tags). - Jobs:
enqueue(QUEUES.x, data)fromsrc/lib/jobs; handlers insrc/worker/handlers/*(imports must use relative paths like../../db/index, not@/, and must not importnext/*). - Editor JSON helpers (
src/lib/editor/text.ts):docText,docBlocks,toMarkdown,emptyDoc; client:markdownToDocinsrc/lib/editor/markdown-client.ts.
Client-side helpers
api(url, { method, json })andsse(url, body, onEvent, signal)insrc/lib/client/api.ts; UI storeuseUI()insrc/lib/client/store.ts(openAI({ pageId, prefill }), theme, panels);cn,timeAgo,fmtDate,moneyinsrc/lib/utils.ts;Markdownrenderersrc/components/markdown.tsx;Editor(src/components/editor/Editor.tsx, props: content, onChange, pageId, editable).- Pages are opened at
/p/<id>;PageView.tsxrendersDatabaseViewfortype === "database"andResearchPanelfortype === "research". - React Query keys in use:
["tree"](sidebar),["ai-models"],["convs", pageId],["search", mode, q]. Invalidate["tree"]after creating/renaming/deleting pages.
Reporting
When done: run npx tsc --noEmit (must be clean for your files), list the files you created/changed, what you verified in the browser or with curl, and anything left incomplete. Do not commit.