Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
4.3 KiB
Server CLAUDE.md
Agent-facing guide for server/apps/api.
Overview
Hono-based Node.js resource API. The sibling server/apps/auth workspace app owns Better Auth, OIDC, sessions, and account lifecycle; this package owns billing, chat sync, LLM gateway forwarding, and business observability. Multi-instance deployed on Railway — design all features assuming N>1 instances sharing the same Postgres and Redis.
Deployment Model
- Hosted on Railway, multiple instances behind a load balancer.
- Independent applications:
server/apps/api/src/main.tsandserver/apps/auth/src/main.ts. There is no runtime role CLI and neither app imports the other service graph. - The API has no background polling loops or fire-and-forget tasks — every business write happens inside the request thread.
- Stateless per-instance: no local state that matters across requests.
- Cross-instance coordination via Redis Pub/Sub (WebSocket broadcast). DB-level idempotency (
(userId, requestId)partial unique index onflux_transaction) covers retries. - Rate limiting is currently in-memory (not distributed) — keep this in mind when adding rate-sensitive features.
Tech Stack
Hono, Drizzle ORM, PostgreSQL, Redis, Stripe, OpenTelemetry, Valibot, injeca (DI), tsx. Better Auth lives only in server/apps/auth.
Commands
pnpm -F @proj-airi/api-server dev # dev with dotenvx (.env.local)
pnpm -F @proj-airi/api-server typecheck
pnpm -F @proj-airi/api-server exec vitest run # all server tests
pnpm exec vitest run server/apps/api/src/... # single test file
pnpm -F @proj-airi/api-server db:generate # drizzle-kit generate
pnpm -F @proj-airi/api-server db:push # drizzle-kit push
pnpm -F @proj-airi/auth-server auth:generate # better-auth → server/packages/auth-shared/src/schema.ts
Local observability is maintained in proj-airi/airi-railway; run its otel/docker-compose.yaml stack.
Architecture Summary
Entry & DI: server/apps/api/src/main.ts → src/server.ts → src/app.ts; server/apps/auth/src/main.ts → src/server.ts. The workspace apps have separate package manifests, env schemas, Dockerfiles, and composition roots.
Layering:
- Routes (
src/routes/): thin — param validation (Valibot), auth guards, error mapping. No business logic here. - Services (
src/services/): core business logic and DB transactions. - Schemas (
src/schemas/): Drizzle table definitions. Migrations in@proj-airi/drizzle-migration.
Middleware chain (/api/*): CORS → hono/logger → optional otel → sessionMiddleware → bodyLimit(1MB) → per-route guards. WebSocket /ws/chat registered before bodyLimit.
Error model: ApiError(statusCode, errorCode, message, details) in src/utils/error.ts.
Key Design Decisions
- Flux read/write separation:
FluxServicereads (Redis cache-aside),BillingServicewrites (single Postgres tx that mutatesuser_fluxand writes the matchingflux_transactionledger row). Never put write-balance logic influx.ts. - No async billing pipeline: debits and credits update balance + ledger in one transaction. The
(user_id, request_id)partial unique index gives DB-level idempotency for retries; LLMrequest logrows are written best-effort right after the response is delivered. - In-process LLM/TTS router:
/api/v1/openaiis dispatched byservices/domain/llm-routerreadingLLM_ROUTER_CONFIG(per-model upstream chain + envelope-encrypted keys).chat/completionswalks LLM upstreams with key fallback;audio/speechdelegates to a TTS adapter (azure/dashscope-cosyvoice/volcengine);audio/voicesreturns the adapter's compiled-in catalog. Server handles auth/billing/logging, not model execution. - Redis is cache + pub/sub, not truth: balance cache, app_settings read cache, WebSocket cross-instance pub/sub. Truth is always Postgres.
- Auth boundary:
server/apps/authowns Better Auth + OIDC. The API'ssessionMiddlewarevalidates Auth-issued JWTs and fills context but doesn't block;authGuardreturns 401. - Multi-instance safe: all writes go through Postgres transactions; cross-instance messaging uses Redis Pub/Sub. No async work, no in-process singletons — admin flux grants happen synchronously inside the POST that triggered them.