Skip to main content

Silvia Project Rules

Architecture

  • Next.js 15 App Router — All routing uses the app/ directory with file-based routing
  • Supabase (PostgreSQL) as the primary database, Neo4j for graph relationships
  • Clerk for authentication — all protected routes go through middleware
  • Vercel AI SDK for AI features — prefer this over raw API calls

Code Style

  • TypeScript strict mode is currently OFF but being migrated toward strict — all new code must include proper type annotations, avoid any, and use explicit return types on functions
  • Path alias: use @/ for imports from the project root
  • Styling: Tailwind CSS with shadcn/ui components in components/ui/ — do not introduce new CSS frameworks
  • Components: functional components only, no class components
  • State management: React Context + TanStack React Query — no Redux or Zustand

Conventions

  • API routes live in app/api/ and use Next.js Route Handlers
  • Shared UI components go in components/
  • Business logic and service clients go in lib/
  • Custom hooks go in hooks/
  • Type definitions go in types/
  • Contexts go in contexts/
  • Tests use Vitest — add unit tests for new utility functions and hooks in __tests__/ directories
  • Keep package.json scripts minimal. A new one needs a named consumer, not a place on a list: see Package scripts below
  • Every PR is linked to a Linear ticket, and the ticket exists from the start of the work. Begin a working session with /new-task (.agents/skills/new-task/SKILL.md), which reuses an existing ticket or creates one and moves it to In Progress. Put the ticket id in the PR title, e.g. Fix X (CFO-8300). Larger efforts start with /new-project. See docs/LINEAR-WORKFLOW.md

Package scripts

