forked from imbytecat/fullstack-starter
11 KiB
11 KiB
AGENTS.md - AI Coding Agent Guidelines
Guidelines for AI agents working in this project.
Project Overview
This project uses Bun exclusively as both the JavaScript runtime and package manager. Do NOT use Node.js / npm / yarn / pnpm. All commands start with
bun— usebun installfor dependencies andbun run <script>for scripts. Always preferbun run <script>overbun <script>to avoid conflicts with Bun built-in subcommands (e.g.bun buildinvokes Bun's bundler, NOT your package.json script). Never usenpm,npx, ornode.
- Framework: TanStack Start (React 19 SSR, file-based routing)
- Runtime: Bun (see
mise.tomlfor version) — 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
Build / Lint / Test Commands
# Development
bun run dev # Vite dev server (localhost:3000)
bun run db:studio # Drizzle Studio GUI
# Build
bun run build # Production build → .output/
bun run compile # Compile to standalone binary (current platform, depends on build)
bun run compile:darwin # Compile for macOS (arm64 + x64)
bun run compile:darwin:arm64 # Compile for macOS arm64
bun run compile:darwin:x64 # Compile for macOS x64
bun run compile:linux # Compile for Linux (x64 + arm64)
bun run compile:linux:arm64 # Compile for Linux arm64
bun run compile:linux:x64 # Compile for Linux x64
bun run compile:windows # Compile for Windows (default: x64)
bun run compile:windows:x64 # Compile for Windows x64
# Code Quality
bun run fix # Biome auto-fix
bun run typecheck # TypeScript check
# Database (Drizzle)
bun run db:generate # Generate migrations from schema
bun run db:migrate # Run migrations
bun run 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
Code Style (TypeScript)
Formatting (Biome)
- Indent: 2 spaces | Line endings: LF
- Quotes: Single
'| Semicolons: Omit (ASI) - Arrow parentheses: Always
(x) => x
Imports
Biome auto-organizes. Order: 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 Strictness
strict: true,noUncheckedIndexedAccess: true,noImplicitOverride: true,verbatimModuleSyntax: true- Use
@/*path aliases (maps tosrc/*)
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Files (utils) | kebab-case | auth-utils.ts |
| Files (components) | PascalCase | UserProfile.tsx |
| Components | PascalCase arrow | const Button = () => {} |
| Functions | camelCase | getUserById |
| Constants | UPPER_SNAKE | MAX_RETRIES |
| Types/Interfaces | PascalCase | UserProfile |
React Patterns
- Components: arrow functions (enforced by Biome)
- Routes: TanStack Router file conventions (
export const Route = createFileRoute(...)) - Data fetching:
useSuspenseQuery(orpc.feature.list.queryOptions())
Error Handling
- Use
try-catchfor async operations; throw descriptive errors - ORPC: Use
ORPCErrorwith proper codes (NOT_FOUND,INPUT_VALIDATION_FAILED) - Never use empty catch blocks
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 + postgres-js)
- ORM: Drizzle ORM
1.0.0-beta(RQBv2) - Driver:
drizzle-orm/postgres-js(NOTbun-sql) - Validation:
drizzle-orm/zod(built-in, NOT separatedrizzle-zodpackage) - Relations: Defined via
defineRelations()insrc/server/db/relations.ts(contains schema info, sodrizzle()only needs{ relations }) - Query style: RQBv2 object syntax (
orderBy: { createdAt: 'desc' },where: { id: 1 })
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 },
})
Environment Variables
- Use
@t3-oss/env-corewith Zod validation insrc/env.ts - Server vars: no prefix | Client vars:
VITE_prefix required - Never commit
.envfiles
// src/env.ts
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.
- 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 "just in case".
- 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. This includes updating code snippets in docs when imports, APIs, or patterns change. - Forward-only migration — When upgrading dependencies, fully adopt the new API. Don't mix old and new patterns in the same codebase.
Critical Rules
DO:
- Run
bun run fixbefore committing - Use
@/*path aliases (not relative imports) - Include
createdAt/updatedAton all tables - Use
ORPCErrorwith proper codes - Use
drizzle-orm/zod(NOTdrizzle-zod) for schema validation - Use RQBv2 object syntax for
orderByandwhere - Update
AGENTS.mdand other docs whenever code patterns change
DON'T:
- Use
npm,npx,node,yarn,pnpm— always usebun/bunx - Edit
src/routeTree.gen.ts(auto-generated) - Use
as any,@ts-ignore,@ts-expect-error - Commit
.envfiles - Use empty catch blocks
catch(e) {} - Import from
drizzle-zod(usedrizzle-orm/zodinstead) - Use RQBv1 callback-style
orderBy/ oldrelations()API - Use
drizzle-orm/bun-sqldriver (usedrizzle-orm/postgres-js) - Pass
schematodrizzle()constructor (onlyrelationsis needed in RQBv2) - Import
osfrom@orpc/serverin middleware — use@/server/api/server(the local typed instance) - Leave docs out of sync with code changes
Git Workflow
- Make changes following style guide
bun run fix- auto-format and lintbun run typecheck- verify typesbun run dev- test locally- Commit with descriptive message
Directory Structure
.
├── src/
│ ├── client/ # Client-side code
│ │ └── orpc.ts # ORPC client + TanStack Query utils (single entry point)
│ ├── 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
├── biome.json # Linting/formatting config
├── compile.ts # Cross-platform binary compilation script
├── drizzle.config.ts # Drizzle Kit config
├── vite.config.ts # Vite + TanStack Start + Nitro config
├── tsconfig.json # TypeScript config
└── package.json # Dependencies and scripts