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.jsonscripts 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. Seedocs/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.tsrunsnpm run e2e:devas itswebServer). - 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.
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 ofenum Status { Active, Inactive } interfacefor object shapes,typefor unions and intersections — be consistentunknownoverany— when the type is truly unknown, useunknownand 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,Recordinstead 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— usesatisfiesto validate a value matches a type while preserving the narrower inferred type
Avoid
any— never useanyin new code. Useunknown, generics, or proper types instead- Type assertions (
as) — avoidascasts. 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
Functiontype — use specific function signatures:(args: Args) => ReturnTypeinstead ofFunctionobjector{}— these accept almost anything. UseRecord<string, unknown>or define the actual shape- Barrel exports (
index.tsre-exports) — these hurt tree-shaking and slow down builds. Import directly from the source file
Documentation
Before working on a specific subsystem, checkdocs/ for existing documentation. Key docs include:
docs/CLERK-AUTH-ONBOARDING-ROUTING.md— Loop-proof auth/onboarding routing, Clerk session-token claims, write-path invariantsdocs/ACCOUNT-TYPE-SYSTEM.md— Account types and classificationdocs/PLAID-RATE-LIMIT-HANDLING.md— Plaid API rate limiting strategydocs/BROKERAGE-CASH-SWEEP-DEDUP.md— Wells Fargo cash-sweep double-count: detection rules, read-time dedup architecture, theaccount_effective_balancesview for SQL consumers, snapshots caveat, rollout flagdocs/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 flagdocs/EARNINGS-CALENDAR-SYSTEM.md— Earnings calendar implementationdocs/REFERRAL-SYSTEM.md— Referral system logicdocs/TAX-TREATMENT-IMPLEMENTATION.md— Tax treatment handlingdocs/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 thefetchAllPageshelper. 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 insupabase/migrations/— CREATE OR REPLACE VIEW silently dropssecurity_invokerand 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 anduseExperimenthook: exposure rules, control-by-default, QA overrides, and the constraints on agent-authored experiment PRsdocs/WATCHLIST.md— Dashboard watchlist: thewatchlist_itemstable, per-plan limits enforced in the database, and the Silvia skills that document itdocs/precious-metals-integration.md— Precious metals pricing integrationdocs/discourse-integration.md— Discourse community SSO integrationdocs/SUPPORTED_CHAINS.md— Supported blockchain chainsdocs/PERSONAL-INFO-TOOL.md— Personal info tool for AI chatdocs/blog/agent-rules.md— Blog architecture, Sanity integration, routing, and SEO conventionsdocs/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 themdocs/SILVIA-SKILLS-SYNC.md— Required reading before any user-visible UI change. Silvia AI’s knowledge of the UI lives in the separateCFO-Silvia/skillsrepo and goes stale when the UI changespackages/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-editeddocs/MODULUS-CLEANUP-PLAN.md— Required reading before deleting anything fromcomponents/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)
Silvia AI Skills — UI Knowledge Sync
Silvia (the AI agent users chat with) answers “how do I…” questions only fromsilvia-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:- 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. - Keep
app/(app)/chat/[[...sessionId]]/__tests__/chat-main-flows.test.tsxpassing. - State in the PR any live check that was blocked. A blocked check is not a passed one.
Next.js Dynamic Imports
Everydynamic() call must include a loading fallback:
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
.envfiles 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 throughlib/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.