修正 AGENTS.md 里与 1.0 beta 相关的过时条目(drizzle-orm/zod、
defineRelations、RQB v2 对象语法等),改为记录当前真实用法:
drizzle-zod 包、`drizzle({ schema })`、RQB v1 回调写法。顺手裁掉
通用的 Biome/TS 说明,补上几条仓库特有的坑(Nitro 插件在 vite.config
里注册、distroless cc 变体、无 CI/pre-commit 等)。
9.0 KiB
AGENTS.md
Compact, repo-specific notes for AI agents. Generic language/framework knowledge is omitted — only things that will bite you if you don't know.
Stack & runtime
- Bun-only (
mise.tomlpinsbun = 1.3.13). Never invokenpm/npx/node/yarn/pnpm. Usebun run <script>(barebun <script>can collide with Bun built-in subcommands). - TanStack Start (React 19 SSR, file-routed) + Vite 8 + Nitro (nightly, preset
bun). Vite dev port is strict 3000. - PostgreSQL + Drizzle ORM
0.45.2(0.x, NOT 1.0 beta) — see "Drizzle" section, this matters a lot. - ORPC (contract-first), TanStack Query v5, Tailwind v4.
Scripts
bun run dev # bunx --bun vite dev (localhost:3000)
bun run build # bunx --bun vite build → .output/
bun run compile # bun compile.ts → out/server-<target> (standalone binary)
bun run typecheck # tsc --noEmit
bun run fix # biome check --write (lint + format + organize imports)
bun run db:push # dev only — push schema to DB, no migration file
bun run db:generate # produce SQL migration files in ./drizzle
bun run db:migrate # apply migrations (also auto-run at prod server startup)
bun run db:studio # Drizzle Studio
Cross-compile targets live under compile:{linux,darwin,windows}[:arch]. compile.ts accepts --target bun-<os>-<arch>; default derives from host.
Before committing: bun run fix && bun run typecheck. No CI, no pre-commit hooks, no lint-staged — so these are on you.
Drizzle (v0.x — critical)
Why it matters: the project was on 1.0 beta and was rolled back. Online docs default to 1.0 beta APIs that do NOT exist here. If typecheck complains, you are probably importing a 1.0 beta API.
- Driver:
drizzle-orm/postgres-js. Do NOT usedrizzle-orm/bun-sql. drizzle()is called with{ connection, schema }whereschema = import * as schema from '@/server/db/schema'. There is norelations.tsand nodefineRelationsin 0.x.- Zod generators live in the separate
drizzle-zodpackage (^0.8.3). Import fromdrizzle-zod, notdrizzle-orm/zod(that subpath only exists in 1.0 beta). - Relational queries use RQB v1 callback syntax:
Do NOT use the v2 object form (
db.query.todoTable.findMany({ orderBy: (t, { desc }) => desc(t.createdAt), })orderBy: { createdAt: 'desc' },where: { id }) — it won't type-check. - To add relations later: declare per-table with
relations()fromdrizzle-ormand export them from the same file as the table; they get picked up automatically becauseindex.tsdoesdrizzle({ schema })viaimport *. - Every table must spread
...generatedFieldsfromsrc/server/db/fields.ts(givesidUUIDv7 with$defaultFnfallback,createdAt,updatedAtwith$onUpdateFn). There's also ageneratedFieldKeyshelper to feedcreateInsertSchema(...).omit(...). drizzle.config.tsruns outside Vite —@/*path aliases do NOT resolve there. It currently doesimport { env } from './src/env'(relative). Preserve that.- The
./drizzle/migrations directory is gitignored-by-absence right now (no migrations yet).src/server/plugins/migrate.tsrunsmigrate(db, { migrationsFolder: './drizzle' })at server startup, but only when!import.meta.dev. Dev usesdb:push. Don't mixpushandmigrateon the same DB.
ORPC
Contract → Router → Handler → Client, all type-safe from a single contract.
osis built insrc/server/api/server.tsviaimplement(contract).$context<BaseContext>(). Always importosfrom@/server/api/server, never from@orpc/serverdirectly.ORPCError,onError,ValidationErrorcome from@orpc/server.- Contracts (
src/server/api/contracts/*.contract.ts) generate Zod from Drizzle tables viadrizzle-zod:Barrel-aggregated inconst insertSchema = createInsertSchema(todoTable).omit(generatedFieldKeys)contracts/index.tsasexport const contract = { todo }. - Routers chain the
dbmiddleware (src/server/api/middlewares/db.middleware.ts, injects thegetDB()singleton into context). Barrel inrouters/index.tsbuildsos.router({ todo }). - Interceptors are attached at the handler level, not in
server.tsand not onos. Bothsrc/routes/api/rpc.$.ts(RPCHandler) andsrc/routes/api/$.ts(OpenAPIHandler) register[onError(logError)](server) and[onError(handleValidationError)](client). The validation interceptor rewritesBAD_REQUEST + ValidationErrorintoINPUT_VALIDATION_FAILED(422) and output validation errors intoOUTPUT_VALIDATION_FAILED. - OpenAPI/Scalar: docs at
/api/docs, spec at/api/spec.json(handler prefix/api, plugin paths/docsand/spec.json). - SSR isomorphism (
src/client/orpc.ts):createIsomorphicFn().server(createRouterClient(...)).client(new RPCLink(...)). Server branch readsgetRequestHeaders()for context; client branch POSTs to${origin}/api/rpc. - Global mutation invalidation uses
experimental_defaultsincreateTanstackQueryUtils(...)— currently invalidatesorpc.todo.list.key()on everytodo.{create,update,remove}success. Add new features here rather than in each mutation site. - SSR prefetch in route loaders:
await context.queryClient.ensureQueryData(orpc.todo.list.queryOptions()). Components useuseSuspenseQuery(orpc.feature.list.queryOptions()).
Code style (Biome)
- 2-space, LF, single quotes, semicolons as-needed (omitted unless required), 120-col, arrow parens always,
useArrowFunction: "error"— so React components must beconst Foo = () => {...}notfunction Foo() {}. AlsonoReactPropAssignments: "error". - Imports are auto-organized into two groups (external, then
@/*), each alphabetical, withimport typeinterleaved (NOT a separate group).bun run fixhandles this; don't hand-sort. - Files: utils
kebab-case.ts, componentsPascalCase.tsx. routeTree.gen.tsis generated — ignored by Biome, never edit.
TypeScript
Strict mode, plus noUncheckedIndexedAccess, verbatimModuleSyntax, erasableSyntaxOnly, noImplicitOverride. No as any / @ts-ignore / @ts-expect-error.
Path alias: @/* → src/*. For files outside src/ use @/../<file> (example in the codebase: src/routes/api/$.ts imports name, version from @/../package.json).
Env
src/env.ts via @t3-oss/env-core. Server: DATABASE_URL (required, z.url()). Client needs VITE_ prefix (VITE_APP_TITLE optional). Never commit .env.
Docker / deploy
- Multi-stage:
oven/bun:1.3.13builds and runsbun compile.ts, thengcr.io/distroless/cc-debian13:nonrootruns the single./serverbinary. Thecc(glibc) distroless variant is required because Bun's compiled binary links glibc. drizzle/folder is copied into the runtime image somigrate.tsplugin can find migrations at startup.compose.yaml:appwaits ondbhealthcheck (pg_isready);DATABASE_URL=postgres://postgres:postgres@db:5432/postgres.
Layout (non-obvious parts only)
src/
├── client/orpc.ts # isomorphic ORPC client + experimental_defaults invalidation
├── routes/api/
│ ├── $.ts # OpenAPI + Scalar; interceptors registered here
│ └── rpc.$.ts # RPC; interceptors registered here
├── server/
│ ├── api/
│ │ ├── server.ts # the ONLY place to build `os`
│ │ ├── context.ts # BaseContext / DBContext types
│ │ ├── interceptors.ts # logError, handleValidationError
│ │ ├── contracts/ # Zod schemas from Drizzle tables (barrel: contract)
│ │ ├── routers/ # os.* handlers (barrel: router)
│ │ └── middlewares/ # db middleware injects getDB() singleton
│ ├── db/
│ │ ├── index.ts # createDB({ connection, schema }) + getDB()/closeDB() singleton
│ │ ├── fields.ts # pk (UUIDv7), createdAt, updatedAt, generatedFields(Keys)
│ │ └── schema/ # pgTable definitions; also put `relations()` here when adding
│ └── plugins/
│ ├── migrate.ts # Nitro plugin: runs drizzle migrate at prod startup
│ └── shutdown.ts # SIGINT/SIGTERM → closeDB() with 500ms delay
├── env.ts # t3-oss env validation
├── router.tsx # QueryClient + setupRouterSsrQueryIntegration
└── routeTree.gen.ts # auto-generated, do not edit
Nitro plugins are wired in vite.config.ts (nitro({ plugins: [...] })), not via a Nitro config file.
Don'ts (specific, non-obvious)
- Don't edit
routeTree.gen.ts. - Don't import
osfrom@orpc/serverin middleware/routers — always@/server/api/server. - Don't import from
drizzle-orm/zod(1.0 beta only). Usedrizzle-zod. - Don't use RQB v2 object syntax,
defineRelations, or passrelationstodrizzle(). All are 1.0 beta. - Don't use
drizzle-orm/bun-sql. - Don't use
@/*aliases indrizzle.config.ts. - Don't commit
.env.