This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Bun is the runtime for everything (dev server, scripts, seeds, ClickHouse migrate script). Do not assume Node — tsconfig.json declares types: ["bun-types"] and Bun is a global in ESLint. Scripts are invoked via bun run … or make … (the Makefile is a thin wrapper around the bun run scripts).
bun run dev— start the API with hot reload (entry:src/index.ts→src/app.ts). The BullMQ worker is loaded in-process viasrc/bull/index.ts, so there is no separatedev:workerdespite what the README implies.bun run build—bun buildtodist/api, targetnode.bun run start— production start.bun run lint/lint:fix/format— ESLint (typed) and Prettier (tabs, double quotes, 80 cols, LF).bun run typecheck—tsc --noEmit.bun run db:generate/db:migrate/db:push/db:studio/db:drop— Drizzle Kit against PostgreSQL. Schema and migrations live insrc/libs/database/postgres/(seedrizzle.config.ts).bun run db:seed— seeds viasrc/libs/database/seed/index.ts.bun run db:clickhouse:migrate/db:clickhouse:status— custom migrator atsrc/libs/database/clickhouse/scripts/migrate.ts.bun run i18n:keys— regenerateTranslationKeytype fromen.json. Validates all locales have matching keys.make fresh=db:drop && db:push && db:seed(dev only).make reset=db:generate && db:migrate && db:seed.
There is no test runner configured; do not invent test commands.
Note: the root README.md references dev:server, dev:worker, dev:all, build:all, start:all — those scripts do not exist in package.json. Use the commands above.
.husky/pre-commit runs: bun install → bun run format → bun run lint:fix → bunx drizzle-kit generate → bunx drizzle-kit migrate → tsc --noEmit → bun run build. This means a commit will attempt to run real DB migrations against DATABASE_URL — keep .env pointing at a safe local DB while committing, or expect the hook to fail/migrate.
src/index.ts exports { port, fetch } for Bun. src/app.ts builds the Hono app and applies middleware in this fixed order — preserve it when adding new ones:
requestIdMiddleware(must be first; everything downstream reads the ID)loggerMiddleware,performanceMiddlewarelocaleMiddleware— parsesAccept-Languageheader and sets locale viaAsyncLocalStoragediMiddleware— injects services from the DI container intoc.varbodyLimitMiddleware,corsMiddleware,securityHeadersMiddleware,rateLimiterMiddlewareregisterException(app)— centralized error handler (seesrc/libs/hono/errors/error.handler.ts)app.route("/", bootstrap)mountssrc/modules/index.ts, which wires/,/auth,/profile,/settings, plus/docs(Scalar) and/docs/openapi.json.
Hand-rolled container at src/libs/hono/core/container.ts (singleton cache keyed by string name).
- Register services once in
src/bootstrap.ts(bootstrap()is called fromapp.ts). - Inject services into the Hono context in
src/libs/hono/middlewares/core/di.middleware.ts— every registered service must bec.set(...)here and typed insrc/libs/types/hono/app.types.ts(Variables). - Resolve in routes via
c.get("authService")etc. Routes nevernewservices directly.
When you add a new service: implement an IXxxService interface in service.interface.ts, register in bootstrap.ts, set in di.middleware.ts, add to Variables. Forgetting any one of those four steps breaks types or runtime.
Each feature module under src/modules/<name>/ follows:
routes.ts—OpenAPIHonoinstance,createRoutedefinitions, handlers that pull the service viac.get(...).schema.ts— Zod request/response schemas (use.openapi(...)for docs).service.interface.ts— interface only (ESLint ignores**/*.interface.ts).service.ts(orservices.tsinsettings/users) — implementation; talks to repositories from@database.
src/modules/settings/index.ts is a sub-router that further mounts permissions, roles, users, select-options.
src/libs/database/index.tsre-exports postgres, clickhouse, and redis clients. Always import via@databaserather than reaching into the subpaths.- PostgreSQL: Drizzle (
drizzle-orm/node-postgres) with a sharedpg.Pool. Schema insrc/libs/database/postgres/schema/, repositories in…/repositories/. Repositories are factory functions (e.g.UserRepository()) that accept an optionalDbTransactionso they compose insidedb.transaction(async trx => …). - ClickHouse migrations are custom (not Drizzle) — script-based.
- Redis is used by both
hono-rate-limiterand BullMQ.
src/bull/queue/*.queue.ts defines queues; src/bull/worker/*.worker.ts defines workers. src/bull/index.ts side-effect-imports the workers, so simply importing anything from @bull boots the worker in the same process as the API. There is no separate worker entrypoint.
RBAC primitives live in src/libs/hono/guards/:
roleGuard(roles[]),permissionGuard(permissions[])requireGuards({ roles, permissions })returns a middleware array to spread.Guards.*(e.g.Guards.userManagement.list()) are named convenience guards backed by string permission keys like"user list","role create". When adding a new resource, add both the permission strings (seed/migrations) and theGuards.*helpers so routes stay declarative.
Routes apply AuthMiddleware first, then a Guards.* middleware before the handler.
- Throw typed errors from
@errors(UnprocessableEntityError,NotFoundError,UnauthorizedError, …). The handler registered inapp.tsformats them; neverc.json({error: …}, 400)by hand. UnprocessableEntityErrortakes a field-keyed validation array — match the existing shape so the OpenAPIcommonResponse(...)examples remain accurate.- Use
ResponseToolkit.success(c, data, message, status)for success responses to keep the envelope consistent. defaultHookfrom@errorsis passed to everynew OpenAPIHono({ defaultHook })so Zod validation failures route through the same formatter.
src/libs/i18n/ provides locale-aware response messages. Supported locales: en (default), id (Indonesian).
localeMiddlewareparses theAccept-Languageheader on every request and stores the locale inAsyncLocalStorageviaenterLocale(). It also sets theContent-Languageresponse header.- Call
t("translation.key")from@i18nanywhere — services, routes, error handler, guards — it reads the current locale automatically. - Translation files are flat JSON in
src/libs/i18n/locales/{en,id}.jsonusing dot-notation keys (e.g."auth.loginSuccess"). TranslationKeyis a generated union type inlocales/keys.generated.ts. Regenerate withbun run i18n:keysafter editing locale JSON files.- Variable interpolation:
t("key", { name: "John" })resolves{{name}}in the template. - When adding a new translation: add the key to both
en.jsonandid.json, then runbun run i18n:keys. The script validates both files have matching keys.
Defined in tsconfig.json and used pervasively — prefer them over relative paths:
@config, @cache, @default, @mail/*, @database, @database/*,
@hono-libs, @hono-libs/*, @errors, @guards,
@utils, @utils/*, @types, @modules, @modules/*, @bull, @bull/*,
@i18n
- TypeScript strict mode + ESLint
recommendedTypeChecked.anyis an error, not a warning; useunknown+ narrowing.no-floating-promisesis also an error —awaitorvoidevery promise. - Prettier uses tabs (width 2), double quotes, trailing commas, LF endings. The repo is tab-indented; preserve that.
- Don't add inline comments on every line. Comments belong above non-obvious blocks only (per
.github/copilot-instructions.md). - Files matching
**/*.interface.tsand**/interface(s)/**are excluded from lint — keep interfaces in those names so they stay declaration-only. - Console logging triggers
no-consolewarnings; existing exceptions use// eslint-disable-next-line no-console.
Architectural details beyond this file live in docs/: API_DOCUMENTATION.md, SECURITY.md, MIDDLEWARE.md, ERROR_HANDLING.md, CONFIGURATION.md, PLUGINS.md. The roadmap is in TODO.md.