20 KiB
20 KiB
System Architecture
Customer Portal - Comprehensive System Design
Table of Contents
- System Overview
- Architecture Principles
- Monorepo Structure
- Application Layers
- Domain Layer
- Technology Stack
- Data Flow & Integration
- Deployment Architecture
System Overview
The Customer Portal is a modern full-stack application built with clean architecture principles. It provides customers with a self-service portal to manage their telecommunications services, billing, and support cases.
High-Level Architecture
┌─────────────────────────────────────────────────────────────┐
│ Customer Portal │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ │ │ │ │
│ │ Portal │──────────────│ BFF │ │
│ │ (Next.js) │ REST/ │ (NestJS) │ │
│ │ │ GraphQL │ │ │
│ └──────────────┘ └──────┬───────┘ │
│ │ │
│ ┌────────────────────────────────────┴─────────────────┐ │
│ │ Domain Package │ │
│ │ (Shared Types, Schemas, Validators) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
┌───────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ │ │ │ │ │
│ Salesforce │ │ WHMCS │ │ Freebit │
│ CRM │ │ Billing │ │ SIM Mgmt │
│ │ │ │ │ │
└──────────────┘ └─────────────┘ └─────────────┘
Key Components
- Portal (Frontend): Next.js 14+ with App Router, React 18, TypeScript
- BFF (Backend for Frontend): NestJS with clean architecture layers
- Domain Package: Framework-agnostic shared types and business logic
- External Systems: Salesforce (CRM), WHMCS (Billing), Freebit (SIM management)
Architecture Principles
1. Clean Architecture
The system follows clean architecture principles with clear separation of concerns:
- Presentation Layer: UI components, pages, controllers
- Application Layer: Use cases, orchestration, business workflows
- Domain Layer: Business entities, rules, and logic
- Infrastructure Layer: External integrations, databases, APIs
2. Domain-Driven Design
- Business domains organized as independent modules
- Ubiquitous language shared across technical and business teams
- Bounded contexts with clear interfaces
- Domain events for cross-domain communication
3. Single Responsibility
- Each module, service, and component has one clear purpose
- Clear boundaries between concerns
- No circular dependencies
- Minimal coupling between layers
4. Type Safety
- Strong TypeScript typing throughout the stack
- Runtime validation with Zod schemas
- Single source of truth for types
- Contract-first API design
5. Security First
- No sensitive data exposure memory:6689308
- Production-ready error handling
- PII redaction in logs
- Row-level security for data access
Monorepo Structure
customer-portal/
├── apps/
│ ├── portal/ # Next.js frontend application
│ └── bff/ # NestJS backend-for-frontend
├── packages/
│ ├── domain/ # Shared domain types and business logic
│ ├── logging/ # Centralized logging utilities
│ └── validation/ # Shared validation schemas
├── docker/ # Docker configurations
├── scripts/ # Build and deployment scripts
└── docs/ # Documentation
Workspace Organization
The monorepo uses pnpm workspaces for efficient package management:
- Isolated dependencies: Each app manages its own dependencies
- Shared packages: Common code reused across apps
- Atomic changes: Related changes across multiple packages in single commits
- Type safety: TypeScript project references for fast builds
Application Layers
Portal Architecture (Next.js)
apps/portal/src/
├── app/ # Next.js App Router
│ ├── (public)/ # Public marketing and auth routes
│ ├── (authenticated)/ # Authenticated portal routes
│ └── api/ # API routes (if needed)
├── components/ # Shared UI components
│ ├── ui/ # Basic UI elements (atoms)
│ ├── molecules/ # Component combinations
│ ├── organisms/ # Complex UI sections
│ └── templates/ # Page layouts
├── features/ # Feature modules
│ ├── auth/ # Authentication
│ ├── billing/ # Invoices and payments
│ ├── catalog/ # Product catalog
│ ├── orders/ # Order management
│ ├── subscriptions/ # Service management
│ ├── support/ # Support cases
│ └── dashboard/ # Customer dashboard
├── lib/ # Core utilities
│ ├── api/ # API client with Zod validation
│ ├── hooks/ # Shared React hooks
│ └── utils/ # Utility functions
├── providers/ # React context providers
└── styles/ # Global styles and design tokens
Key Principles:
- Feature-First Organization: Related code grouped by business feature
- Atomic Design: UI components organized hierarchically
- Server-First: Leverage Next.js server components where possible
- Type Safety: Full TypeScript coverage with strict mode
- Performance: Code splitting, lazy loading, optimized bundles
BFF Architecture (NestJS)
apps/bff/src/
├── modules/ # Feature modules
│ ├── auth/ # Authentication
│ ├── billing/ # Invoice and payment management
│ ├── catalog/ # Product catalog
│ ├── orders/ # Order processing
│ ├── subscriptions/ # Subscription management
│ └── support/ # Support cases
├── integrations/ # External service integrations
│ ├── salesforce/ # Salesforce CRM
│ │ ├── services/ # API services
│ │ ├── utils/ # Query builders (SOQL)
│ │ └── events/ # Platform events (Pub/Sub)
│ ├── whmcs/ # WHMCS billing
│ │ ├── services/ # API services
│ │ └── utils/ # Request builders
│ └── freebit/ # Freebit SIM management
│ └── services/ # API services
├── core/ # Core services
│ ├── config/ # Configuration management
│ ├── logging/ # Logging setup
│ └── database/ # Database connections
├── common/ # Shared resources
│ ├── guards/ # Auth guards
│ ├── interceptors/ # Request/response interceptors
│ ├── filters/ # Exception filters
│ └── pipes/ # Validation pipes
└── main.ts # Application entry point
Key Principles:
- Modular Architecture: Features organized as NestJS modules
- Integration Layer: External services abstracted behind clean interfaces
- Single Transformation: Domain mappers transform data once
- Infrastructure Concerns: Separated from business logic
- Event-Driven: Platform Events for Salesforce integration
Domain Layer
The domain package provides framework-agnostic types and business logic shared across applications.
Domain Structure
packages/domain/
├── billing/
│ ├── contract.ts # Normalized types (provider-agnostic)
│ ├── schema.ts # Zod validation schemas
│ └── providers/ # Provider-specific adapters
│ └── whmcs/
│ ├── raw.types.ts # WHMCS API response types
│ └── mapper.ts # Transform WHMCS → Domain
├── subscriptions/
│ ├── contract.ts
│ ├── schema.ts
│ └── providers/
│ └── whmcs/
├── orders/
│ ├── contract.ts
│ ├── schema.ts
│ └── providers/
│ ├── salesforce/ # Read orders from Salesforce
│ └── whmcs/ # Create orders in WHMCS
├── catalog/
│ ├── contract.ts
│ ├── schema.ts
│ └── providers/
│ └── salesforce/ # Product catalog
├── customer/
│ ├── contract.ts
│ ├── schema.ts
│ └── providers/
│ ├── salesforce/
│ └── whmcs/
├── sim/
│ ├── contract.ts
│ ├── schema.ts
│ └── providers/
│ └── freebit/ # SIM management
└── common/
├── types.ts # Common types (Address, Money, etc.)
├── api.ts # API response wrappers
└── schema.ts # Common schemas
Provider Pattern
The domain layer uses a provider pattern to abstract external system details:
// Domain contract (provider-agnostic)
export interface Invoice {
id: number;
status: InvoiceStatus;
amount: Money;
dueDate: Date;
// ... normalized fields
}
// Provider-specific raw type
export interface WhmcsInvoiceRaw {
invoiceid: string;
status: string;
total: string;
// ... WHMCS-specific fields
}
// Provider mapper
export function transformWhmcsInvoice(raw: unknown): Invoice {
const validated = whmcsInvoiceRawSchema.parse(raw);
const result: Invoice = {
id: parseInt(validated.invoiceid),
status: mapWhmcsStatus(validated.status),
amount: {
value: parseFloat(validated.total),
currency: validated.currencycode
},
// ... map all fields
};
return invoiceSchema.parse(result); // Validate domain model
}
Benefits:
- Provider Isolation: External API details don't leak into application code
- Single Transformation: Map once using domain mappers
- Type Safety: Runtime validation at boundaries
- Testability: Easy to mock and test transformations
- Scalability: Add new providers without changing application code
Technology Stack
Frontend
- Framework: Next.js 14+ (App Router)
- Language: TypeScript 5+
- UI Library: React 18
- Styling: Tailwind CSS
- State Management: React Query (TanStack Query)
- Form Handling: React Hook Form
- Validation: Zod
- HTTP Client: Fetch API with Zod validation
Backend
- Framework: NestJS 10+
- Language: TypeScript 5+
- Runtime: Node.js 20+
- Validation: Zod + class-validator
- ORM: Prisma
- Caching: Redis
- Queue: BullMQ (Redis-backed)
- Logging: Pino
- Testing: Jest
Infrastructure
- Database: PostgreSQL 15+
- Cache: Redis 7+
- Container: Docker
- Orchestration: Docker Compose
- Reverse Proxy: Nginx
- CI/CD: GitHub Actions (assumed)
External Services
- Salesforce: CRM and order management
- REST API
- Platform Events (Pub/Sub gRPC)
- SOQL queries
- WHMCS: Billing and provisioning
- REST API
- Webhooks for events
- Freebit: SIM card management
- REST API
Data Flow & Integration
Order Processing Flow
1. Customer creates order in Portal
│
├─> Portal validates selections
│
└─> POST /api/orders → BFF
2. BFF creates Salesforce Order
│
├─> Validates customer data
├─> Creates Order record
├─> Creates OrderItems
└─> Status: Draft
3. Customer/Admin approves Order in Salesforce
│
└─> Record-Triggered Flow publishes OrderProvisionRequested__e
4. BFF receives Platform Event
│
├─> PlatformEventsSubscriber enqueues job
└─> BullMQ provisioning queue
5. Provisioning Worker processes job
│
├─> OrderFulfillmentService orchestrates
├─> Creates WHMCS order (AddOrder)
├─> Accepts WHMCS order (AcceptOrder → creates services)
└─> Updates Salesforce Order status
6. Customer sees updated status in Portal
│
└─> GET /api/orders/{id} → Shows provisioned services
Catalog Data Flow
┌──────────────┐
│ Salesforce │
│ Product2 │
└──────┬───────┘
│
│ SOQL Query
│
┌──────▼──────────────────────────┐
│ BFF Catalog Service │
│ • Query products │
│ • Filter by Portal_Catalog__c │
│ • Transform with domain mapper │
└──────┬──────────────────────────┘
│
│ Domain Types
│
┌──────▼───────────────┐
│ Cache (Redis) │
│ TTL: 5-15 min │
└──────┬───────────────┘
│
│ REST API
│
┌──────▼───────┐
│ Portal │
│ Catalog │
└──────────────┘
Authentication Flow
1. User submits login credentials
│
├─> Portal: POST /auth/login
│
2. BFF validates credentials
│
├─> Check against database
├─> Verify WHMCS account link (optional)
│
3. Issue tokens
│
├─> Access Token (JWT, 15 min)
├─> Refresh Token (stored in Redis, 7 days)
│
4. Return tokens to client
│
├─> Access Token in response body
├─> Refresh Token in httpOnly cookie
│
5. Client makes authenticated requests
│
├─> Include Access Token in Authorization header
│
6. Token refresh
│
├─> Portal: POST /auth/refresh
├─> BFF validates Refresh Token
├─> Issues new Access Token
└─> Rotates Refresh Token (token family)
Deployment Architecture
Production Environment
┌─────────────────────────────────────────────────────────┐
│ Load Balancer │
└────────────────────┬────────────────────────────────────┘
│
┌────────────┴────────────┐
│ │
┌───────▼────────┐ ┌─────────▼────────┐
│ Portal │ │ BFF │
│ (Next.js) │ │ (NestJS) │
│ • SSR/SSG │ │ • REST API │
│ • Static │ │ • WebSockets │
└───────┬────────┘ └─────────┬────────┘
│ │
│ ┌──────────┴──────────┐
│ │ │
│ ┌───────▼────────┐ ┌───────▼────────┐
│ │ PostgreSQL │ │ Redis │
│ │ (Primary DB) │ │ (Cache+Queue) │
│ └────────────────┘ └────────────────┘
│
┌───────┴──────────────────────────────────────┐
│ External Services │
│ • Salesforce (CRM) │
│ • WHMCS (Billing) │
│ • Freebit (SIM Management) │
└──────────────────────────────────────────────┘
Scalability Considerations
- Horizontal Scaling: Multiple BFF instances behind load balancer
- Stateless Design: JWT tokens, no server-side sessions
- Redis Clustering: Distributed cache and queue
- Database Read Replicas: Separate read/write connections
- CDN: Static assets served via CDN
- Queue Workers: Separate processes for background jobs
Monitoring & Observability
- Structured Logging: Pino with correlation IDs
- Metrics: Response times, error rates, API latency
- Health Checks:
/healthendpoints for both apps - Error Tracking: Centralized error logging
- Performance Monitoring: Real-time performance metrics
Design Decisions
Why Monorepo?
- Shared Code: Easy to share types, utilities, and business logic
- Atomic Changes: Update frontend and backend in sync
- Consistent Tooling: Single ESLint, TypeScript, and Prettier config
- Developer Experience: Single repository, single checkout
Why BFF Pattern?
- Frontend-Specific: API tailored to portal needs
- Aggregation: Combine multiple backend services
- Security: Backend handles sensitive operations
- Performance: Caching and optimization close to frontend
Why Domain Package?
- Single Source of Truth: Types defined once, used everywhere
- Provider Abstraction: External systems isolated from application
- Type Safety: Runtime validation with Zod
- Testability: Business logic separate from infrastructure
Why Platform Events (Not Webhooks)?
- Security: No inbound connections from Salesforce
- Reliability: Durable replay with replayId tracking
- Scalability: High-volume event processing
- Real-time: Near-instant event delivery
Future Considerations
Planned Enhancements
- GraphQL API: Consider GraphQL for complex data fetching
- Event Sourcing: Event log for audit trail
- CQRS: Separate read/write models for complex domains
- Microservices: Split BFF into domain-specific services
- Real-time Updates: WebSocket for live order status
Technical Debt
- Legacy Migration: Some validation logic being migrated to domain
- Test Coverage: Increase unit and integration test coverage
- Performance: Optimize database queries and caching strategy
- Documentation: Continue documenting API contracts and flows
Last Updated: October 2025
Status: Active - Production System