Backend Building
Stack: tRPC + Drizzle ORM + Hono + MySQL + OAuth 2.0
A backend-only skill that grafts onto an existing webapp-building project. It adds api/, contracts/ directories and optionally db/ — but never replaces or modifies existing frontend files.
Prerequisite: An existing project created by webapp-building.
Features
Features can be installed incrementally. Base infrastructure (Hono server, tRPC, contracts) is always installed automatically on first run.
| Feature |
What it provides |
Dependencies |
db |
Drizzle ORM + MySQL — adds db/, api/queries/connection.ts, drizzle.config.ts |
— |
auth |
Kimi OAuth + user management — adds api/kimi/, Login page, useAuth, AuthLayout |
requires db (auto-included) |
Default: init.sh "App" with no --features → defaults to auth (= db + auth), preserving backward compat.
Workflows
Frontend-first (recommended when UI is already built):
webapp-building init → develop UI pages and components
backend-building graft (this skill) — auto-wires tRPC providers and routes
- Verify with
npm run check, then add tRPC routers, database tables, wire frontend to API
Full-stack from scratch (recommended when backend is needed immediately):
webapp-building init → immediately graft backend-building
- Verify with
npm run check
- Develop frontend and backend together
Incremental features (add capabilities over time):
- Start with
--features db for database only
- Later add auth:
init.sh "App" --features auth
Quick Start
1. Initialize Frontend First (webapp-building)
bash /app/.agents/skills/webapp-building/scripts/init-webapp.sh "My App"
cd /mnt/agents/output/app
2. Graft Backend
# Full stack with auth (default, same as before):
bash /app/.agents/skills/backend-building/scripts/init.sh "My App"
# Database only (no auth):
bash /app/.agents/skills/backend-building/scripts/init.sh "My App" --features db
# Add auth later:
bash /app/.agents/skills/backend-building/scripts/init.sh "My App" --features auth
What this does (base, always on first run):
- Copies
api/, contracts/ into the project
- Patches
vite.config.ts in-place (adds @contracts alias, envDir, build.outDir)
- Adds
tsconfig.server.json and merges @contracts/* path into existing tsconfigs
- Merges
package.json (adds backend deps and scripts)
- Generates
.env with portal credentials
- Runs
npm install
Additional per feature:
- db: Adds
db/ directory, drizzle.config.ts, database connection, DATABASE_URL env var, and db/seed.ts (scaffold for seeding — run with npx tsx db/seed.ts). Do not overwrite the generated api/queries/connection.ts, drizzle.config.ts, or .env — they are complete and correct. Only add your tables to db/schema.ts
- auth: Adds
api/kimi/, auth router, client patches (Login, useAuth, AuthLayout, TRPCProvider), auto-wires Login/NotFound routes into App.tsx
3. Verify Auto-Wiring (see below)
4. Database Setup (if db or auth feature installed)
npm run db:push # sync schema to database (recommended for development)
5. Development
npm run dev
Start development server with HMR at http://localhost:3000
Post-Init Wiring
On first run, init.sh auto-wires TRPCProvider into src/main.tsx. When auth feature is installed, it also adds Login/NotFound routes to src/App.tsx. Check the init output:
- "Auto-wired" — no action needed, proceed to verification
- "Wiring required" — complete the listed steps manually (see Post-Init Wiring for details)
If manual wiring is needed, it typically means:
- Add
import { TRPCProvider } from "@/providers/trpc" to src/main.tsx
- Wrap the content inside
<BrowserRouter> with <TRPCProvider>
- Add Login and NotFound routes to
src/App.tsx
Verification
After init (and manual wiring if needed), verify everything works:
- Run
npm run check — must pass with zero type errors
- Run
npm run dev — server should start at http://localhost:3000
- If any step fails, read the error and fix before proceeding
Common Commands
| Command |
Description |
Requires |
npm run dev |
Start development server with HMR at http://localhost:3000 |
base |
npm run build |
Build for production (outputs to dist/) |
base |
npm start |
Start production server |
base |
npm run check |
Type-check all TypeScript files |
base |
npm run format |
Format code with Prettier |
base |
npm run test |
Run tests with Vitest |
base |
npm run db:push |
Sync schema to DB during development (recommended) |
db |
npm run db:generate |
Generate migration SQL for production deployment |
db |
npm run db:migrate |
Apply pending migration files to the database |
db |
Stack Overview
Backend (added by this skill)
- Hono + tRPC 11.x
- End-to-end type safety
- Public query procedure (base); + authenticated/admin procedures (auth feature)
Database (db feature)
- Drizzle ORM with MySQL
- Lazy
getDb() connection, ready to use
- Type-safe queries (Guide)
- Schema migrations
Authentication (auth feature)
- OAuth 2.0 (Details)
- JWT sessions, admin role support
Frontend (from webapp-building, untouched)
- React 19 + TypeScript + Vite (HMR)
- Tailwind CSS + shadcn/ui components
- Dark/light mode
Common Mistakes
- Don't hand-write TypeScript interfaces for DB entities — use
typeof table.$inferSelect from db/schema.ts to keep types aligned with superjson's Date serialization via tRPC. Hand-written createdAt: string will conflict with the actual Date objects delivered by superjson
- Don't write raw SQL — always use Drizzle's type-safe query API (
getDb().query.*, getDb().select(), getDb().insert())
- Don't modify
api/lib/ or api/kimi/ — these are framework internals (auth, static file serving). Build on top of them, not inside them
- Don't import
api/ from frontend code — use @contracts/ for types/constants that cross the boundary. The only exception is the type import in src/providers/trpc.tsx
- Don't use
api as the tRPC client name — it's trpc (imported from @/providers/trpc)
- Don't skip Zod validation on tRPC inputs — always use
.input(z.object({...})) for mutations and parameterized queries
- Don't use
serial() for foreign key columns — serial() creates bigint unsigned auto_increment, and MySQL only allows one auto-increment column per table. FK columns must use bigint("col", { mode: "number", unsigned: true }) to match the PK type
- Don't use
int() for foreign keys referencing serial() PKs — int is signed 4-byte, serial() is bigint unsigned 8-byte. MySQL rejects the type mismatch. Use bigint("col", { mode: "number", unsigned: true })
- NEVER drop tables to fix a failed migration — the database may contain user data. Fix
schema.ts and run npm run db:push to sync. See Troubleshooting below
- NEVER use
db:push --force — it auto-accepts destructive changes (dropping columns/tables) which can destroy user data
- NEVER modify
.env values — .env is generated by init.sh with valid, working credentials. All values (API keys, OAuth URLs, secrets, database URL) are pre-configured and ready to use. Do not overwrite, regenerate, or placeholder-ify them
- NEVER overwrite or replace init.sh-generated infrastructure files — files like
api/queries/connection.ts, api/middleware.ts, api/lib/, drizzle.config.ts, src/providers/trpc.tsx, and .env are generated by init.sh with correct configuration. Read the init output to see what was created. Build on top of these files (e.g., add routers, schemas), but do not rewrite them
- Don't change the default port (3000) — the server runs at
http://localhost:3000. Do not change this in vite.config.ts, api/boot.ts, or anywhere else
AI Agent Instructions
Required reading before implementing features — read these docs (under docs/) for the features you're working with:
- db feature: Database.md (schema, migrations, connection), tRPC.md (router patterns, procedures, client usage)
- auth feature: Authentication.md (OAuth flow, session handling), tRPC.md
- All features: Development-Guide.md (dev workflow, scripts), Project-Structure.md (file layout)
When building features on this stack:
- Read existing frontend code in
src/ to understand the app structure
- Design tRPC routers that match the frontend's data needs
- Add DB tables in
db/schema.ts, then run npm run db:push (requires db feature)
- Create new routers in
api/ and register them in api/router.ts
- Use type-safe queries via Drizzle ORM — never raw SQL (requires db feature)
- Frontend components go in
src/ using the existing @/ alias
When to use which features:
- If the user needs a database but no auth, use
--features db
- If they need user login, use
--features auth (includes db automatically)
- Default (no
--features flag) = auth, which is backward compatible
Key Paths
| Path |
What to do |
src/main.tsx |
TRPCProvider auto-wired here (verify after init) |
src/App.tsx |
Login/NotFound routes auto-wired here (verify after init, auth feature only) |
src/pages/ |
Route-level page components (one per route in App.tsx) |
src/sections/ |
Visual sections within a page (Hero, Footer, etc.) — from webapp-building templates |
src/components/ |
Create new UI components |
api/router.ts |
Register new tRPC routers |
api/queries/ |
Add query functions for new tables (db feature) |
db/schema.ts |
Add new database tables (db feature) |
contracts/ |
Shared types/constants (frontend + backend) |
api/lib/ |
Framework internals — don't modify |
api/kimi/ |
Kimi SDK modules — don't modify (auth feature) |
Full directory tree: Project Structure
Detailed Documentation
- Project Structure - Directory layout and where to put things
- Development Guide - Component development, API patterns, common workflows
- Authentication - OAuth 2.0 implementation details (optional)
- Database Guide - Drizzle ORM configuration, schema design, migrations
- tRPC Best Practices - Router patterns and type safety
Routing (react-router)
This template uses react-router v7.
// Navigation
import { useNavigate } from "react-router";
const navigate = useNavigate();
navigate("/dashboard");
// Current location
import { useLocation } from "react-router";
const location = useLocation();
console.log(location.pathname);
// Route params
import { useParams } from "react-router";
const { id } = useParams<{ id: string }>();
// Links
import { Link } from "react-router";
<Link to="/about">About</Link>
// Route definitions (in App.tsx)
import { Routes, Route } from "react-router";
<Routes>
<Route path="/" element={<Home />} />
<Route path="/users/:id" element={<UserProfile />} />
<Route path="*" element={<NotFound />} />
</Routes>
// BrowserRouter wraps the app in main.tsx
import { BrowserRouter } from "react-router";
<BrowserRouter>
<App />
</BrowserRouter>
Troubleshooting
Port 3000 already in use — Another process is using the port. Kill it with lsof -ti:3000 | xargs kill.
Database connection refused — Check DATABASE_URL in .env is correct and MySQL is running. Run npm run db:push before npm run dev.
OAuth callback fails — The callback URL must be {origin}/api/oauth/callback. Verify VITE_KIMI_AUTH_URL and VITE_APP_ID in .env match the portal app config.
npm run db:migrate fails — NEVER drop tables to recover. MySQL doesn't support transactional DDL, so a failed migration can leave the DB in a partial state. To recover:
- Fix
schema.ts (e.g., correct FK types)
- Run
npm run db:push — it introspects the actual DB state and syncs it to your corrected schema
- Delete the broken migration file and its entry in
db/migrations/meta/_journal.json
- Run
npm run db:generate to create a clean baseline migration
Type errors after adding a new router — Make sure you registered the router in api/router.ts inside appRouter. The AppRouter type is derived from there and propagates to the frontend automatically.
1---2name: backend-building3description: Backend building that grafts tRPC + Drizzle ORM + Hono onto an existing webapp-building frontend. Supports incremental features (db, auth). Use when the user needs a backend, API, database, server, authentication, or wants to add tRPC/Drizzle to their webapp-building project. Requires webapp-building first.4---56# Backend Building78**Stack**: tRPC + Drizzle ORM + Hono + MySQL + OAuth 2.0910A backend-only skill that grafts onto an existing `webapp-building` project. It adds `api/`, `contracts/` directories and optionally `db/` — but **never replaces or modifies** existing frontend files.1112**Prerequisite**: An existing project created by `webapp-building`.1314## Features1516Features can be installed incrementally. Base infrastructure (Hono server, tRPC, contracts) is always installed automatically on first run.1718| Feature | What it provides | Dependencies |19|---------|-----------------|--------------|20| `db` | Drizzle ORM + MySQL — adds `db/`, `api/queries/connection.ts`, `drizzle.config.ts` | — |21| `auth` | Kimi OAuth + user management — adds `api/kimi/`, Login page, useAuth, AuthLayout | requires `db` (auto-included) |2223Default: `init.sh "App"` with no `--features` → defaults to `auth` (= db + auth), preserving backward compat.2425## Workflows2627**Frontend-first** (recommended when UI is already built):28291. `webapp-building` init → develop UI pages and components302. `backend-building` graft (this skill) — auto-wires tRPC providers and routes313. Verify with `npm run check`, then add tRPC routers, database tables, wire frontend to API3233**Full-stack from scratch** (recommended when backend is needed immediately):34351. `webapp-building` init → immediately graft `backend-building`362. Verify with `npm run check`373. Develop frontend and backend together3839**Incremental features** (add capabilities over time):40411. Start with `--features db` for database only422. Later add auth: `init.sh "App" --features auth`4344## Quick Start4546### 1. Initialize Frontend First (webapp-building)4748```bash49bash /app/.agents/skills/webapp-building/scripts/init-webapp.sh "My App"50cd /mnt/agents/output/app51```5253### 2. Graft Backend5455```bash56# Full stack with auth (default, same as before):57bash /app/.agents/skills/backend-building/scripts/init.sh "My App"5859# Database only (no auth):60bash /app/.agents/skills/backend-building/scripts/init.sh "My App" --features db6162# Add auth later:63bash /app/.agents/skills/backend-building/scripts/init.sh "My App" --features auth6465```6667**What this does (base, always on first run):**6869- Copies `api/`, `contracts/` into the project70- Patches `vite.config.ts` in-place (adds `@contracts` alias, `envDir`, `build.outDir`)71- Adds `tsconfig.server.json` and merges `@contracts/*` path into existing tsconfigs72- Merges `package.json` (adds backend deps and scripts)73- Generates `.env` with portal credentials74- Runs `npm install`7576**Additional per feature:**7778- **db**: Adds `db/` directory, `drizzle.config.ts`, database connection, `DATABASE_URL` env var, and `db/seed.ts` (scaffold for seeding — run with `npx tsx db/seed.ts`). **Do not overwrite** the generated `api/queries/connection.ts`, `drizzle.config.ts`, or `.env` — they are complete and correct. Only add your tables to `db/schema.ts`79- **auth**: Adds `api/kimi/`, auth router, client patches (Login, useAuth, AuthLayout, TRPCProvider), auto-wires Login/NotFound routes into `App.tsx`8081### 3. Verify Auto-Wiring (see below)8283### 4. Database Setup (if db or auth feature installed)8485```bash86npm run db:push # sync schema to database (recommended for development)87```8889### 5. Development9091```bash92npm run dev93```9495Start development server with HMR at `http://localhost:3000`9697## Post-Init Wiring9899On first run, `init.sh` auto-wires `TRPCProvider` into `src/main.tsx`. When `auth` feature is installed, it also adds Login/NotFound routes to `src/App.tsx`. Check the init output:100101- **"Auto-wired"** — no action needed, proceed to verification102- **"Wiring required"** — complete the listed steps manually (see [Post-Init Wiring](docs/Post-Init-Wiring.md) for details)103104If manual wiring is needed, it typically means:1051061. Add `import { TRPCProvider } from "@/providers/trpc"` to `src/main.tsx`1072. Wrap the content inside `<BrowserRouter>` with `<TRPCProvider>`1083. Add Login and NotFound routes to `src/App.tsx`109110### Verification111112After init (and manual wiring if needed), verify everything works:1131141. Run `npm run check` — must pass with zero type errors1152. Run `npm run dev` — server should start at http://localhost:30001163. If any step fails, read the error and fix before proceeding117118## Common Commands119120| Command | Description | Requires |121| -------------------- | ---------------------------------------------------------- | -------- |122| `npm run dev` | Start development server with HMR at http://localhost:3000 | base |123| `npm run build` | Build for production (outputs to dist/) | base |124| `npm start` | Start production server | base |125| `npm run check` | Type-check all TypeScript files | base |126| `npm run format` | Format code with Prettier | base |127| `npm run test` | Run tests with Vitest | base |128| `npm run db:push` | Sync schema to DB during development (recommended) | db |129| `npm run db:generate`| Generate migration SQL for production deployment | db |130| `npm run db:migrate` | Apply pending migration files to the database | db |131132## Stack Overview133134### Backend (added by this skill)135136- Hono + tRPC 11.x137- End-to-end type safety138- Public query procedure (base); + authenticated/admin procedures (auth feature)139140### Database (db feature)141142- Drizzle ORM with MySQL143- Lazy `getDb()` connection, ready to use144- Type-safe queries ([Guide](docs/Database.md))145- Schema migrations146147### Authentication (auth feature)148149- OAuth 2.0 ([Details](docs/Authentication.md))150- JWT sessions, admin role support151152### Frontend (from webapp-building, untouched)153154- React 19 + TypeScript + Vite (HMR)155- Tailwind CSS + shadcn/ui components156- Dark/light mode157158## Common Mistakes159160- **Don't hand-write TypeScript interfaces for DB entities** — use `typeof table.$inferSelect` from `db/schema.ts` to keep types aligned with superjson's Date serialization via tRPC. Hand-written `createdAt: string` will conflict with the actual `Date` objects delivered by superjson161- **Don't write raw SQL** — always use Drizzle's type-safe query API (`getDb().query.*`, `getDb().select()`, `getDb().insert()`)162- **Don't modify `api/lib/` or `api/kimi/`** — these are framework internals (auth, static file serving). Build on top of them, not inside them163- **Don't import `api/` from frontend code** — use `@contracts/` for types/constants that cross the boundary. The only exception is the type import in `src/providers/trpc.tsx`164- **Don't use `api` as the tRPC client name** — it's `trpc` (imported from `@/providers/trpc`)165- **Don't skip Zod validation** on tRPC inputs — always use `.input(z.object({...}))` for mutations and parameterized queries166- **Don't use `serial()` for foreign key columns** — `serial()` creates `bigint unsigned auto_increment`, and MySQL only allows one auto-increment column per table. FK columns must use `bigint("col", { mode: "number", unsigned: true })` to match the PK type167- **Don't use `int()` for foreign keys referencing `serial()` PKs** — `int` is signed 4-byte, `serial()` is `bigint unsigned` 8-byte. MySQL rejects the type mismatch. Use `bigint("col", { mode: "number", unsigned: true })`168- **NEVER drop tables to fix a failed migration** — the database may contain user data. Fix `schema.ts` and run `npm run db:push` to sync. See Troubleshooting below169- **NEVER use `db:push --force`** — it auto-accepts destructive changes (dropping columns/tables) which can destroy user data170- **NEVER modify `.env` values** — `.env` is generated by `init.sh` with valid, working credentials. All values (API keys, OAuth URLs, secrets, database URL) are pre-configured and ready to use. Do not overwrite, regenerate, or placeholder-ify them171- **NEVER overwrite or replace init.sh-generated infrastructure files** — files like `api/queries/connection.ts`, `api/middleware.ts`, `api/lib/`, `drizzle.config.ts`, `src/providers/trpc.tsx`, and `.env` are generated by `init.sh` with correct configuration. Read the init output to see what was created. Build on top of these files (e.g., add routers, schemas), but do not rewrite them172- **Don't change the default port (3000)** — the server runs at `http://localhost:3000`. Do not change this in `vite.config.ts`, `api/boot.ts`, or anywhere else173174## AI Agent Instructions175176**Required reading before implementing features** — read these docs (under `docs/`) for the features you're working with:177- **db feature**: [Database.md](docs/Database.md) (schema, migrations, connection), [tRPC.md](docs/tRPC.md) (router patterns, procedures, client usage)178- **auth feature**: [Authentication.md](docs/Authentication.md) (OAuth flow, session handling), [tRPC.md](docs/tRPC.md)179- **All features**: [Development-Guide.md](docs/Development-Guide.md) (dev workflow, scripts), [Project-Structure.md](docs/Project-Structure.md) (file layout)180181When building features on this stack:1821831. **Read existing frontend code** in `src/` to understand the app structure1842. **Design tRPC routers** that match the frontend's data needs1853. **Add DB tables** in `db/schema.ts`, then run `npm run db:push` (requires db feature)1864. **Create new routers** in `api/` and register them in `api/router.ts`1875. **Use type-safe queries** via Drizzle ORM — never raw SQL (requires db feature)1886. **Frontend components** go in `src/` using the existing `@/` alias189190**When to use which features:**191- If the user needs a database but no auth, use `--features db`192- If they need user login, use `--features auth` (includes db automatically)193- Default (no `--features` flag) = `auth`, which is backward compatible194195### Key Paths196197| Path | What to do |198|------|-----------|199| `src/main.tsx` | TRPCProvider auto-wired here (verify after init) |200| `src/App.tsx` | Login/NotFound routes auto-wired here (verify after init, auth feature only) |201| `src/pages/` | Route-level page components (one per route in App.tsx) |202| `src/sections/` | Visual sections within a page (Hero, Footer, etc.) — from webapp-building templates |203| `src/components/` | Create new UI components |204| `api/router.ts` | Register new tRPC routers |205| `api/queries/` | Add query functions for new tables (db feature) |206| `db/schema.ts` | Add new database tables (db feature) |207| `contracts/` | Shared types/constants (frontend + backend) |208| `api/lib/` | Framework internals — don't modify |209| `api/kimi/` | Kimi SDK modules — don't modify (auth feature) |210211Full directory tree: [Project Structure](docs/Project-Structure.md)212213## Detailed Documentation214215- [Project Structure](docs/Project-Structure.md) - Directory layout and where to put things216- [Development Guide](docs/Development-Guide.md) - Component development, API patterns, common workflows217- [Authentication](docs/Authentication.md) - OAuth 2.0 implementation details (optional)218- [Database Guide](docs/Database.md) - Drizzle ORM configuration, schema design, migrations219- [tRPC Best Practices](docs/tRPC.md) - Router patterns and type safety220221## Routing (react-router)222223This template uses **react-router** v7.224225```typescript226// Navigation227import { useNavigate } from "react-router";228const navigate = useNavigate();229navigate("/dashboard");230231// Current location232import { useLocation } from "react-router";233const location = useLocation();234console.log(location.pathname);235236// Route params237import { useParams } from "react-router";238const { id } = useParams<{ id: string }>();239240// Links241import { Link } from "react-router";242<Link to="/about">About</Link>243244// Route definitions (in App.tsx)245import { Routes, Route } from "react-router";246<Routes>247 <Route path="/" element={<Home />} />248 <Route path="/users/:id" element={<UserProfile />} />249 <Route path="*" element={<NotFound />} />250</Routes>251252// BrowserRouter wraps the app in main.tsx253import { BrowserRouter } from "react-router";254<BrowserRouter>255 <App />256</BrowserRouter>257```258259## Troubleshooting260261**Port 3000 already in use** — Another process is using the port. Kill it with `lsof -ti:3000 | xargs kill`.262263**Database connection refused** — Check `DATABASE_URL` in `.env` is correct and MySQL is running. Run `npm run db:push` before `npm run dev`.264265**OAuth callback fails** — The callback URL must be `{origin}/api/oauth/callback`. Verify `VITE_KIMI_AUTH_URL` and `VITE_APP_ID` in `.env` match the portal app config.266267**`npm run db:migrate` fails** — NEVER drop tables to recover. MySQL doesn't support transactional DDL, so a failed migration can leave the DB in a partial state. To recover:2682691. Fix `schema.ts` (e.g., correct FK types)2702. Run `npm run db:push` — it introspects the actual DB state and syncs it to your corrected schema2713. Delete the broken migration file and its entry in `db/migrations/meta/_journal.json`2724. Run `npm run db:generate` to create a clean baseline migration273274**Type errors after adding a new router** — Make sure you registered the router in `api/router.ts` inside `appRouter`. The `AppRouter` type is derived from there and propagates to the frontend automatically.