# 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 - **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 # 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.client.ts # ORPC isomorphic client │ └── query-client.ts # TanStack Query client ├── components/ # React components ├── routes/ # TanStack Router file routes │ ├── __root.tsx # Root layout │ ├── index.tsx # Home page │ └── api/ │ └── rpc.$.ts # ORPC HTTP endpoint ├── server/ # Server-side code │ ├── api/ # ORPC layer │ │ ├── contracts/ # Input/output schemas (Zod) │ │ ├── middlewares/ # Middleware (db provider, auth) │ │ ├── routers/ # Handler implementations │ │ ├── context.ts # Request context │ │ ├── server.ts # ORPC server instance │ │ └── types.ts # Type exports │ └── db/ │ ├── schema/ # Drizzle table definitions │ └── index.ts # Database instance ├── 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-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() }) ``` ### 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/orpc.client' const { data } = useSuspenseQuery(orpc.feature.list.queryOptions()) const mutation = useMutation(orpc.feature.create.mutationOptions()) ``` ## Database Schema (Drizzle) ```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()), }) ``` ## 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(), }, }) ``` ## Critical Rules **DO:** - Run `bun fix` before committing - Use `@/*` path aliases - Include `createdAt`/`updatedAt` on all tables - Use `ORPCError` with proper codes **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