5.7 KiB
5.7 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Customer portal with BFF (Backend for Frontend) architecture. Users can self-register, manage subscriptions, view/pay invoices, and manage support cases.
Systems of Record:
- WHMCS: Billing, subscriptions, invoices, authoritative address storage
- Salesforce: CRM (Accounts, Contacts, Cases), order address snapshots
- Portal: Next.js UI + NestJS BFF
Development Commands
# Start development environment (PostgreSQL + Redis via Docker)
pnpm dev:start
# Start both frontend and backend with hot reload
pnpm dev
# Build domain package (required before running apps if domain changed)
pnpm domain:build
# Type checking
pnpm type-check
# Linting
pnpm lint
pnpm lint:fix
# Database commands
pnpm db:migrate # Run migrations
pnpm db:studio # Open Prisma Studio GUI
pnpm db:generate # Generate Prisma client
# Run tests
pnpm test # All packages
pnpm --filter @customer-portal/bff test # BFF only
# Stop services
pnpm dev:stop
Access points:
- Frontend: http://localhost:3000
- Backend API: http://localhost:4000/api
- Prisma Studio: http://localhost:5555
Architecture
Monorepo Structure
apps/
├── portal/ # Next.js 15 frontend (React 19, Tailwind, shadcn/ui)
└── bff/ # NestJS 11 backend (Prisma, BullMQ, Zod validation)
packages/
└── domain/ # Unified domain layer (contracts, schemas, provider mappers)
Three-Layer Boundary (Non-Negotiable)
| Layer | Location | Purpose |
|---|---|---|
| Domain | packages/domain/ |
Shared contracts, Zod validation, provider mappers. Framework-agnostic. |
| BFF | apps/bff/ |
HTTP boundary, orchestration, external integrations (Salesforce/WHMCS/Freebit) |
| Portal | apps/portal/ |
UI layer. Pages are thin wrappers over feature modules. |
Domain Package Structure
Each domain module follows this pattern:
packages/domain/<module>/
├── contract.ts # Normalized types (provider-agnostic)
├── schema.ts # Zod validation schemas
├── index.ts # Public exports
└── providers/ # Provider-specific adapters (BFF-only)
└── whmcs/
├── raw.types.ts # Raw API response types
└── mapper.ts # Transform raw → domain
Import Rules (ESLint Enforced)
Allowed (Portal + BFF):
import type { Invoice } from "@customer-portal/domain/billing";
import { invoiceSchema } from "@customer-portal/domain/billing";
import { Formatting } from "@customer-portal/domain/toolkit";
Allowed (BFF only):
import { Whmcs } from "@customer-portal/domain/billing/providers";
Forbidden everywhere:
// Root import
import { Billing } from "@customer-portal/domain";
// Deep imports beyond entrypoints
import { Invoice } from "@customer-portal/domain/billing/contract";
import { transformWhmcsInvoice } from "@customer-portal/domain/billing/providers/whmcs/mapper";
Forbidden in Portal:
// Portal must NEVER import provider adapters
import { Whmcs } from "@customer-portal/domain/billing/providers";
Portal Feature Architecture
apps/portal/src/
├── app/ # Next.js App Router (thin route shells, no API calls)
├── components/ # Atomic design: atoms/, molecules/, organisms/, templates/
├── core/ # App infrastructure: api/, logger/, providers/
├── features/ # Feature modules with: api/, stores/, components/, hooks/, views/
└── shared/ # Cross-feature: hooks/, utils/, constants/
Feature module pattern:
api/: Data fetching layer (built on shared apiClient)stores/: Zustand state managementhooks/: React Query hooks wrapping API servicescomponents/: Feature-specific UIviews/: Page-level view componentsindex.ts: Feature public API (barrel exports)
BFF Integration Pattern
Map Once, Use Everywhere:
External API → Integration Service → Domain Mapper → Domain Type → Use Directly
Integration services live in apps/bff/src/integrations/{provider}/:
services/: Connection services, entity-specific servicesutils/: Query builders (SOQL, etc.)
Domain mappers live in packages/domain/{module}/providers/{provider}/:
- Integration services fetch data and call domain mappers
- No business logic in integration layer
- No double transformation
Key Patterns
Validation (Zod-First)
- Schemas live in domain:
packages/domain/<module>/schema.ts - Derive types from schemas:
export type X = z.infer<typeof xSchema> - Query params: use
z.coerce.*for URL strings
BFF Controllers
- Controllers are thin: no business logic, no Zod imports
- Use
createZodDto(schema)with globalZodValidationPipe - Integrations: build queries in utils, fetch data, transform via domain mappers
Logging
- BFF: Use
nestjs-pinoLogger, inject via constructor - Portal: Use
@/core/logger - No
console.login production code (ESLint enforced)
Naming
- No
anyin public APIs - No
console.log(use logger) - Avoid
V2suffix in service names
Documentation
Read before coding:
docs/README.md(entrypoint)docs/development/(BFF/Portal/Domain patterns)docs/architecture/(boundaries)docs/integrations/(external API details)
Rule: Never guess endpoint behavior or payload shapes. Find docs or existing implementation first.