Backend Overview
The backend lives in apps/backend/ and runs a Hono API server on Node.js. It uses Prisma for schema management, Kysely
for typed queries, PostgreSQL for storage, and Redis (Valkey) for caching.
Folder structure
apps/backend/src/
cache/ # Valkey (Redis) client and initialization
config/ # Environment config, app constants
db/ # Database client (Postgres + Kysely)
features/ # Domain-organized business logic
libs/ # Shared backend libraries and WebSocket setup
middleware/ # Error handling, validation, auth, file uploads
routes/ # HTTP route definitions (thin layer)
sockets/ # WebSocket event definitions
testing/ # Test utilities, mocks, setup
utils/ # Shared helpersBootstrap order
At process start the server roughly does this:
Startup
Validate env
Zod parses process.env. Missing keys stop the process before ports open.
Attach context
Postgres, Valkey, storage, and other clients land on the Hono app context.
Register sockets and routes
WebSocket routers and HTTP routers mount under their prefixes.
Global middleware
CORS and the centralized error middleware wrap every request.
Request lifecycle
Authenticated HTTP request
Global middleware
CORS runs, then the error middleware wraps the rest of the chain.
Route match
Hono picks the feature router mounted from registerRoutes.
Auth and permissions
getAuthenticatedApp() runs withAuthUser. Routes add hasAccess({ action, subject }) when needed.
After a starter packValidation
The validate middleware parses json or query input with Zod and types the handler.
Module then db
The thin route calls a feature module. The module calls queries or mutations. No business logic in the route file.
JSON response or typed error
Success returns ctx.json(...). Failures throw ApplicationError / AuthError / ValidationError and the error middleware formats the response (and reports unexpected errors to Sentry when configured).
Layer responsibilities
| Layer | Responsibility | Must not do |
|---|---|---|
main / app bootstrap | env, context, mount routers, global middleware | Own feature rules |
| Global middleware | CORS, centralized errors | Feature-specific DB writes |
| Route middleware | Auth, validate(), hasAccess(), uploads | Orchestrate multi-step business flows |
| Route handler | Read context, call one module, return JSON | Raw SQL or provider SDK sprawl |
| Feature module | Business rules and orchestration | Know HTTP status codes |
db/queries / db/mutations | Persistence | Session / request semantics beyond args |
Key patterns
-
Features own business logic. Each domain (projects, notifications, auth) has its own folder in
src/features/with modules, database queries, mutations, and schemas. See How to organize features. -
Routes are thin. Route files validate input, read context, call a module function, and return a response. No business logic in routes. See Routing.
-
No try/catch. A centralized error middleware handles all errors. Throw typed errors (
ApplicationError,AuthError,ValidationError) and let the middleware do its job. Swallowing errors also skips Sentry. -
Validation goes through Zod. Every route that accepts input uses the
validate()middleware with a Zod schema. This powers both runtime validation and the end-to-end type bridge to the frontend SDK.
HyperFetch SDK generation
registerRoutes returns a typed Hono app. The frontend HyperFetch SDK is derived from that type, so a new route appears
on sdk.* without a separate codegen step. Details: end-to-end type safety and
HTTP.
Database access
- Prisma for schema definition, migrations, and seeding
- Kysely for typed SQL queries (reads and complex writes)
- Prisma for inserts, Kysely for everything else
Reads go in db/queries/, writes go in db/mutations/. See Prisma and Kysely and
Migrations.
Every purchase and sponsorship funds my 8+ years of work on open source given freely to the community. It keeps the lights on, funds new packs, and keeps the ecosystem alive. Even a small tier means a lot. Thank you!
