Files
seastem-electronjs/AGENTS.md
T
imbytecat 4518a63959 docs(agents): 同步 drizzle 0.x 降级后的指引
修正 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 等)。
2026-04-24 20:13:56 +08:00

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.toml pins bun = 1.3.13). Never invoke npm/npx/node/yarn/pnpm. Use bun run <script> (bare bun <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 use drizzle-orm/bun-sql.
  • drizzle() is called with { connection, schema } where schema = import * as schema from '@/server/db/schema'. There is no relations.ts and no defineRelations in 0.x.
  • Zod generators live in the separate drizzle-zod package (^0.8.3). Import from drizzle-zod, not drizzle-orm/zod (that subpath only exists in 1.0 beta).
  • Relational queries use RQB v1 callback syntax:
    db.query.todoTable.findMany({
      orderBy: (t, { desc }) => desc(t.createdAt),
    })
    
    Do NOT use the v2 object form (orderBy: { createdAt: 'desc' }, where: { id }) — it won't type-check.
  • To add relations later: declare per-table with relations() from drizzle-orm and export them from the same file as the table; they get picked up automatically because index.ts does drizzle({ schema }) via import *.
  • Every table must spread ...generatedFields from src/server/db/fields.ts (gives id UUIDv7 with $defaultFn fallback, createdAt, updatedAt with $onUpdateFn). There's also a generatedFieldKeys helper to feed createInsertSchema(...).omit(...).
  • drizzle.config.ts runs outside Vite — @/* path aliases do NOT resolve there. It currently does import { env } from './src/env' (relative). Preserve that.
  • The ./drizzle/ migrations directory is gitignored-by-absence right now (no migrations yet). src/server/plugins/migrate.ts runs migrate(db, { migrationsFolder: './drizzle' }) at server startup, but only when !import.meta.dev. Dev uses db:push. Don't mix push and migrate on the same DB.

ORPC

Contract → Router → Handler → Client, all type-safe from a single contract.

  • os is built in src/server/api/server.ts via implement(contract).$context<BaseContext>(). Always import os from @/server/api/server, never from @orpc/server directly. ORPCError, onError, ValidationError come from @orpc/server.
  • Contracts (src/server/api/contracts/*.contract.ts) generate Zod from Drizzle tables via drizzle-zod:
    const insertSchema = createInsertSchema(todoTable).omit(generatedFieldKeys)
    
    Barrel-aggregated in contracts/index.ts as export const contract = { todo }.
  • Routers chain the db middleware (src/server/api/middlewares/db.middleware.ts, injects the getDB() singleton into context). Barrel in routers/index.ts builds os.router({ todo }).
  • Interceptors are attached at the handler level, not in server.ts and not on os. Both src/routes/api/rpc.$.ts (RPCHandler) and src/routes/api/$.ts (OpenAPIHandler) register [onError(logError)] (server) and [onError(handleValidationError)] (client). The validation interceptor rewrites BAD_REQUEST + ValidationError into INPUT_VALIDATION_FAILED (422) and output validation errors into OUTPUT_VALIDATION_FAILED.
  • OpenAPI/Scalar: docs at /api/docs, spec at /api/spec.json (handler prefix /api, plugin paths /docs and /spec.json).
  • SSR isomorphism (src/client/orpc.ts): createIsomorphicFn().server(createRouterClient(...)).client(new RPCLink(...)). Server branch reads getRequestHeaders() for context; client branch POSTs to ${origin}/api/rpc.
  • Global mutation invalidation uses experimental_defaults in createTanstackQueryUtils(...) — currently invalidates orpc.todo.list.key() on every todo.{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 use useSuspenseQuery(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 be const Foo = () => {...} not function Foo() {}. Also noReactPropAssignments: "error".
  • Imports are auto-organized into two groups (external, then @/*), each alphabetical, with import type interleaved (NOT a separate group). bun run fix handles this; don't hand-sort.
  • Files: utils kebab-case.ts, components PascalCase.tsx.
  • routeTree.gen.ts is 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.13 builds and runs bun compile.ts, then gcr.io/distroless/cc-debian13:nonroot runs the single ./server binary. The cc (glibc) distroless variant is required because Bun's compiled binary links glibc.
  • drizzle/ folder is copied into the runtime image so migrate.ts plugin can find migrations at startup.
  • compose.yaml: app waits on db healthcheck (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 os from @orpc/server in middleware/routers — always @/server/api/server.
  • Don't import from drizzle-orm/zod (1.0 beta only). Use drizzle-zod.
  • Don't use RQB v2 object syntax, defineRelations, or pass relations to drizzle(). All are 1.0 beta.
  • Don't use drizzle-orm/bun-sql.
  • Don't use @/* aliases in drizzle.config.ts.
  • Don't commit .env.