This guide is for AI coding agents (Claude, Cursor, Copilot, etc.) working in this codebase.
For detailed project documentation and workflows, see CLAUDE.md
- bun (instead of node/npm):
~/.bun/bin/bun(also in PATH) - tofu (instead of terraform):
/opt/homebrew/bin/tofu
Run these commands in order and fix ALL issues before committing. Repeat until all pass:
bun run test:all # All tests must pass (backend + frontend)
bun format # Format code with Biome
bun check:fix # Lint and auto-fix with Biome
bun typecheck # Fix all TypeScript errors
bun run check:boundaries # Verify architecture boundariesNever commit without passing all five checks.
bun run dev:api # Start API server (port 3000, hot reload)
bun run dev:app # Start Vite frontend (port 5173)
bun install # Install dependenciesbun test # Backend/integration tests (PGlite in-memory DB)
bun test <path/to/test.ts> # Run single backend test file
bun run test:components # Frontend component tests (watch mode)
bun run test:components:run # Frontend component tests (CI mode)
bun run test:all # Run ALL tests (backend + frontend)bun typecheck # TypeScript type checking
bun check # Biome lint (read-only)
bun check:fix # Biome lint with auto-fix
bun format # Format code with Biomebun run db:generate # Generate Drizzle migrations
bun run db:migrate # Run migrations
bun run db:reset # Reset database
bun run db:seed # Seed test dataDatabase schema changes: Run bun run db:generate and update scripts in scripts/db/ if needed.
- Use explicit
.jsextensions (required by Bun):from "../../domain/heat/index.js" - Type-only imports:
import type { HeatState } from "./types.js" - Auto-organized by Biome on format (source action: organizeImports)
- Barrel exports via
index.tsfiles for public API
- Indentation: 2 spaces
- Line width: 100 characters
- Quotes: Double quotes (
") - Semicolons: Always (
;) - Trailing commas: ES5 style (arrays/objects only)
- Strict mode enabled - no implicit any, strict null checks
- Explicit return types on exported functions
- Interface for object shapes:
interface HeatState { ... } - Type for unions/aliases:
type Score = WaveScore | JumpScore - Discriminated unions: Use
typefield for discrimination - Type exports: Export types from
index.tsbarrel files - No
anyin production code (allowed in__tests__/**/*.test.tsonly)
- Files: kebab-case (
heat-service.ts,error-handling.ts) - Components: PascalCase files (
Button.tsx,HeatCard.tsx) - Types/Interfaces: PascalCase (
HeatState,ScoreRepository) - Functions: camelCase (
calculateWaveTotal,handleCreateHeat) - Constants: UPPER_SNAKE_CASE (
DEFAULT_HEAT_RULES,MAX_WAVE_SCORE) - Private helpers: camelCase with descriptive names
neverthrowResult types: Domain services returnPromise<Result<T, E>>instead of throwing- Custom error classes extending
Error(e.g.,HeatDoesNotExistError) — used as theEinResult<T, E> - oRPC
.errors()for typed error contracts: DefineNOT_FOUND,BAD_REQUEST,UNAUTHORIZED,FORBIDDENon procedures result.match(onOk, onErr): Functional Result handling — use.match()instead of imperativeif (result.isErr())throwDomainError(error, errors): Maps domain errors to oRPC typed errors; replacesDOMAIN_ERROR_MAP+unwrapOrThrowwithResultTransaction(db, fn): Result-aware Drizzle transaction wrapper — rollbacks onerr(), returns the ResultdomainErrorMappermiddleware: Safety net for unexpected infrastructure errors → 500getDomainErrorStatusCode(error): Maps domain errors to HTTP status codes for legacy REST routes- Error union types:
HeatServiceError,BracketServiceErrorfor type-safe error handling - Pattern: Domain services return
err(...), handlers useresult.match()withthrowDomainErrorfor the error branch - API-level errors: Use
throw errors.NOT_FOUND()/throw errors.FORBIDDEN()for auth/existence checks (API concerns)
- Repository pattern: Interfaces in
domain/, implementations ininfrastructure/ - Service layer: Business logic in
domain/*/service.ts - Pure functions: Score calculators, validators
- Domain types separate from API types
- Validation at API boundary using Zod schemas
- Transactions: PostgreSQL ACID guarantees via Drizzle ORM
The dependency graph is strictly directional. Violations fail CI via bun run check:boundaries.
Allowed dependencies:
api/→domain/,infrastructure/infrastructure/→domain/(implements interfaces defined in domain)app/→domain/(types and pure functions only),api/(router type only)domain/→domain/(cross-module imports)
Forbidden dependencies:
domain/→infrastructure/,api/,app/infrastructure/→api/,app/
Transaction ownership:
- API handlers start and commit/rollback transactions via
db.transaction() - Domain services never call
getDb()or manage transactions - Repositories receive their connection at construction:
createHeatRepository(db)orcreateHeatRepository(tx) - For transactional operations, create repositories with
tx
When reviewing code, verify:
- No runtime imports crossing forbidden boundaries
- Domain services don't call
getDb()ordb.transaction() - Repositories don't call
getDb()— connection is injected via factory - Transaction scope is owned by the API handler
- Handler functions:
async function handleCreateHeat(request: Request): Promise<Response> - Middleware composition:
withValidation,withErrorHandling,withAuth - Type-safe requests:
Request & { user: { id: string } }for authenticated routes - Response helpers:
createSuccessResponse(data),createErrorResponse(msg, status) - Zod schemas in
api/schemas.tsfor request validation
- Functional components:
const MyComponent: Component<Props> = (props) => { ... } - Reactive primitives: Access via functions:
variant(),size(), notvariant - JSX: Use
class(notclassName),for(nothtmlFor) - Props: Don't destructure - use
props.variantto preserve reactivity - Tailwind CSS: Utility-first classes, avoid inline styles
- Component composition: Accept
children: JSX.Elementprop
- Repository pattern for all data access
- Connection injection: Repositories receive
DbConnectionvia factory:createHeatRepository(db) - Transactions: API handlers own transaction scope:
db.transaction(async (tx) => { ... }) - JSON columns: Stringify arrays:
JSON.stringify(riderIds) - Timestamps: Include
createdAt,updatedAt,completedAtwhere appropriate
- Location:
__tests__/api/,__tests__/domain/,__tests__/integration/ - Framework: Bun test runner with PGlite in-memory PostgreSQL
- Pattern: Arrange-Act-Assert
- Isolation: Each test file gets isolated PGlite database
Template:
import { describe, expect, it, beforeAll, afterAll, beforeEach } from "bun:test";
import { setupTestDb, teardownTestDb, clearTestData, getDb } from "../test-db.js";
describe("My Test Suite", () => {
beforeAll(async () => {
await setupTestDb(); // Setup isolated PGlite instance
});
afterAll(async () => {
await teardownTestDb(); // Cleanup
});
beforeEach(async () => {
await clearTestData(); // Clear data between tests
});
it("should do something", async () => {
const db = await getDb();
// Arrange, Act, Assert
});
});- Location:
__tests__/components/**/*.test.{ts,tsx} - Framework: Vitest with happy-dom
- Query by: Role, label, text (not test IDs or classes) for accessibility
Template:
import { render, screen } from "@solidjs/testing-library";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
describe("MyComponent", () => {
it("should handle user interaction", async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(() => <MyComponent onSubmit={onSubmit} />);
const button = screen.getByRole("button", { name: /submit/i });
await user.click(button);
expect(onSubmit).toHaveBeenCalled();
});
});When to write component tests:
- Complex UI logic (modals, forms, multi-step flows)
- User interactions requiring simulation
- Conditional rendering logic
When NOT to write component tests:
- Simple presentational components (just display props)
- Better tested via integration/E2E
- Pure styling/layout concerns
src/
├── api/ # REST API layer
│ ├── routes/ # Route handlers
│ ├── middleware/ # Error handling, validation, auth
│ ├── schemas.ts # Zod validation schemas
│ └── websocket*.ts # WebSocket handlers
├── app/ # SolidJS frontend
│ ├── components/ # UI components
│ ├── pages/ # Page components
│ ├── contexts/ # State contexts
│ └── utils/ # Frontend utilities
├── domain/ # Domain-Driven Design
│ ├── heat/ # Heat scoring (core domain)
│ ├── contest/ # Contest management
│ ├── bracket/ # Bracket generation
│ └── */index.ts # Public API exports
├── infrastructure/ # Infrastructure concerns
│ ├── db/ # Drizzle schema, migrations
│ └── repositories/ # Repository implementations
└── viewer/ # Standalone web component
__tests__/
├── api/ # API route tests
├── components/ # Component tests (Vitest)
├── domain/ # Domain logic tests
├── integration/ # Integration tests
├── test-db.ts # PGlite utilities
└── setup.ts # Vitest setup
- ✅ All tests pass before every commit
- ✅ Zero type errors - strict mode enabled
- ✅ Formatted code - Biome auto-format
- ✅ No lint errors - Biome recommended rules
- ✅ Test coverage - Write tests for new features
- ✅ Follow patterns - Check existing code first
- ✅ Domain errors - Use custom error classes, not generic Error
- ✅ Type safety - Explicit types, no implicit any
❌ Don't use node or npm → Use bun
❌ Don't forget .js extensions in imports
❌ Don't destructure SolidJS props → Breaks reactivity
❌ Don't use className in JSX → Use class
❌ Don't commit without passing all checks
❌ Don't use any type outside of tests
❌ Don't skip error logging
❌ Don't access dev database in tests → Use PGlite via test utilities
❌ Don't import from infrastructure/ in domain code → Inject dependencies instead
❌ Don't start transactions in domain services → API handlers own transaction lifecycle
❌ Don't call getDb() in repositories → Accept connection via constructor
❌ Don't use new ORPCError(...) directly → Use typed errors.CODE() from oRPC procedures
❌ Don't use imperative if (result.isErr()) → Use result.match() for cleaner handling