# AGENTS.md - Server App Guidelines TanStack Start fullstack web app with ORPC (contract-first RPC). ## Tech Stack > **⚠️ This project uses Bun — NOT Node.js / npm. All commands use `bun`. Never use `npm`, `npx`, or `node`.** - **Framework**: TanStack Start (React 19 SSR, file-based routing) - **Runtime**: Bun — **NOT Node.js** - **Package Manager**: Bun — **NOT npm / yarn / pnpm** - **Language**: TypeScript (strict mode) - **Styling**: Tailwind CSS v4 - **Database**: PostgreSQL + Drizzle ORM v1 beta (`drizzle-orm/postgres-js`, RQBv2) - **State**: TanStack Query v5 - **RPC**: ORPC (contract-first, type-safe) - **Build**: Vite + Nitro ## Commands ```bash # Development bun dev # Vite dev server (localhost:3000) bun db:studio # Drizzle Studio GUI # Build bun build # Production build → .output/ bun compile # Compile to standalone binary (current platform, depends on build) bun compile:darwin # Compile for macOS (arm64 + x64) bun compile:darwin:arm64 # Compile for macOS arm64 bun compile:darwin:x64 # Compile for macOS x64 bun compile:linux # Compile for Linux (x64 + arm64) bun compile:linux:arm64 # Compile for Linux arm64 bun compile:linux:x64 # Compile for Linux x64 bun compile:windows # Compile for Windows (default: x64) bun compile:windows:x64 # Compile for Windows x64 # Code Quality bun fix # Biome auto-fix bun typecheck # TypeScript check # Database bun db:generate # Generate migrations from schema bun db:migrate # Run migrations bun db:push # Push schema directly (dev only) # Testing (not yet configured) bun test path/to/test.ts # Run single test bun test -t "pattern" # Run tests matching pattern ``` ## Directory Structure ``` src/ ├── client/ # Client-side code │ ├── orpc.ts # ORPC isomorphic client (internal) │ └── query-client.ts # TanStack Query utils (used by components) ├── components/ # React components ├── routes/ # TanStack Router file routes │ ├── __root.tsx # Root layout │ ├── index.tsx # Home page │ └── api/ │ ├── $.ts # OpenAPI handler + Scalar docs │ ├── health.ts # Health check endpoint │ └── rpc.$.ts # ORPC RPC handler ├── server/ # Server-side code │ ├── api/ # ORPC layer │ │ ├── contracts/ # Input/output schemas (Zod) │ │ ├── middlewares/ # Middleware (db provider, auth) │ │ ├── routers/ # Handler implementations │ │ ├── interceptors.ts # Shared error interceptors │ │ ├── context.ts # Request context │ │ ├── server.ts # ORPC server instance │ │ └── types.ts # Type exports │ └── db/ │ ├── schema/ # Drizzle table definitions │ ├── fields.ts # Shared field builders (id, createdAt, updatedAt) │ ├── relations.ts # Drizzle relations (defineRelations, RQBv2) │ └── index.ts # Database instance (postgres-js driver) ├── env.ts # Environment variable validation ├── router.tsx # Router configuration ├── routeTree.gen.ts # Auto-generated (DO NOT EDIT) └── styles.css # Tailwind entry ``` ## ORPC Pattern ### 1. Define Contract (`src/server/api/contracts/feature.contract.ts`) ```typescript import { oc } from '@orpc/contract' import { createSelectSchema } from 'drizzle-orm/zod' import { z } from 'zod' import { featureTable } from '@/server/db/schema' const selectSchema = createSelectSchema(featureTable) export const list = oc.input(z.void()).output(z.array(selectSchema)) export const create = oc.input(insertSchema).output(selectSchema) ``` ### 2. Implement Router (`src/server/api/routers/feature.router.ts`) ```typescript import { ORPCError } from '@orpc/server' import { db } from '../middlewares' import { os } from '../server' export const list = os.feature.list.use(db).handler(async ({ context }) => { return await context.db.query.featureTable.findMany({ orderBy: { createdAt: 'desc' }, }) }) ``` ### 3. Register in Index Files ```typescript // src/server/api/contracts/index.ts import * as feature from './feature.contract' export const contract = { feature } // src/server/api/routers/index.ts import * as feature from './feature.router' export const router = os.router({ feature }) ``` ### 4. Use in Components ```typescript import { useSuspenseQuery, useMutation } from '@tanstack/react-query' import { orpc } from '@/client/query-client' const { data } = useSuspenseQuery(orpc.feature.list.queryOptions()) const mutation = useMutation(orpc.feature.create.mutationOptions()) ``` ## Database (Drizzle ORM v1 beta) - **Driver**: `drizzle-orm/postgres-js` (NOT `bun-sql`) - **Validation**: `drizzle-orm/zod` (built-in, NOT separate `drizzle-zod` package) - **Relations**: Defined via `defineRelations()` in `src/server/db/relations.ts` - **Query**: RQBv2 — use `db.query.tableName.findMany()` with object-style `orderBy` and `where` ### Schema Definition ```typescript import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' import { sql } from 'drizzle-orm' export const myTable = pgTable('my_table', { id: uuid().primaryKey().default(sql`uuidv7()`), name: text().notNull(), createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow().$onUpdateFn(() => new Date()), }) ``` ### Relations (RQBv2) ```typescript // src/server/db/relations.ts import { defineRelations } from 'drizzle-orm' import * as schema from './schema' export const relations = defineRelations(schema, (r) => ({ // Define relations here using r.one / r.many / r.through })) ``` ### DB Instance ```typescript // src/server/db/index.ts import { drizzle } from 'drizzle-orm/postgres-js' import { relations } from '@/server/db/relations' // In RQBv2, relations already contain schema info — no separate schema import needed const db = drizzle({ connection: env.DATABASE_URL, relations, }) ``` ### RQBv2 Query Examples ```typescript // Object-style orderBy (NOT callback style) const todos = await db.query.todoTable.findMany({ orderBy: { createdAt: 'desc' }, }) // Object-style where const todo = await db.query.todoTable.findFirst({ where: { id: someId }, }) ``` ## Code Style ### Formatting (Biome) - **Indent**: 2 spaces - **Quotes**: Single `'` - **Semicolons**: Omit (ASI) - **Arrow parens**: Always `(x) => x` ### Imports Biome auto-organizes: 1. External packages 2. Internal `@/*` aliases 3. Type imports (`import type { ... }`) ```typescript import { createFileRoute } from '@tanstack/react-router' import { z } from 'zod' import { db } from '@/server/db' import type { ReactNode } from 'react' ``` ### TypeScript - `strict: true` - `noUncheckedIndexedAccess: true` - array access returns `T | undefined` - Use `@/*` path aliases (maps to `src/*`) ### Naming | Type | Convention | Example | |------|------------|---------| | Files (utils) | kebab-case | `auth-utils.ts` | | Files (components) | PascalCase | `UserProfile.tsx` | | Components | PascalCase arrow | `const Button = () => {}` | | Functions | camelCase | `getUserById` | | Types | PascalCase | `UserProfile` | ### React - Use arrow functions for components (Biome enforced) - Use `useSuspenseQuery` for guaranteed data - Let React Compiler handle memoization (no manual `useMemo`/`useCallback`) ## Environment Variables ```typescript // src/env.ts - using @t3-oss/env-core import { createEnv } from '@t3-oss/env-core' import { z } from 'zod' export const env = createEnv({ server: { DATABASE_URL: z.string().url(), }, clientPrefix: 'VITE_', client: { VITE_API_URL: z.string().optional(), }, }) ``` ## Development Principles > **These principles apply to ALL code changes. Agents MUST follow them on every task.** 1. **No backward compatibility** — This project is in rapid iteration. Always use the latest API and patterns. Never keep deprecated code paths or old API fallbacks. 2. **Always sync documentation** — When code changes, immediately update all related documentation (`AGENTS.md`, `README.md`, inline code examples). Code and docs must never drift apart. 3. **Forward-only migration** — When upgrading dependencies, fully adopt the new API. Don't mix old and new patterns. ## Critical Rules **DO:** - Run `bun fix` before committing - Use `@/*` path aliases - Include `createdAt`/`updatedAt` on all tables - Use `ORPCError` with proper codes - Use `drizzle-orm/zod` (NOT `drizzle-zod`) for schema validation - Use RQBv2 object syntax for `orderBy` and `where` - Update `AGENTS.md` and other docs whenever code patterns change **DON'T:** - Use `npm`, `npx`, `node`, `yarn`, `pnpm` — always use `bun` / `bunx` - Edit `src/routeTree.gen.ts` (auto-generated) - Use `as any`, `@ts-ignore`, `@ts-expect-error` - Commit `.env` files - Use empty catch blocks - Import from `drizzle-zod` (use `drizzle-orm/zod` instead) - Use RQBv1 callback-style `orderBy` / old `relations()` API - Use `drizzle-orm/bun-sql` driver (use `drizzle-orm/postgres-js`) - Pass `schema` to `drizzle()` constructor (only `relations` is needed in RQBv2) - Leave docs out of sync with code changes