This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Avkash is an open-source HR management platform (leave, attendance, people, org, policy) with Slack integration. This branch is Avkash v2 — a Bun + Hono + Drizzle backend in a Turborepo + pnpm monorepo. (Avkash v1 — Next.js 15 + Supabase + Ant Design — lives on main; do not bring its patterns here.)
Stack: Bun (API runtime), Hono (HTTP), Drizzle ORM, Better Auth, PostgreSQL 16, Zod 4. Turborepo + pnpm workspaces. The apps/web Next.js app is currently a stub.
All orchestrated by Turbo from the repo root:
pnpm dev # turbo run dev — API hot-reloads via `bun run --watch`
pnpm build # turbo run build
pnpm typecheck # turbo run typecheck — AUTHORITATIVE for cross-package types
pnpm lint # turbo run lint — ESLint 9 flat config
pnpm format # prettier --check . (format:fix to write)
pnpm db:push # drizzle-kit push — sync schema to the DB (no migration files)
pnpm db:studio # drizzle-kit studio
pnpm knip # dead-code / unused-dep analysisLocal stack runs in Docker (Postgres + API + web):
docker compose up -d --build --force-recreate api # rebuild + REPLACE the api container
docker compose ps # status / health
docker compose logs -f api--force-recreate is essential — --build alone rebuilds the image but does not replace a running container, so code changes silently don't take effect.
Type errors: trust
pnpm typecheck(orturbo run typecheck), not the editor. The editor's LSP frequently shows stale "Cannot find module '@avkash/…'" for the just-in-time packages; Turbo's run is the source of truth.
apps/api— the Bun + Hono HTTP server. The only place transports (HTTP, Slack SDK) are wired.apps/web— Next.js app (stub for now).packages/*— the domain and foundation libraries.
Just-in-time packages: every package exports ./src/index.ts directly ("exports" / "main" point at source) and extends @avkash/tsconfig/base.json — there is no build step. Import across packages via @avkash/<name>; never reach into another package's internal files.
Packages form a strict graph. A package may only import from layers below it:
- Foundation (zero internal deps):
shared(enums, errors,AuthContext, primitives),config(Zod-validated env, fails fast at boot),db(Drizzle schema — the single source of schema truth — client,db:push),i18n(message/error catalogs + locale),emails(React Email templates →renderEmail; preview viapnpm email:dev),tsconfig,eslint-config. - Identity:
auth— Better Auth + API keys; authN resolvers (one per transport) and authz guards. - Domain:
org(the Organisation tenant + lifecycle + invitations),users(people, teams, roles, profiles),leave,attendance,holidays,policy,documents. Domains own their tables and business rules. - Orchestration (top of graph):
jobs(BullMQ workers),payroll,notifications,slack. These reach through domain packages, never intodbdirectly.
When adding code, put it at the right layer. If an orchestration package wants to touch the database, route it through the owning domain instead.
app.ts is the wiring layer: each route group is thin (parse request → call a domain function with ctx → return JSON). Domain logic lives in packages/*, never in routes. The exported AppType is the type-safe contract the web client consumes (no codegen).
- Auth context: handlers receive an
AuthContext(orgId,userId,role); guard withrequireRole(ctx, 'MANAGER')etc. Domain functions arectx-first. - Errors: code-first
DomainErrortaxonomy (packages/shared/src/errors.ts). One envelope:{ error: { code, message, details, requestId } }. Messages localize byAccept-Languagevia@avkash/i18n;mapDatabaseErrortranslates DB constraint violations. Internals are hidden unlessEXPOSE_ERRORS/non-prod. - Request validation: Zod 4 via
validateBody/validateQuerymiddleware (middleware/validate.ts) — read parsed data withc.get('body')/c.get('query'). Don't hand-parsec.req.json(). - Response DTOs:
dto.tsbuilds DTOs fromcreateSelectSchema(table).omit({...})(drizzle-zod) to drop internal columns (orgId, audit fields). Return viaserialize(dto, data)— never dump raw rows. - Optimistic concurrency: mutable resources carry a
versioncolumn. GET returns anETag; PATCH requiresIf-Match(428PreconditionRequiredErrorif missing, 412PreconditionFailedErrorif stale). Helpers inconcurrency.ts; the UPDATE does a CAS onversion. - Idempotency: unsafe creates accept an
Idempotency-Key; theidempotencymiddleware +IdempotencyKeytable fingerprint the request and replay the cached response on retry. - Cross-cutting middleware:
request-id,locale, CORS,internal-auth(token-guarded/internal). Health:/health(liveness),/health/ready(readiness, DB ping with timeout). - Inherit-by-null cascades: settings resolve down a chain, where
nullmeans "inherit" (e.g. workweek user → team → default; holiday location team → org). Mirror this when adding configurable fields.
Drizzle ORM. Schema is defined entirely in packages/db/src/schema/ (one file per area, re-exported from index.ts) and is the single source of truth. No migration files — pnpm db:push (drizzle-kit push) syncs the schema to Postgres directly. After changing schema, run db:push, then rebuild the API container with --force-recreate.
Key enums live in @avkash/shared / packages/db/src/schema/enums.ts (e.g. Role: OWNER, MANAGER, USER, ANON, ADMIN; leave status; attendance punch type/source).
- ESLint 9 flat config. Rules live in
@avkash/eslint-config(typescript-eslint recommended + node globals,eslint-config-prettierlast so Prettier owns formatting). The rooteslint.config.jsis resolved from every package dir, so alleslint .runs and the editor share one config.no-explicit-any/no-unused-varsare warnings. - Prettier owns formatting (
.prettierrc.json, width 120)..zed/settings.jsonenables format-on-save. Keep eslint and prettier non-overlapping — never re-add aprettier/prettiereslint rule.
Conventional Commits, validated by commitlint.config.ts. Format: type(scope): message.
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
- Scopes: setup, config, deps, feature, bug, docs, style, refactor, test, build, ci, release, other
Git hooks are not currently wired (husky was removed with v1). Until they're re-added, commits use --no-verify; keep messages convention-compliant by hand.
- API runtime: Bun. Tooling requires Node >= 20.
@/*-style path aliases are a v1 concept and do not exist here — use@avkash/*workspace imports.