Files
fullstack-starter/apps/server/AGENTS.md

9.1 KiB

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

# 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
│   └── 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
│       ├── 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)

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)

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

// 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

import { useSuspenseQuery, useMutation } from '@tanstack/react-query'
import { orpc } from '@/client/orpc'

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

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)

// 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

// 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

// 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 { ... })
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

// 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