Code style
Foundry uses Biome for linting and formatting. The configuration lives in biome.json at the repository root and applies across 6 workspaces.
Biome setup
Section titled “Biome setup”Formatter settings
Section titled “Formatter settings”| Setting | Value |
|---|---|
| Indent style | Spaces |
| Indent width | 2 |
| Line width | 100 |
| Quote style | Double quotes |
| Semicolons | Always |
| Trailing commas | All |
Lint rule levels
Section titled “Lint rule levels”Errors (blocking — must fix before commit):
- All Biome recommended rules not listed below
Warnings (non-blocking — fix when practical):
noExplicitAnynoImplicitAnyLetnoNonNullAssertionnoNonNullAssertedOptionalChainnoUnusedFunctionParametersnoUnusedVariablesnoUnusedImportsnoUnsafeFinallyuseIterableCallbackReturnnoAssignInExpressions
Accessibility warnings (non-blocking):
useButtonType,noSvgWithoutTitle,noLabelWithoutControl,useKeyWithClickEvents,noStaticElementInteractions,useSemanticElements,useAriaPropsForRole,useAriaPropsSupportedByRole,useFocusableInteractive
Test file overrides:
noExplicitAnyis disabled in**/*.test.ts,**/*.test.tsx, and**/__tests__/**
Running Biome
Section titled “Running Biome”# Check lint + format (no changes)bun run check
# Auto-fix everything Biome can fixbun run check:fix
# Format only (check)bun run format
# Format only (fix)bun run format:fixEnforcement
Section titled “Enforcement”Biome runs at two enforcement points:
-
PostToolUse hook — When editing files with Claude Code,
biome_check.shruns automatically after every file edit. It auto-fixes formatting and reports remaining lint issues. This hook has a 10-second timeout. -
Pre-commit hook — Lefthook runs Biome on staged files before every commit. Commits with lint errors are blocked.
Naming conventions
Section titled “Naming conventions”| Category | Convention | Example |
|---|---|---|
| Convex tables | camelCase plural | agentExecutions, skillVersions |
| Convex functions | camelCase verb-noun | requirements.listByProgram |
| React components | PascalCase | MissionControlDashboard, TaskBoard |
| Route directories | kebab-case | agent-activity, source-control |
| Environment variables | SCREAMING_SNAKE_CASE | NEXT_PUBLIC_CONVEX_URL |
| TypeScript types/interfaces | PascalCase | ProgramWithStats, TaskStatus |
| Utility functions | camelCase | assertOrgAccess, buildContextPayload |
| CSS classes | Tailwind utilities | flex items-center gap-2 |
TypeScript patterns
Section titled “TypeScript patterns”Strictness
Section titled “Strictness”TypeScript 5.9.3 with strict mode enabled. Key settings:
strict: truenoUncheckedIndexedAccesswhere applicable- Path-based exports from
packages/ui(e.g.,@foundry/ui/tasks)
Convex-specific patterns
Section titled “Convex-specific patterns”Always use .withIndex() for queries. Never use .filter() — it causes full table scans and kills reactive performance.
// Correctconst tasks = await ctx.db .query("tasks") .withIndex("by_program", (q) => q.eq("programId", programId)) .collect();
// Wrong - full table scanconst tasks = await ctx.db .query("tasks") .filter((q) => q.eq(q.field("programId"), programId)) .collect();Row-level security on every query/mutation:
import { assertOrgAccess } from "../model/access";
export const listByProgram = query({ args: { orgId: v.string(), programId: v.id("programs") }, handler: async (ctx, args) => { await assertOrgAccess(ctx, args.orgId); return ctx.db .query("tasks") .withIndex("by_program", (q) => q.eq("programId", args.programId)) .collect(); },});Mutations vs. actions:
- Mutations are transactional but cannot call external APIs or Node.js libraries.
- Actions can call anything but are not transactional.
- Use mutations for data changes. Use actions for AI calls and external integrations.
Next.js 16 patterns
Section titled “Next.js 16 patterns”params and searchParams are Promises in Next.js 15+. You must await them:
// Correctexport default async function Page({ params,}: { params: Promise<{ programId: string }>;}) { const { programId } = await params; // ...}Use the "skip" token on Convex useQuery when auth state has not resolved:
const data = useQuery( api.tasks.listByProgram, orgId ? { orgId, programId } : "skip");UI architecture
Section titled “UI architecture”All feature UI lives in packages/ui/src/. Page files in apps/web/ are ultra-thin wrappers:
// apps/web/src/app/(dashboard)/[programId]/tasks/page.tsx"use client";import { ProgramTasksRoute } from "@foundry/ui/tasks";export default function ProgramTasksPage() { return <ProgramTasksRoute />;}Do not add business logic to page files. This breaks the shared component model used by the Tauri desktop app.
Import organization
Section titled “Import organization”Biome auto-organizes imports. The general order is:
- External packages (
react,next,convex) - Internal packages (
@foundry/ui,@foundry/types) - Relative imports (
./components,../utils)
Do not manually sort imports — Biome handles this on save and during the pre-commit hook.