Conventions
These rules are codified in AGENTS.md at the repo root — read it before
writing or restructuring code.
Routes: file = URL namespace
Section titled “Routes: file = URL namespace”- Routes live in
src/server/routes/<feature>.routes.tswith handlers inline. - Given any URL, the file name follows from its first segment:
/posts*→routes/posts.routes.ts. Every URL is defined in exactly one file — GET renders and POST actions for the same path live together. pages.routes.tsis the app shell only (/,/dashboard,/admin).- Infra endpoints (
/health,/assets/*) stay inapp.ts.
// routes/auth.routes.ts — sub-app per feature, mounted via app.route()export const authRoutes = () => { const app = new Hono<AppEnv>(); app.get("/login", guestOnly, (c) => c.var.inertia.render("Login", {})); app.post("/login", validateJson(loginBody), async (c) => { ... }); return app;};Server layout
Section titled “Server layout”src/server/is flat exceptroutes/. No feature subfolders.- Shared/transport-independent logic becomes a single flat module — extract only when reused across routes, not to slim a file.
- All SQL lives in
db.tsas prepared statements (db.query(...)), created once after migrations run. Schema changes are new numbered migration files — never edit an applied migration. - Env is read once in
config.ts, validated, fails fast. Never readprocess.envelsewhere.
Validation & types
Section titled “Validation & types”- Validation via TypeBox schemas (
src/server/validation.ts) at route level;app.onErrormapsValidationFailedto Inertia 422 page payloads (VALIDATION_MESSAGESinauth.routes.ts). - TypeScript:
strict+noUncheckedIndexedAccess+verbatimModuleSyntax. Type-only imports MUST useimport type. No ORM, no looseany.
Hono integration notes
Section titled “Hono integration notes”- Middleware runs in registration order; global
app.use()middleware must precede the routes they cover. - Middleware/guards MUST call
next()to continue — returningundefinedwithoutnext()errors with “Context is not finalized”. - Hono converts HEAD → GET (body stripped, headers kept) but
c.req.methodstill reports “HEAD”; the tusdispatchrelies on this. c.header()-queued headers are dropped when a handler returns a customResponse— cookie helpers append toc.res.headersinstead.- The
/*wildcard produces no named param — derive path segments fromc.req.path(seeuploads.routes.ts). @sinclair/typeboxdoes not pre-register string formats —emailis registered invalidation.ts.