Add structured error code enums to domain package for WHMCS, Salesforce, and Freebit providers. Create BaseProviderError and typed error classes for each provider. Update UnifiedExceptionFilter to handle provider errors. Migrate all three error handler services from DomainHttpException with brittle string matching to typed error classes with instanceof checks.
Customer Portal Project
A modern customer portal where users can self-register, log in, browse & buy subscriptions, view/pay invoices, and manage support cases.
Architecture Overview
Systems of Record
- WHMCS: Billing, subscriptions, invoices, and authoritative address storage
- Salesforce: CRM (Accounts, Contacts, Cases) and order address snapshots
- Portal: Modern UI with backend for frontend (BFF) architecture
Identity Management
- Portal-native authentication (email + password, optional MFA)
- One-time WHMCS user verification with forced password reset
- User mapping:
user_id ↔ whmcs_client_id ↔ sf_contact_id/sf_account_id
Tech Stack
Frontend (Portal UI)
- Next.js 15 with App Router
- Turbopack for ultra-fast development and builds
- React 19 with TypeScript
- Tailwind CSS with shadcn/ui components
- TanStack Query for data fetching and caching
- Zod for validation
- React Hook Form for form management
Backend (BFF API)
- NestJS 11 (Node 24 Current or 22 LTS)
- Prisma 6 ORM with PostgreSQL 17
- jsforce for Salesforce REST API integration
- salesforce-pubsub-api-client for Salesforce Platform Events
- p-queue for request throttling and queue management
- WHMCS custom API client with comprehensive service layer
- Freebit SIM management integration
- Zod-first validation shared via the domain package
- Bcrypt for password hashing
Queue Management
- p-queue for intelligent request throttling to external APIs
- Separate queues for Salesforce (standard + long-running) and WHMCS
- Configurable concurrency, rate limiting, and timeout handling
- Prevents API rate limit violations and resource exhaustion
Logging
- Centralized structured logging via Pino using
nestjs-pinoin the BFF - Sensitive fields are redacted; each request has a correlation ID
- Usage pattern in services:
- Inject
Loggerfromnestjs-pino:constructor(@Inject(Logger) private readonly logger: Logger) {} - Log with structured objects:
this.logger.error('Message', { error }) - See
docs/LOGGING.mdfor full guidelines
- Inject
Data & Infrastructure
- PostgreSQL 17 for users, ID mappings, and optional mirrors
- Redis 7 for cache, token blacklists, and rate limiting
- Docker for local development (Postgres/Redis)
Project Structure
customer-portal/
├── apps/
│ ├── portal/ # Next.js 15 frontend (React 19, Tailwind, shadcn/ui)
│ └── bff/ # NestJS 11 backend (Prisma, p-queue, Zod validation)
├── packages/
│ ├── domain/ # Unified domain layer with contracts & schemas
│ ├── validation/ # Unified validation service (NestJS + React)
│ ├── logging/ # Centralized logging utilities (Pino)
│ └── integrations/ # External service integrations
│ ├── whmcs/ # WHMCS API client
│ └── freebit/ # Freebit SIM management
├── scripts/
│ ├── dev/ # Development management scripts
│ └── prod/ # Production deployment scripts
├── docker/
│ └── dev/ # Docker Compose for local development
│ └── docker-compose.yml # PostgreSQL 17 + Redis 7
├── docs/ # Comprehensive documentation
├── secrets/ # Private keys (git ignored)
├── env/ # Environment file templates
├── package.json # Root workspace configuration
├── pnpm-workspace.yaml # pnpm workspace definition
└── README.md # This file
Getting Started
Prerequisites
- Node.js: Version 22 (LTS) or 24 (Current) - specified in
package.jsonengines - pnpm: Version 10.0.0+ (managed via
packageManagerfield) - Docker & Docker Compose: For local PostgreSQL and Redis services
- Git: For version control
Quick Start (2 minutes)
-
Clone and Install Dependencies
git clone <repository-url> cd customer-portal pnpm install -
Setup Environment
# Copy development environment template (if available) # Note: .env files may be filtered by .cursorignore # Contact your team for environment configuration # Edit with your values (most defaults work for local development) # Required: DATABASE_URL, REDIS_URL, JWT_SECRET # Optional for basic dev: WHMCS, Salesforce, Freebit credentials -
Start Development Environment
# Start database and Redis services pnpm dev:start # In another terminal, start the applications with hot reload pnpm dev -
Access Your Applications
- Frontend: http://localhost:3000
- Backend API: http://localhost:4000/api
Development Commands
# === Daily Development ===
pnpm dev:start # Start PostgreSQL + Redis services
pnpm dev # Start both apps with hot reload
pnpm dev:stop # Stop all services
# === Database Management ===
pnpm dev:migrate # Run database migrations
pnpm db:studio # Open Prisma Studio (database GUI)
pnpm dev:tools # Start admin tools (Adminer + Redis Commander)
# === Utilities ===
pnpm dev:status # Check service status
pnpm dev:logs # View service logs
pnpm dev:reset # Reset development environment
pnpm lint # Run linting across all packages
pnpm type-check # Run TypeScript checks
Build and Export Images (for Plesk upload)
# Frontend
docker build -t customer-portal-frontend:latest -f apps/portal/Dockerfile .
docker save -o customer-portal-frontend.latest.tar customer-portal-frontend:latest
# Backend
docker build -t customer-portal-backend:latest -f apps/bff/Dockerfile .
docker save -o customer-portal-backend.latest.tar customer-portal-backend:latest
Upload the tar files in Plesk → Docker → Images → Upload, then deploy using the appropriate compose stack configuration.
API Client
The portal uses TanStack Query with a lightweight fetch client that shares request/response contracts from
@customer-portal/domain and validates them with Zod:
import { apiClient } from "@/lib/api-client";
import type { DashboardSummary } from "@customer-portal/domain/dashboard";
const { data: summary } = useQuery({
queryKey: ["dashboard", "summary"],
queryFn: () => apiClient.get<DashboardSummary>("/api/me/summary"),
});
Because the schemas and types live in the shared domain package there is no separate code generation step.
Environment Configuration
- Local development: configure environment variables (contact team for template)
- Docker services use defaults: PostgreSQL (dev/dev/portal_dev), Redis (no auth)
- Plesk production: use split env files (no secrets under
httpdocs)- Frontend: ensure
NEXT_PUBLIC_API_BASE=/api - Backend: ensure
TRUST_PROXY=true, DB usesdatabase:5432, Redis usescache:6379 - See deployment documentation for full instructions
- Frontend: ensure
Key Environment Variables
Required environment variables (contact your team for specific values):
# === Application ===
NODE_ENV=development
BFF_PORT=4000
NEXT_PORT=3000
# === Database & Cache ===
DATABASE_URL=postgresql://dev:dev@localhost:5432/portal_dev?schema=public
REDIS_URL=redis://localhost:6379
# === Frontend (exposed to browser) ===
NEXT_PUBLIC_API_BASE=http://localhost:4000
NEXT_PUBLIC_APP_NAME=Customer Portal (Dev)
NEXT_PUBLIC_ENABLE_DEVTOOLS=true
# === Security ===
JWT_SECRET=<secure_secret_minimum_32_chars>
JWT_REFRESH_SECRET=<different_secure_secret_minimum_32_chars>
BCRYPT_ROUNDS=12
# === External APIs (required for full functionality) ===
WHMCS_BASE_URL=<your_whmcs_url>
WHMCS_API_IDENTIFIER=<your_identifier>
WHMCS_API_SECRET=<your_secret>
SF_LOGIN_URL=<salesforce_instance_url>
SF_CLIENT_ID=<oauth_client_id>
SF_PRIVATE_KEY_PATH=./secrets/sf-private.key
SF_USERNAME=<salesforce_username>
FREEBIT_API_BASE_URL=<freebit_api_url>
FREEBIT_CLIENT_ID=<freebit_client_id>
FREEBIT_CLIENT_SECRET=<freebit_secret>
Salesforce Pub/Sub (Events)
# Enable Pub/Sub subscription for order provisioning
SF_EVENTS_ENABLED=true
SF_PROVISION_EVENT_CHANNEL=/event/Order_Fulfilment_Requested__e
SF_EVENTS_REPLAY=LATEST # or ALL for retention replay
SF_PUBSUB_ENDPOINT=api.pubsub.salesforce.com:7443
SF_PUBSUB_NUM_REQUESTED=25 # flow control window
- Verify subscriber status:
GET /api/health/sf-eventsenabled: whether Pub/Sub is enabledchannel: topic namereplay.lastReplayId: last committed cursorsubscriber.status: connected | disconnected | unknown
Read more about the provisioning flow in docs/provisioning/RUNBOOK_PROVISIONING.md.
Development Tools Access
When running pnpm dev:tools, you get access to:
- Adminer (Database GUI): http://localhost:8080
- Server:
postgres, User:dev, Password:dev, Database:portal_dev
- Server:
- Redis Commander: http://localhost:8081
- User:
admin, Password:dev
- User:
Data Model
Core Tables (PostgreSQL)
users- Portal user accounts with auth credentialsid_mappings- Cross-system user ID mappingsinvoices_mirror- Optional WHMCS invoice cachesubscriptions_mirror- Optional WHMCS service cacheidempotency_keys- Prevent duplicate operations
API Surface (BFF)
Authentication
POST /api/auth/signup- Create portal user → WHMCS AddClient → SF upsertPOST /api/auth/login- Portal authenticationPOST /api/auth/migrate- Account migration from legacy portalPOST /api/auth/set-password- Required after WHMCS link
User Management
GET /api/me- Current user profileGET /api/me/summary- Dashboard summaryPATCH /api/me- Update profilePATCH /api/me/address- Update address fields
Catalog & Orders
GET /api/services/*- Services catalog endpoints (internet/sim/vpn)POST /api/orders- WHMCS AddOrder with idempotency
Invoices
GET /api/invoices- Paginated invoice list (cached 60-120s)GET /api/invoices/:id- Invoice detailsPOST /api/invoices/:id/sso-link- WHMCS CreateSsoToken
Subscriptions
GET /api/subscriptions- WHMCS GetClientsProducts
Support Cases (Salesforce)
GET /api/cases- Cases list (cached 30-60s)GET /api/cases/:id- Case details with commentsPOST /api/cases- Create new casePOST /api/cases/:id/comments- Add comment to case
Webhooks & Events
POST /api/webhooks/whmcs- WHMCS action hooks → update mirrors + bust cache- Salesforce Platform Events - Real-time order provisioning via gRPC Pub/Sub
Frontend Pages
Public Pages
/- Landing page for non-authenticated users/auth/login- Sign in/auth/signup- Create account/auth/set-password- Set password after WHMCS link
Authenticated Pages
/dashboard- Dashboard (invoices, active subs, orders)/catalog- Product catalog home/catalog/internet- Internet plans/catalog/vpn- VPN products/checkout- Checkout flow/orders- Order list/orders/[id]- Order details/subscriptions- Active subscriptions/subscriptions/[id]- Subscription details/billing/invoices- Invoice list/billing/invoices/[id]- Invoice details/billing/payments- Payment methods/support/cases- Support cases list/support/cases/[id]- Case details/support/new- Create new case/account- User account management
Development Milestones
Milestone 1: Identity & Linking
- Portal login/signup with JWT authentication
- One-time WHMCS verification with SSO
- Set new portal password (Bcrypt)
- Store id_mappings (user ↔ WHMCS ↔ Salesforce)
- Refresh token rotation
- Account lockout after failed attempts
- Rate limiting on auth endpoints
Milestone 2: Catalog & Orders
- Product catalog (GetProducts from WHMCS)
- Catalog caching with CDC invalidation
- Internet plan catalog with address verification
- VPN product catalog
- Checkout flow with cart management
- Order creation (WHMCS AddOrder)
- Order list and details from Salesforce
- Real-time order provisioning via Salesforce Platform Events
Milestone 3: Billing & Invoices
- Invoice list/detail (GetInvoices from WHMCS)
- Invoice caching with TTL
- WHMCS SSO deep links for payment
- Payment method management
- Payment gateway listing
- Subscription list and details
- WHMCS webhooks → cache bust + mirror updates
Milestone 4: Support & Cases
- Salesforce case list (cached)
- Case details with comments
- Create new support case
- Add comments to existing cases
- Case file attachments
- Email notifications for case updates
Milestone 5: SIM Management & Provisioning
- Freebit SIM management integration
- Order provisioning workflow
- Real-time event processing via Salesforce Platform Events
- Comprehensive error handling and retry logic
- Customer-facing SIM management UI
Security Features
- HTTPS only with HttpOnly/SameSite cookies
- JWT access + refresh tokens with Redis-backed blacklist
- Bcrypt password hashing (configurable rounds)
- Account lockout after failed login attempts
- Rate limiting on auth endpoints and external API calls
- Idempotency keys on all mutating operations
- Row-level security (user must own resources)
- PII minimization with encryption at rest/in transit
- Audit logging for security-critical actions
- No WHMCS/SF credentials exposed to browser
Caching Strategy
- Invoices: TTL-based (90s); bust on WHMCS webhook
- Catalog: CDC-driven (no TTL); manual bust on data changes
- Orders: CDC-driven (no TTL); real-time invalidation via Salesforce Platform Events
- WHMCS API responses: TTL-based caching (configurable per endpoint)
- Redis-backed with request coalescing to prevent thundering herd
- Keys include user_id to prevent cross-user leakage
- Metrics tracking for cache hits, misses, and invalidations
Troubleshooting
Common Issues
Port Already in Use
# Check what's using the port
lsof -i :3000 # or :4000, :5432, :6379
# Kill the process or change ports in .env
Database Connection Issues
# Check if PostgreSQL is running
pnpm dev:status
# Restart services
pnpm dev:restart
# Reset everything
pnpm dev:reset
Environment Variables Not Loading
- Ensure environment variables are configured (contact team for configuration)
- Restart applications after changing environment variables
- Check for typos in variable names
- Frontend variables must start with
NEXT_PUBLIC_
Docker Issues
# Clean up Docker resources
docker system prune -f
# Rebuild containers
pnpm dev:stop && pnpm dev:start
pnpm Issues
# Clear pnpm cache
pnpm store prune
# Reinstall dependencies
rm -rf node_modules && pnpm install
Getting Help
- Check the logs:
pnpm dev:logs - Verify service status:
pnpm dev:status - Review environment configuration in
.env - Check the documentation in
docs/folder - Look for similar issues in the project's issue tracker
Documentation
- Getting Started - Detailed setup guide
- Development Guide - Quick reference for daily development
- Deployment Guide - Production deployment instructions
- Architecture - Code organization and conventions
- Logging - Logging configuration and best practices
- Portal Guides - High-level flow, data ownership, and error handling (
docs/how-it-works/README.md)
Contributing
-
Setup Development Environment
# Configure environment variables (contact team) pnpm install pnpm dev:start -
Follow Code Standards
- Run
pnpm lintbefore committing - Use
pnpm formatto format code - Ensure
pnpm type-checkpasses - Write tests for new features
- Run
-
Development Workflow
- Create feature branches from
main - Make small, focused commits
- Update documentation as needed
- Test thoroughly before submitting PRs
- Create feature branches from
-
Code Quality
- Follow TypeScript strict mode
- Use proper error handling (no sensitive info exposed)
- Implement clean, minimal UI designs
- Avoid 'V2' suffixes in service names
- Verify API integration against official documentation
Codebase Coding Standard
- Have types and validation in the shared domain layer.
- Keep business logic out of the frontend; use services and APIs instead.
- Reuse existing types and functions; extend them when additional behavior is needed.
- Follow the established folder structures documented in
docs/STRUCTURE.md.
Documentation
📚 Complete Documentation - Full documentation index
Quick Links
- Getting Started - Setup and configuration
- Development Commands - Daily workflow
- Address System - Address management
- Product Catalog - SKU-based catalog
- Deployment Guide - Production deployment
Key Features
- ✅ Required address at signup - No incomplete profiles
- ✅ Order-type specific flows - Internet orders require verification
- ✅ Real-time WHMCS sync - Address updates
- ✅ Salesforce snapshots - Point-in-time order addresses
- ✅ Clean architecture - Modular, maintainable code
License
[Your License Here]
See docs/RUNBOOK_PROVISIONING.md for the provisioning runbook.