# blenaxis-backend: notes for Claude

Node/Express/Prisma/PostgreSQL backend for BlenAxis. Read `README.md` for features, structure, and the full endpoint list.
Two separate frontends consume this backend: `../blenaxis-frontend` (tenant app) and `../blenaxis-super-admin` (platform owner).
The SaaS Admin domain (organizations, plans, modules, settings, audit logs, invites) exists specifically to match
`../blenaxis-super-admin`'s documented API contract exactly — check that repo's `README.md`/types before changing those routes.

## Stack

Express 5, TypeScript (strict), Prisma 7 (`@prisma/adapter-pg`) + PostgreSQL, Joi validation, Firebase Admin SDK
(user creation/token verification) + Identity Toolkit REST via native `fetch` (password sign-in/refresh — no
firebase package needed for that), multer (local disk uploads), winston + winston-daily-rotate-file, `tsx` for
dev/test, Node's built-in `node:test` for tests, swagger-ui-express for `/docs`.

## Commands

`npm run dev` (port 3000) · `npm run typecheck` · `npm test` · `npm run build` · `npm run db:generate` ·
`npm run db:migrate` · `npm run db:deploy` · `npm run db:seed` · `npm run db:studio`.
Before finishing, run typecheck, build and test; all must pass. **Never run `npm run db:seed` unless explicitly
asked** — it's idempotent but the user wants control over when it runs.

## Rules

- MVC per resource: `validators/<x>.validator.ts` → `models/<x>.model.ts` → `controllers/<x>.controller.ts` →
  `routes/<x>.routes.ts`, wired in `routes/index.ts`.
- Controllers: plain `async function` declarations with explicit `try/catch { next(error); return; }` — not
  arrow/const, not an `asyncHandler` wrapper. All exports grouped in one `export { ... }` at the file's bottom.
- snake_case DB columns via Prisma `@map()`; camelCase everywhere in TS.
- Every response is `{ success, message, data, errors }`. Every `:id` in a URL is base64url-encoded
  (`src/utils/idCodec.ts`, `router.param('id', decodeId)`) — no exceptions currently. FK ids inside request
  *bodies* stay plain integers (e.g. `roleId`, `planId`).
- Pagination: `{ items, pagination: { page, limit, total, totalPages } }` — **except** `GET /organizations` and
  `GET /audit-logs`, which return the flat `{ items, total, page, pageSize }` shape blenaxis-super-admin expects.
- RBAC: `authorize('role1', 'role2', ...)` checks the user's role name — used everywhere live.
  `requirePermission()` (finer-grained, checks `role_permissions`) is built and tested but **not wired into any
  route** — don't attach it without being asked.
- `src/config/modules.ts` (`MODULE_KEYS`/`CORE_MODULE_KEYS`) is a cross-repo contract shared with
  `../blenaxis-super-admin` and `../blenaxis-frontend`. Change it only together with both.
- `Plan.id` is a cuid string, not the usual autoincrement int — blenaxis-super-admin treats plan ids as opaque
  strings, same as it does organization ids after encoding.
- Audit logging: always write through `logAudit()` in `src/models/auditLog.model.ts`, never
  `prisma.platformAuditLog.create` directly. Every entry needs a `target: { type, id, label }` and, for updates,
  a `changes: [{ field, before, after }]` array of plain display strings (matches blenaxis-super-admin's
  `AuditLogEntry` exactly — not a nested before/after object).
- Soft delete is explicitly deferred — do not implement on any table until the user asks directly.
- The super admin account is bootstrapped by `seedSuperAdmin()` in `prisma/seed.ts` — self-healing, no required
  env vars (`SUPER_ADMIN_EMAIL`/`PASSWORD` are optional overrides only). Never build a signup path for it.

## Gotchas

- Images (organization logo, profile photos, `/uploads/images`) go to S3 through `src/utils/storage.ts`: store the object
  key, return `fileUrl(key)` (signed, or under `AWS_S3_PUBLIC_BASE_URL`). **Project documents stay on local disk; don't
  move them to S3.** Public files (the email logo, linked directly from the email templates) live in the separate public bucket
  `AWS_S3_PUBLIC_BUCKET` (`npm run storage:create-public-bucket`, `npm run storage:upload-brand`); the main bucket stays private.

- An organization is suspended when `status === 'suspended'` (set by Super Admin's `PATCH /organizations/:id/status`, which
  also keeps the legacy `isActive` in step). Always check it with `isOrganizationSuspended()` (`src/utils/organizationStatus.ts`),
  never `organization.isActive` alone.

- IDE diagnostics often show stale Prisma-type errors right after a schema edit, before `db:generate` finishes
  propagating. Always verify with a fresh `npx tsc --noEmit` before trusting them.
- `prisma migrate dev` fails in this non-interactive environment. Instead: `npx prisma migrate diff
  --from-config-datasource --to-schema prisma/schema.prisma --script > <migrations>/<ts>_<name>/migration.sql`,
  review the SQL, then `npx prisma migrate deploy`. Prefer additive/nullable columns so existing rows never need
  backfilling or deleting — destructive statements (`TRUNCATE`, etc.) get blocked by an external safety
  classifier regardless of intent.
- `tsx watch` clears the terminal by default — the `dev` script already passes `--clear-screen=false`; keep it.
- `signup`/`login`/`refresh` return the session two ways: `accessToken`/`refreshToken` in the JSON body (for a
  client with no Firebase SDK, like blenaxis-super-admin) *and* `X-Id-Token`/`X-Refresh-Token` headers.
- Firebase Web API Key ≠ anything in the service-account JSON — it's a separate credential (Console → Project
  Settings → General), needed for `firebaseIdentity.ts`'s password sign-in/refresh calls.
- `CORS_ORIGIN` is empty (blocks everything) by default — add the frontend's real origin
  (`http://localhost:5173`/`5174`) or every cross-origin request fails before it reaches a route.
- blenaxis-super-admin's mocking layer uses MSW with `onUnhandledRequest: 'bypass'` — an endpoint it doesn't mock
  automatically hits this real backend already. When an endpoint here is ready for that frontend, the matching
  mock handler in `../blenaxis-super-admin/src/mocks/handlers/*` needs removing too, or nothing changes for the
  user even though the API works.
- Local Postgres: `PGPASSWORD=eia123 psql -h localhost -U root -d blenaxis`. Ask before running writes/DDL
  against it, per standing user preference — reads are fine.
- Tests stub Prisma methods directly on the shared client (`stubMethod(t, prisma.x, 'method', fn)`, restored via
  `t.after()`). Careless stubbing of `prisma.user.findUnique` can also break `verifyFirebaseToken`'s own user
  lookup for later requests in the same test, since both use the same method — stub `prisma.user.update`/other
  distinct methods instead when a model function already uses a different one for its own purpose.