A script earns its place by having a NAMED CONSUMER, cited by path and line in the PR that adds it. Rank the kinds, because they are not equally strong:
  • Sufficient on its own: a step in .github/workflows/**, or a config file that invokes it (playwright.config.ts runs npm run e2e:dev as its webServer).
  • Not sufficient on its own: a line in a doc, or a tool’s scaffolding convention. Both are real, and both can be supplied by the same author in the same commit, so either needs a reviewer to agree in the thread.
  • Never counts: the script’s own source file. A header that says “run me with npm run x” is the script describing itself.
Ask why the invocation needs a stable NAME, not why it cannot be run directly. Almost nothing here cannot be run directly, so that test settles nothing: measured on 2026-09-11 there are 41 runnable files under scripts/ and package.json names five of them. The answers that do carry are that CI and a human must run the identical command, that the flags are long enough to get wrong, or that a tool looks the name up itself. Delete a script in the PR that removes its last consumer. Recorded because the previous wording invited the opposite repair. It listed the seven scripts that existed on 2026-03-13, which was a verbatim snapshot of package.json that day rather than a considered allowlist. It was then edited twice to track the file rather than to govern it: dev-turbo was added in c3408765dd to address a review comment, and removed again in f80976e72a. It still drifted to seventeen entries against seven names without anyone noticing, and in one case it bent the code instead, with CI invoking a verifier by path specifically to avoid adding a script the list did not permit. A list of names is not a policy: it goes stale silently, and the only repair it suggests is a longer list.

Clean Code Guidelines

  • Single Responsibility — each function, component, and module should do one thing well
  • Small functions — extract logic into well-named helper functions rather than writing long procedural blocks
  • Meaningful names — variables, functions, and components should clearly describe their purpose. Avoid abbreviations and single-letter names outside of loops
  • No magic numbers or strings — use named constants for values that have meaning
  • DRY but not premature — extract repeated logic only when the pattern appears 3+ times. Don’t abstract prematurely
  • Early returns — prefer guard clauses and early returns over deeply nested conditionals
  • Avoid side effects — functions should be predictable. Keep side effects isolated and explicit
  • Keep components lean — extract business logic into custom hooks or utility functions. Components should focus on rendering
  • Colocation — keep related code close together. Helper functions used by one component should live near that component, not in a global utils file
  • Consistent error handling — handle errors explicitly, never silently swallow them. Use try/catch at API boundaries and propagate errors meaningfully

TypeScript Patterns & Anti-Patterns

Prefer

  • Union types over enums — use type Status = 'active' | 'inactive' instead of enum Status { Active, Inactive }
  • interface for object shapes, type for unions and intersections — be consistent
  • unknown over any — when the type is truly unknown, use unknown and narrow with type guards
  • Discriminated unions — for state modeling: type Result = { status: 'success'; data: T } | { status: 'error'; error: string }
  • as const — for literal inference on objects and arrays: const ROLES = ['admin', 'user'] as const
  • Utility types — use Pick, Omit, Partial, Required, Record instead of manually redefining shapes
  • Type narrowing — use typeof, in, and custom type guards instead of type assertions
  • Explicit return types on exported functions — helps catch regressions and serves as documentation
  • satisfies — use satisfies to validate a value matches a type while preserving the narrower inferred type

Avoid

  • any — never use any in new code. Use unknown, generics, or proper types instead
  • Type assertions (as) — avoid as casts. If you need to assert, it usually means the types are wrong
  • ! non-null assertions — don’t use ! to silence null checks. Handle the null case explicitly
  • @ts-ignore / @ts-expect-error — do not suppress errors. Fix the underlying type issue
  • Overly complex generics — if a generic type requires more than 2-3 type params or nested conditionals, simplify it
  • Function type — use specific function signatures: (args: Args) => ReturnType instead of Function
  • object or {} — these accept almost anything. Use Record<string, unknown> or define the actual shape
  • Barrel exports (index.ts re-exports) — these hurt tree-shaking and slow down builds. Import directly from the source file

Documentation

Before working on a specific subsystem, check docs/ for existing documentation. Key docs include:
  • docs/CLERK-AUTH-ONBOARDING-ROUTING.md — Loop-proof auth/onboarding routing, Clerk session-token claims, write-path invariants
  • docs/ACCOUNT-TYPE-SYSTEM.md — Account types and classification
  • docs/PLAID-RATE-LIMIT-HANDLING.md — Plaid API rate limiting strategy
  • docs/BROKERAGE-CASH-SWEEP-DEDUP.md — Wells Fargo cash-sweep double-count: detection rules, read-time dedup architecture, the account_effective_balances view for SQL consumers, snapshots caveat, rollout flag
  • docs/TRANSFER-IN-TRANSIT-DEDUP.md — bank → brokerage transfers counted twice while the bank debit is pending: the matching rule, the lossless gap guard, every consumer that applies it, rollout flag
  • docs/EARNINGS-CALENDAR-SYSTEM.md — Earnings calendar implementation
  • docs/REFERRAL-SYSTEM.md — Referral system logic
  • docs/TAX-TREATMENT-IMPLEMENTATION.md — Tax treatment handling
  • docs/VALUATION-SURFACES.md — Full map of every surface that prices/values/converts holdings (FX, minor units, net worth). Required reading before touching any holding valuation logic so no surface is missed.
  • docs/SUPABASE-PAGINATION.md — PostgREST’s silent 1,000-row response cap and the fetchAllPages helper. Required reading before adding any read of a per-item × per-day table (snapshot/history tables) — an unpaged select silently truncates and shows wrong money. Also covers chunking unbounded .in() lists.
  • docs/DATABASE-VIEW-RLS-SAFETY.md — security_invoker / RLS rules for views, tables, and functions. Required reading before creating or editing anything in supabase/migrations/ — CREATE OR REPLACE VIEW silently drops security_invoker and turns a per-user view into a fleet-wide RLS bypass (CFO-7727). Agent trigger wrapper: .agents/skills/database-migration-safety/SKILL.md.
  • docs/EXPERIMENTS.md — Required reading before any A/B test. The experiment registry and useExperiment hook: exposure rules, control-by-default, QA overrides, and the constraints on agent-authored experiment PRs
  • docs/WATCHLIST.md — Dashboard watchlist: the watchlist_items table, per-plan limits enforced in the database, and the Silvia skills that document it
  • docs/precious-metals-integration.md — Precious metals pricing integration
  • docs/discourse-integration.md — Discourse community SSO integration
  • docs/SUPPORTED_CHAINS.md — Supported blockchain chains
  • docs/PERSONAL-INFO-TOOL.md — Personal info tool for AI chat
  • docs/blog/agent-rules.md — Blog architecture, Sanity integration, routing, and SEO conventions
  • docs/CHAT-MAIN-FLOWS.md — Required reading before changing chat-page behavior (switching chats, loading, the composer and Send, streaming). The flows users actually perform, measured in PostHog, and the page-level tests that replay them
  • docs/SILVIA-SKILLS-SYNC.md — Required reading before any user-visible UI change. Silvia AI’s knowledge of the UI lives in the separate CFO-Silvia/skills repo and goes stale when the UI changes
  • packages/ui-catalog/README.md — Read before adding or changing a component Silvia puts in chat (GenUI). The one schema-described list shared by the server, web and mobile; the prompt section and skills docs are generated from it, never hand-edited
  • docs/MODULUS-CLEANUP-PLAN.md — Required reading before deleting anything from components/ui/ or removing a “dead” file. Scope is UI only: components, pages, and the stories/tests that exist only for them — never hooks, lib/, app/api/, emails or other logic, even when unreferenced. Records which legacy components Modulus genuinely duplicates, which unreachable files are safe and which only look dead (parked features, work in progress, files other open PRs are editing), the checks every delete PR must run, and the PR-by-PR order
  • .agents/skills/author-blog-posts/SKILL.md — Agent workflow for authoring blog posts directly in Sanity (draft-first, via the Sanity MCP)
  • docs/LINEAR-WORKFLOW.md — How agent work is tracked in Linear: team, Bug/Feature/Maintenance categories, assignee and state rules, and why the ticket must be In Progress from the start of the session (it is how task time is measured). Skills: .agents/skills/new-task/SKILL.md (/new-task) and .agents/skills/new-project/SKILL.md (/new-project)
Always read the relevant doc before modifying these systems — they contain important context not obvious from the code alone.

Silvia AI Skills — UI Knowledge Sync

Silvia (the AI agent users chat with) answers “how do I…” questions only from silvia-navigation-skills/SKILL.md in the CFO-Silvia/skills repo — not from this codebase. If a UI element isn’t documented there, Silvia tells users she can’t help and files a support ticket. Whenever a change touches user-visible UI (pages, sidebar, flows, warnings, banners, plan gating, button/tab names, GenUI components), check whether the skills repo needs a matching update, and if so open a companion PR in CFO-Silvia/skills and link it from your PR description. Full checklist and deploy notes: docs/SILVIA-SKILLS-SYNC.md.

Chat page — test the flows users actually perform

A change to chat-page behavior (switching chats, loading a chat, the composer and Send, streaming or re-attaching, the history panel) must:
  1. Re-measure the affected flows in PostHog with the queries in docs/CHAT-MAIN-FLOWS.md, and replay each one in a page-level test at the user’s speed, asserting what the user can do (for example, Send enabled right after switching and typing). Testing only the property the change improves is not enough.
  2. Keep app/(app)/chat/[[...sessionId]]/__tests__/chat-main-flows.test.tsx passing.
  3. State in the PR any live check that was blocked. A blocked check is not a passed one.
Why: CFO-9358 made switching chats instant and its tests measured how fast a chat appeared. None typed after switching, so a half-second disabled Send reached production (CFO-9375, CFO-9379).

Next.js Dynamic Imports

Every dynamic() call must include a loading fallback:
Why: Without loading, a dynamic() component suspends on first render and bubbles up to the nearest <Suspense> boundary. On the dashboard that boundary has fallback={<DashboardSkeleton>}, causing the entire page to flash back to skeleton when any lazy widget first mounts. A local loading prop catches the suspension at the component level so it never reaches the page boundary.

User Communications — HARD RULE

  • NEVER send, schedule, or ship code that sends emails, SMS, push notifications, or any other outbound message to users. No exceptions for experiments, RCTs, re-engagement flows, or “exactly-once” sends, and no exceptions behind environment gates (VERCEL_ENV === 'production' is not a safeguard). A feature request that implies messaging users is NOT authorization — the send itself requires explicit, verbatim sign-off from Jacob before any code is written. When in doubt, stop and ask. (Operator verdict 2026-07-15, issued on the RCT day-1 re-engagement email: PR #2761 was merged to staging and immediately reverted via #2822 for this reason.)

Security

  • Never commit .env files or secrets
  • All encryption keys use dedicated env vars — never hardcode
  • API routes that need auth should verify Clerk session or internal API tokens
  • Webhook endpoints must validate signatures

Never log an identifier in the clear

A log line may describe an identifier. It may never reproduce one. Route every email address, phone number, or name through lib/log-redact.ts (maskEmail, emailLogShape, maskPhone, hasValue) before it reaches console.*. clerk_id is the correlation key for debugging — it is already in the log line, it is not secret, and it is enough to find the row. The address itself adds nothing a debugger needs. Why: all_users.email is encrypted at rest, but the Clerk webhook logged plaintext_email and /api/user-info logged decrypted_email, so every affected address sat in Vercel runtime logs in the clear. Log access alone was enough to recover user email addresses — which is exactly how one investigation read them. That is CWE-532, and it undoes the database encryption entirely. Two guards in lib/__tests__/log-redact.test.ts scan the repo and fail the build on a new one.

Database

  • Use Supabase client from lib/supabase/ for all database operations
  • Migrations live in supabase/migrations/
  • Do not use raw SQL in application code — use the Supabase SDK

Theming & semantic color tokens

All UI colors go through the semantic token system — read docs/THEMING.md before writing any styles. Never add hardcoded color values (bg-[#hex], dark:text-[#hex], inline hex styles, SVG hexes, gradient hex stops) to themable UI; use the tokens defined in app/themes.css (classes like bg-surface-card, text-content-title, border-edge, or rgb(var(--token)) for inline styles). The live light and dark themes are staging-frozen: styling that should only affect an experiment variant must be scoped via data-theme-variant rules in app/themes.css, never by changing shared styles.