apps/api is the full-stack backend. One source tree hosts three entry points that share the same code — resources, schemas, the @/db service:
The stack is Hono · oRPC · Drizzle ORM · PostgreSQL · better-auth · Socket.IO. No controllers, no route registry — the filesystem is the wiring.
A resource owns everything
Every business entity is a folder underapps/api/src/resources/<name>/. The resource owns its table, its schemas, its endpoints, its side effects and its gates:
Endpoints are oRPC
Every endpoint builds on the shared@/endpoint builder — the oRPC builder with all global middlewares already applied — and declares its input, output and handler:
resources/users/endpoints/list.ts
.input() / .output() are Zod schemas — the single source of truth for the runtime and the types the client sees. Gates compose with .use(...). The handler returns a value; oRPC serialises it. See Routing and Middlewares.
Contract-first, generated from the filesystem
oRPC separates a thin contract (route methods + paths) from the router (the implementations). Ship generates both from your endpoint files so you never hand-edit a route table:scripts/codegen-router.ts walks resources/**/endpoints/ and writes:
src/contract.ts— theoc.router(...)contract, oneoc.route({ method, path })per endpoint file.src/router.ts—implement(contract).router({...})wiring each endpoint into its slot, plus the exportedAppClienttype the web app consumes.
list.ts → GET /users, create.ts → POST, [userId]/update.ts → PUT /users/{userId}. The route table is the directory tree.
Data access is a typed service
scripts/codegen-db.ts generates a DbService per table, exported from @/db — a thin, typed wrapper over Drizzle with a uniform filter API:
find / findFirst / findPage / count / insertOne / insertMany / updateOne / updateMany / deleteOne / deleteMany, plus db.transaction(...) and relation loading via with / columns. Every table extends baseColumns — a uuid id, createdAt, updatedAt and a deletedAt soft-delete column. Mutations emit a typed MutationEvent; drop a handler in <resource>/handlers/ to react. The DbService lives in the @ship/db package; see How Ship works for the full data-access model.
Migrations
Drizzle owns the schema lifecycle:Auth and context
better-auth resolves the session on every request.server.ts builds an ORPCContext and calls serverConfig.resolveUser, which reads the better-auth session from the request headers and attaches the row to context.user when signed in:
isAuthorized and isAdmin gates read context.user. The full auth surface — sign-in/up, verification, reset, Google OAuth, plus the web pages — is delivered by the Auth plugin (see Plugins).
Interactive API reference
In non-production, Hono serves a live Scalar UI in-process.server.ts generates an OpenAPI 3.1 spec from the router and renders the docs with an interactive “try it out”:
- Scalar UI — http://localhost:3001/docs
- Raw spec —
http://localhost:3001/spec.json

