pnpm dev # Start dev server with Turbopack
pnpm d # Full dev setup (install, docker, migrate, dev)
pnpm dx # Setup dependencies (install, docker up, migrate dev)
pnpm dx:up # Start Docker containers
pnpm dx:down # Stop Docker containers- when using git worktrees, store them in the .worktrees directory in the root of the project folder
- when creating a worktree, copy
.envfrom the root project into the worktree - update worktree-local app urls/port to avoid conflicts with the main workspace:
- set
NEXTAUTH_URLandNEXTAUTH_URL_INTERNALto a unique localhost port - run dev server with that same port (for example
pnpm dev --port 3174)
- set
- use a separate postgres instance per worktree (different host port + db name)
- set unique values in the worktree
.env:POSTGRES_PORT,POSTGRES_DB,POSTGRES_CONTAINER_NAME,DATABASE_URL - start dedicated db container, for example:
docker run -d --name <unique-container-name> -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=strong-password -e POSTGRES_DB=<worktree-db> -p <worktree-port>:5432 ossapps/postgres:17.7-trixie postgres -c shared_preload_libraries=pg_cron -c cron.database_name=<worktree-db> -c cron.timezone=UTC
- enable pg_cron extension once:
psql "postgresql://postgres:strong-password@localhost:<worktree-port>/<worktree-db>" -c "CREATE EXTENSION IF NOT EXISTS pg_cron;" - initialize schema and data:
pnpm exec prisma migrate reset --force --skip-seed && pnpm db:seed
- set unique values in the worktree
pnpm db:push # Push Prisma schema to database
pnpm db:studio # Open Prisma Studio
pnpm db:dev # Run Prisma migrations (dev)
pnpm db:seed # Seed the database
pnpm generate # Generate Prisma clientpnpm test # Run all tests
pnpm test:watch # Run tests in watch mode
pnpm test src/tests/simplify.test.ts # Run a specific test file
pnpm test -- -t "test name pattern" # Run tests matching patternpnpm lint # Run oxlint with type-aware checking
pnpm prettier --write . # Format all files
pnpm prettier --check . # Check formatting without changes
pnpm tsgo --noEmit # Type check (used in pre-commit)pnpm build # Build for productionpnpm prettier --check .- Format checkpnpm lint- Lintingpnpm tsgo --noEmit- Type checkingpnpm test- Unit testspnpm build --no-lint- Build verification
- Semicolons: required
- Quotes: single quotes
- Trailing commas: all
- Print width: 100 characters
- Tailwind CSS classes are auto-sorted
- Strict mode enabled
- Target: ES2022
noUncheckedIndexedAccess: true- handle undefined for index access- Path aliases:
~/*maps to./src/*@/*maps to./*
- External packages first (react, next, third-party libs)
- Internal imports using
~/alias - Relative imports for nearby files
- Use
typekeyword for type-only imports
- Server-side vars: Use
env.VAR_NAME(available only on server). - Client-side vars: Use
env.NEXT_PUBLIC_VAR_NAME(exposed to browser, prefixed withNEXT_PUBLIC_). - Defaults: Hardcode in
src/env.tsusing Zod.default()for fallback values.
import { HeartHandshakeIcon, X } from 'lucide-react';
import { useTranslation } from 'next-i18next';
import React, { useCallback } from 'react';
import { type CurrencyCode } from '~/lib/currency';
import { useAddExpenseStore } from '~/store/addStore';
import { api } from '~/utils/api';
import { Button } from '../ui/button';- Components: PascalCase (
AddExpensePage.tsx,Button.tsx) - Utility files: camelCase (
utils.ts,currency.ts) - Test files:
*.test.tspattern - Pages: kebab-case for routes (
import-splitwise.tsx) - Dynamic routes:
[paramName].tsx - Functions: camelCase (
calculateParticipantSplit) - Constants: UPPER_SNAKE_CASE (
DEFAULT_CATEGORY) - Types/Interfaces: PascalCase (
AddExpenseState) - Hooks: camelCase with
useprefix (useAddExpenseStore)
Use literal on the left side for comparisons:
if ('authenticated' === status) { ... }
if (0n === amount) { ... }
if ('' === description) { ... }- All monetary values are BigInt (cents/smallest unit) to prevent rounding errors
- Use
nsuffix for BigInt literals:0n,1n,10000n - Use
BigMathhelper (src/utils/numbers.ts) for arithmetic:abs,sign,min,max,roundDiv - Use
getCurrencyHelpers({ currency, locale })for display/parsing
tRPC API errors:
if (!group) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Group not found' });
}Client-side errors:
try {
await mutation.mutateAsync(data);
} catch (error) {
toast.error(error instanceof Error ? error.message : 'An unexpected error occurred');
}Promise chains:
router.push('/path').catch(console.error);-
Prefer arrow functions over function declarations:
// Good const calculateTotal = (items: Item[]) => items.reduce((sum, item) => sum + item.amount, 0n); // Avoid function calculateTotal(items: Item[]) { ... }
-
Prefer iterator methods over for loops (
map,filter,reduce,forEach,find,some,every):// Good const totals = expenses.filter(e => !e.deletedAt).map(e => e.amount); // Avoid for (const expense of expenses) { ... }
-
Prefer early returns to reduce nesting. Put the shorter/simpler case first:
// Good const processItem = (item: Item) => { if (!item.needsProcessing) { return item; } // Complex processing logic here... return processedItem; }; // Avoid const processItem = (item: Item) => { if (item.needsProcessing) { // Complex processing logic here... return processedItem; } else { return item; } };
- Functional components with TypeScript interfaces
useCallbackfor memoized callbacks,useMemofor computed values- Zustand for global state, tRPC/React Query for server state
cn()utility for conditional Tailwind classes- Use
as constandsatisfiesfor fixed arrays/objects
describe('functionName', () => {
describe('ScenarioGroup', () => {
it('should do something specific', () => {
const result = functionName(input);
expect(result).toBe(expected);
});
});
});no-unused-vars: warnno-console: warn (allows info, warn, error, debug, trace in JSX/TSX)sort-imports: error- Ignored paths:
prisma/seed.ts,src/components/ui/*
- Framework: Next.js 15 (Pages Router)
- Database: PostgreSQL + Prisma ORM
- API: tRPC
- State: Zustand + React Query
- Styling: Tailwind CSS v4
- UI: Radix UI primitives
- Auth: NextAuth.js
- Validation: Zod
- i18n: next-i18next
- Package Manager: pnpm
- Double-entry balance accounting: Every expense creates two balance records (bidirectional)
- Transaction batching: Use
db.$transaction(operations)for expense mutations - Split types:
EQUAL,PERCENTAGE,SHARE,EXACT,ADJUSTMENT,SETTLEMENT,CURRENCY_CONVERSION - Schema typo:
firendId(notfriendId) in GroupBalance - maintain for consistency
Data migrations too complex for SQL queries run automatically during server startup via src/instrumentation.ts.
src/migrations/index.ts- Migration runner that checksAppMetadata.schema_versionand runs pending migrationssrc/migrations/{name}.ts- Individual migration files exporting an async function
- Create a new file
src/migrations/{description}.ts:
import { db } from '~/server/db';
export async function myMigrationFunction(): Promise<void> {
// Migration logic using the shared db connection
const records = await db.someModel.findMany({ ... });
for (const record of records) {
await db.$transaction(async (tx) => {
// Atomic operations
});
}
console.log('Migration completed');
}- Register the migration in
src/migrations/index.tsby calling it in its respective case block.
- Schema version is stored in
AppMetadatatable with keyschema_version - Versions are compared lexicographically (semver format:
"2.0.0","2.1.0", etc.) - Missing version (pre-2.0.0 databases) is treated as needing all migrations
- Migrations run on every server start but are idempotent (version check)
- Use the shared
dbconnection from~/server/db - Wrap related operations in
db.$transaction()for atomicity - Log progress for visibility during deployment
- Only create English translation keys when adding features
- Translations are handled via Weblate by the community
- Translation files:
public/locales/{lang}/*.json
Husky runs on commit:
- Prettier formatting
- oxlint with auto-fix
- Prisma format (for .prisma files)
- Type checking with tsgo
Override with git commit --no-verify if needed.
Use the ctx7 CLI to fetch current documentation whenever the user asks about a library, framework, SDK, API, CLI tool, or cloud service -- even well-known ones like React, Next.js, Prisma, Express, Tailwind, Django, or Spring Boot. This includes API syntax, configuration, version migration, library-specific debugging, setup instructions, and CLI tool usage. Use even when you think you know the answer -- your training data may not reflect recent changes. Prefer this over web search for library docs.
Do not use for: refactoring, writing scripts from scratch, debugging business logic, code review, or general programming concepts.
- Resolve library:
npx ctx7@latest library <name> "<user's question>"— use the official library name with proper punctuation (e.g., "Next.js" not "nextjs", "Customer.io" not "customerio", "Three.js" not "threejs") - Pick the best match (ID format:
/org/project) by: exact name match, description relevance, code snippet count, source reputation (High/Medium preferred), and benchmark score (higher is better). If results don't look right, try alternate names or queries (e.g., "next.js" not "nextjs", or rephrase the question) - Fetch docs:
npx ctx7@latest docs <libraryId> "<user's question>" - Answer using the fetched documentation
You MUST call library first to get a valid ID unless the user provides one directly in /org/project format. Use the user's full question as the query -- specific and detailed queries return better results than vague single words. Do not run more than 3 commands per question. Do not include sensitive information (API keys, passwords, credentials) in queries.
For version-specific docs, use /org/project/version from the library output (e.g., /vercel/next.js/v14.3.0).
If a command fails with a quota error, inform the user and suggest npx ctx7@latest login or setting CONTEXT7_API_KEY env var for higher limits. Do not silently fall back to training data.