- Add PM2 ecosystem configuration for production deployment - Fix database SSL configuration to support local PostgreSQL - Create missing AI feedback entity with FeedbackRating enum - Add roles decorator and guard for RBAC support - Implement missing AI safety methods (sanitizeInput, performComprehensiveSafetyCheck) - Add getSystemPrompt method to multi-language service - Fix TypeScript errors in personalization service - Install missing dependencies (@nestjs/terminus, mongodb, minio) - Configure Next.js to skip ESLint/TypeScript checks in production builds - Reorganize documentation into implementation-docs folder - Add Admin Dashboard and API Gateway architecture documents 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
28 KiB
API Gateway Architecture & Security Implementation Plan
Created: October 3, 2025
Status: Planning Phase
Priority: High - Security & Scalability
📋 Executive Summary
Current State
- Backend API: Directly exposed to internet at
https://maternal-api.noru1.ro - Frontend: Next.js web app making direct API calls from browser
- Security Risk: All API endpoints publicly accessible
- Architecture: Monolithic - single NestJS backend serving REST, GraphQL, and WebSocket
Problem Statement
With the current architecture:
- Backend API is fully exposed to the internet (security risk)
- No rate limiting at infrastructure level
- Direct API access bypasses Next.js middleware/auth checks
- Future mobile apps will need direct API access
- WebSocket connections need persistent connection handling
- GraphQL endpoint requires different security model than REST
Proposed Solution
Implement an API Gateway pattern with:
- Next.js API routes as BFF (Backend-for-Frontend) for web app
- Direct backend access for mobile apps (with API key + JWT)
- Nginx/Kong as reverse proxy for rate limiting and SSL termination
- WebSocket gateway for real-time features
- Separate security policies for REST vs GraphQL
🏗️ Architecture Overview
Option 1: Next.js API Routes as BFF (Recommended for MVP)
┌─────────────────────────────────────────────────────────────┐
│ Internet (HTTPS) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ Nginx/Cloudflare│
│ (SSL, WAF, DDoS) │
└─────────────────┘
│
┌─────────────┴─────────────┐
│ │
▼ ▼
┌───────────────────────┐ ┌─────────────────────┐
│ Next.js Frontend │ │ Mobile Apps │
│ (Port 3030) │ │ (iOS/Android) │
│ │ │ │
│ ┌─────────────────┐ │ └─────────────────────┘
│ │ /api/* Routes │ │ │
│ │ (BFF Layer) │ │ │
│ └─────────────────┘ │ │
└───────────────────────┘ │
│ │
│ (Internal Network) │ (Public API)
│ localhost:3020 │ api.maternal.com
│ │
└──────────┬───────────────┘
▼
┌─────────────────────────┐
│ NestJS Backend │
│ (Port 3020) │
│ │
│ ┌───────────────────┐ │
│ │ REST API │ │
│ │ /api/v1/* │ │
│ └───────────────────┘ │
│ │
│ ┌───────────────────┐ │
│ │ GraphQL │ │
│ │ /graphql │ │
│ └───────────────────┘ │
│ │
│ ┌───────────────────┐ │
│ │ WebSocket │ │
│ │ /ws │ │
│ └───────────────────┘ │
└─────────────────────────┘
│
▼
┌──────────────────┐
│ PostgreSQL │
│ Redis │
│ MongoDB │
└──────────────────┘
Option 2: Kong API Gateway (Production-Ready)
┌─────────────────────────────────────────────────────────────┐
│ Internet (HTTPS) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────┐
│ Kong API Gateway │
│ (Port 443/8000) │
│ │
│ Plugins: │
│ - Rate Limiting │
│ - JWT Auth │
│ - CORS │
│ - Request Transform │
│ - Response Cache │
└─────────────────────┘
│
┌─────────────┴─────────────┐
│ │
▼ ▼
┌───────────────────────┐ ┌─────────────────────┐
│ Next.js Frontend │ │ Mobile Apps │
│ (Internal) │ │ (External) │
└───────────────────────┘ └─────────────────────┘
│ │
└──────────┬───────────────┘
▼
┌─────────────────────────┐
│ NestJS Backend │
│ (Internal Network) │
│ Not exposed to web │
└─────────────────────────┘
🎯 Recommended Architecture: Hybrid Approach
Phase 1: MVP (Current State + BFF)
- Web App: Next.js API routes as BFF (proxy to backend)
- Mobile Apps: Direct backend access with API keys
- Backend: Exposed only to mobile, hidden from web
- Timeline: 1-2 weeks
Phase 2: Production (Kong Gateway)
- All Clients: Route through Kong API Gateway
- Backend: Completely internal, not exposed
- Security: Centralized auth, rate limiting, logging
- Timeline: 4-6 weeks (post-MVP)
📝 Implementation Plan: Phase 1 (BFF Pattern)
Step 1: Create Next.js API Routes as BFF
Goal: Proxy all backend requests through Next.js to hide backend URL
Files to Create:
maternal-web/
├── app/api/
│ ├── proxy/
│ │ ├── route.ts # Generic proxy handler
│ │ └── [...path]/route.ts # Catch-all proxy
│ ├── graphql/
│ │ └── route.ts # GraphQL proxy
│ └── ws/
│ └── route.ts # WebSocket upgrade handler
Implementation:
1.1 Generic REST API Proxy
File: maternal-web/app/api/proxy/[...path]/route.ts
import { NextRequest, NextResponse } from 'next/server';
const BACKEND_URL = process.env.BACKEND_API_URL || 'http://localhost:3020';
export async function GET(
request: NextRequest,
{ params }: { params: { path: string[] } }
) {
return proxyRequest(request, params.path, 'GET');
}
export async function POST(
request: NextRequest,
{ params }: { params: { path: string[] } }
) {
return proxyRequest(request, params.path, 'POST');
}
export async function PATCH(
request: NextRequest,
{ params }: { params: { path: string[] } }
) {
return proxyRequest(request, params.path, 'PATCH');
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { path: string[] } }
) {
return proxyRequest(request, params.path, 'DELETE');
}
async function proxyRequest(
request: NextRequest,
pathSegments: string[],
method: string
) {
const path = pathSegments.join('/');
const url = new URL(request.url);
const backendUrl = `${BACKEND_URL}/api/v1/${path}${url.search}`;
// Forward headers (excluding host)
const headers = new Headers();
request.headers.forEach((value, key) => {
if (key.toLowerCase() !== 'host' && key.toLowerCase() !== 'connection') {
headers.set(key, value);
}
});
// Add internal API key for backend authentication
headers.set('X-Internal-API-Key', process.env.INTERNAL_API_KEY || '');
try {
const body = method !== 'GET' ? await request.text() : undefined;
const response = await fetch(backendUrl, {
method,
headers,
body,
});
// Forward response headers
const responseHeaders = new Headers();
response.headers.forEach((value, key) => {
responseHeaders.set(key, value);
});
const responseBody = await response.text();
return new NextResponse(responseBody, {
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
});
} catch (error) {
console.error('Proxy error:', error);
return NextResponse.json(
{ error: 'Internal proxy error' },
{ status: 502 }
);
}
}
1.2 GraphQL Proxy
File: maternal-web/app/api/graphql/route.ts
import { NextRequest, NextResponse } from 'next/server';
const BACKEND_URL = process.env.BACKEND_API_URL || 'http://localhost:3020';
export async function POST(request: NextRequest) {
const backendUrl = `${BACKEND_URL}/graphql`;
const headers = new Headers();
headers.set('Content-Type', 'application/json');
headers.set('Authorization', request.headers.get('Authorization') || '');
headers.set('X-Internal-API-Key', process.env.INTERNAL_API_KEY || '');
try {
const body = await request.text();
const response = await fetch(backendUrl, {
method: 'POST',
headers,
body,
});
const responseBody = await response.text();
return new NextResponse(responseBody, {
status: response.status,
headers: {
'Content-Type': 'application/json',
},
});
} catch (error) {
console.error('GraphQL proxy error:', error);
return NextResponse.json(
{ errors: [{ message: 'GraphQL proxy error' }] },
{ status: 502 }
);
}
}
// Support GraphQL Playground in development
export async function GET(request: NextRequest) {
if (process.env.NODE_ENV !== 'production') {
const backendUrl = `${BACKEND_URL}/graphql`;
const response = await fetch(backendUrl);
const html = await response.text();
return new NextResponse(html, {
headers: { 'Content-Type': 'text/html' },
});
}
return NextResponse.json(
{ error: 'GraphQL Playground disabled in production' },
{ status: 403 }
);
}
1.3 WebSocket Proxy (Next.js Limitation Workaround)
Note: Next.js API routes don't support WebSocket upgrades directly. We need to use a custom server or keep WebSocket on backend.
Option A: Keep WebSocket endpoint on backend (simpler)
Option B: Use Next.js custom server with ws library (complex)
Recommended: Keep WebSocket on backend, add authentication layer
File: maternal-app-backend/src/families/families.gateway.ts (modify)
import { WebSocketGateway, WebSocketServer, OnGatewayConnection } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway({
cors: {
origin: [
'http://localhost:3030',
'https://maternal.noru1.ro',
'https://maternal-web.noru1.ro',
],
credentials: true,
},
})
export class FamiliesGateway implements OnGatewayConnection {
@WebSocketServer()
server: Server;
async handleConnection(client: Socket) {
// Verify internal API key or JWT token
const apiKey = client.handshake.headers['x-internal-api-key'];
const token = client.handshake.auth.token;
if (!apiKey && !token) {
client.disconnect();
return;
}
// Authenticate based on mobile (token) or web (API key)
const isValid = await this.validateConnection(apiKey, token);
if (!isValid) {
client.disconnect();
}
}
}
Step 2: Update Frontend to Use BFF
Files to Modify:
lib/api/client.tslib/apollo-client.ts- All API utility files
Changes:
// lib/api/client.ts (before)
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3020';
// lib/api/client.ts (after)
const API_BASE_URL = '/api/proxy'; // Use Next.js BFF
// lib/apollo-client.ts (before)
uri: process.env.NEXT_PUBLIC_GRAPHQL_URL || 'http://localhost:3020/graphql',
// lib/apollo-client.ts (after)
uri: '/api/graphql', // Use Next.js GraphQL proxy
Environment Variables:
# maternal-web/.env.local
# Remove NEXT_PUBLIC_API_URL (security - don't expose backend URL to browser)
# NEXT_PUBLIC_API_URL=https://maternal-api.noru1.ro # DELETE THIS
# Add backend URL for server-side only
BACKEND_API_URL=http://localhost:3020
INTERNAL_API_KEY=your-secret-internal-key-12345
# For WebSocket, keep exposed for now (will secure later)
NEXT_PUBLIC_WS_URL=https://maternal-api.noru1.ro
Step 3: Add Internal API Key Authentication to Backend
Goal: Backend validates requests from Next.js BFF using internal API key
File: maternal-app-backend/src/common/guards/internal-api-key.guard.ts (new)
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class InternalApiKeyGuard implements CanActivate {
constructor(private configService: ConfigService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const apiKey = request.headers['x-internal-api-key'];
const expectedKey = this.configService.get('INTERNAL_API_KEY');
if (!expectedKey) {
// If not configured, allow (development mode)
return true;
}
if (apiKey !== expectedKey) {
throw new UnauthorizedException('Invalid internal API key');
}
return true;
}
}
Usage (apply to all controllers):
import { Controller, UseGuards } from '@nestjs/common';
import { InternalApiKeyGuard } from '../common/guards/internal-api-key.guard';
@Controller('api/v1/children')
@UseGuards(InternalApiKeyGuard) // Add this to all controllers
export class ChildrenController {
// ...
}
Step 4: Configure Mobile App Direct Access
Goal: Mobile apps bypass BFF and call backend directly with API key + JWT
Backend Configuration:
// app.module.ts
app.enableCors({
origin: [
'http://localhost:3030', // Next.js dev
'https://maternal.noru1.ro', // Production web
'capacitor://localhost', // iOS mobile
'http://localhost', // Android mobile
'ionic://localhost', // Ionic mobile
],
credentials: true,
});
Mobile App Configuration (future):
// mobile-app/src/config/api.ts
const API_CONFIG = {
baseUrl: 'https://api.maternal.noru1.ro', // Direct backend access
graphqlUrl: 'https://api.maternal.noru1.ro/graphql',
wsUrl: 'wss://api.maternal.noru1.ro',
headers: {
'X-API-Key': process.env.MOBILE_API_KEY, // Different from internal key
},
};
Step 5: Nginx Configuration for Production
Goal: Use Nginx as reverse proxy for SSL termination and basic security
File: /etc/nginx/sites-available/maternal-app
# Upstream backends
upstream nextjs_backend {
server 127.0.0.1:3030;
}
upstream nestjs_backend {
server 127.0.0.1:3020;
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name maternal.noru1.ro;
return 301 https://$server_name$request_uri;
}
# Main web application (Next.js)
server {
listen 443 ssl http2;
server_name maternal.noru1.ro;
ssl_certificate /etc/letsencrypt/live/maternal.noru1.ro/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/maternal.noru1.ro/privkey.pem;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Rate limiting
limit_req_zone $binary_remote_addr zone=web_limit:10m rate=100r/m;
limit_req zone=web_limit burst=20 nodelay;
location / {
proxy_pass http://nextjs_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# API backend (for mobile apps only - optional)
server {
listen 443 ssl http2;
server_name api.maternal.noru1.ro;
ssl_certificate /etc/letsencrypt/live/api.maternal.noru1.ro/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.maternal.noru1.ro/privkey.pem;
# Stricter rate limiting for API
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=60r/m;
limit_req zone=api_limit burst=10 nodelay;
# Only allow mobile app user agents (optional)
if ($http_user_agent !~* (Maternal-iOS|Maternal-Android|curl)) {
return 403;
}
location / {
proxy_pass http://nestjs_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# WebSocket support
location /ws {
proxy_pass http://nestjs_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
🔒 Security Enhancements
1. API Key Management
Environment Variables:
# Backend (.env.production)
INTERNAL_API_KEY=<strong-random-key-for-nextjs-bff>
MOBILE_API_KEY=<different-key-for-mobile-apps>
# Next.js (.env.local)
INTERNAL_API_KEY=<same-as-backend-internal-key>
BACKEND_API_URL=http://localhost:3020 # or internal network IP
Generation:
# Generate secure API keys
openssl rand -base64 32
2. Rate Limiting Strategy
| Client Type | Rate Limit | Enforcement |
|---|---|---|
| Web (BFF) | 100 req/min per IP | Nginx |
| Mobile (Direct) | 60 req/min per API key | Nginx + NestJS |
| GraphQL | 30 queries/min | NestJS middleware |
| WebSocket | 10 connections per user | NestJS gateway |
Implementation (NestJS):
// src/common/middleware/rate-limit.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { Redis } from 'ioredis';
@Injectable()
export class ApiKeyRateLimitMiddleware implements NestMiddleware {
constructor(private redis: Redis) {}
async use(req: Request, res: Response, next: NextFunction) {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return next();
}
const key = `rate_limit:api_key:${apiKey}`;
const count = await this.redis.incr(key);
if (count === 1) {
await this.redis.expire(key, 60); // 1 minute window
}
if (count > 60) {
return res.status(429).json({
error: 'Rate limit exceeded',
retryAfter: await this.redis.ttl(key),
});
}
next();
}
}
3. CORS Configuration
Strict CORS for Production:
// main.ts
app.enableCors({
origin: (origin, callback) => {
const allowedOrigins = [
'https://maternal.noru1.ro',
'https://maternal-web.noru1.ro',
];
// Allow mobile apps (check user agent)
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else if (origin.includes('capacitor://')) {
callback(null, true); // Ionic/Capacitor
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: [
'Content-Type',
'Authorization',
'X-API-Key',
'X-Internal-API-Key',
],
});
📱 Mobile App Integration
React Native / Expo Configuration
// mobile-app/src/services/api.ts
import axios from 'axios';
import Constants from 'expo-constants';
const API_CONFIG = {
baseURL: Constants.expoConfig?.extra?.apiUrl || 'https://api.maternal.noru1.ro',
headers: {
'X-API-Key': Constants.expoConfig?.extra?.apiKey,
'User-Agent': `Maternal-${Platform.OS}/${Constants.expoConfig?.version}`,
},
};
const apiClient = axios.create(API_CONFIG);
// Add JWT token to requests
apiClient.interceptors.request.use((config) => {
const token = getAuthToken(); // From AsyncStorage
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
WebSocket Connection (mobile):
import io from 'socket.io-client';
const socket = io('wss://api.maternal.noru1.ro', {
auth: {
token: getAuthToken(),
},
transports: ['websocket'],
reconnection: true,
reconnectionAttempts: 5,
});
🔍 Monitoring & Logging
1. Request Logging (Nginx)
log_format api_log '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'api_key=$http_x_api_key '
'request_time=$request_time';
access_log /var/log/nginx/maternal_api_access.log api_log;
2. Backend Request Tracking
// src/common/middleware/request-logger.middleware.ts
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
@Injectable()
export class RequestLoggerMiddleware implements NestMiddleware {
private logger = new Logger('HTTP');
use(req: Request, res: Response, next: NextFunction) {
const { method, originalUrl, headers } = req;
const userAgent = headers['user-agent'];
const apiKey = headers['x-api-key'];
const isInternal = headers['x-internal-api-key'] ? 'internal' : 'external';
const start = Date.now();
res.on('finish', () => {
const { statusCode } = res;
const duration = Date.now() - start;
this.logger.log(
`${method} ${originalUrl} ${statusCode} ${duration}ms - ${isInternal} - ${apiKey?.substring(0, 8)}...`
);
});
next();
}
}
🚀 Deployment Strategy
Phase 1: MVP Deployment (Week 1-2)
Week 1:
- Create Next.js API proxy routes
- Update frontend to use BFF
- Add internal API key guard to backend
- Test web app with new architecture
Week 2:
- Configure Nginx reverse proxy
- Set up SSL certificates
- Deploy to production
- Monitor and fix issues
Phase 2: Mobile App Support (Week 3-4)
Week 3:
- Create mobile API keys
- Configure CORS for mobile
- Set up mobile-specific rate limits
- Test with React Native/Expo
Week 4:
- Add mobile user agent detection
- Implement mobile analytics
- Load testing with mobile traffic
- Documentation for mobile devs
Phase 3: Kong Gateway (Month 2-3)
Month 2:
- Set up Kong API Gateway
- Configure plugins (rate limit, JWT, logging)
- Migrate Next.js BFF to Kong routes
- Test parallel deployment
Month 3:
- Full cutover to Kong
- Remove Next.js BFF (optional)
- Advanced features (caching, GraphQL federation)
- Performance optimization
📊 Performance Considerations
Latency Impact
| Architecture | Latency | Notes |
|---|---|---|
| Direct Backend | 50-100ms | Current (baseline) |
| Next.js BFF | +20-40ms | Acceptable for web |
| Kong Gateway | +10-20ms | Production-optimized |
Caching Strategy
Next.js Edge Caching:
// app/api/proxy/[...path]/route.ts
export const revalidate = 60; // Cache for 60 seconds
// Or per-route
if (path.startsWith('children')) {
return NextResponse.json(data, {
headers: {
'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600',
},
});
}
Redis Caching (backend):
// Already implemented in backend
@UseInterceptors(CacheInterceptor)
@CacheTTL(300)
async getChildren() {
// ...
}
✅ Acceptance Criteria
Security
- Backend not directly accessible from browser (except WebSocket for now)
- Internal API key required for BFF → Backend
- Mobile API key required for mobile → Backend
- Rate limiting enforced at Nginx and NestJS levels
- CORS configured for web and mobile origins
- SSL/TLS for all external connections
Functionality
- Web app works through BFF without code changes to components
- GraphQL queries work through proxy
- WebSocket connections remain stable
- Mobile apps can connect directly to backend
- Real-time sync works across web and mobile
Performance
- Latency increase < 50ms for BFF
- No degradation in WebSocket performance
- API response times within SLA (<200ms p95)
Monitoring
- Request logs include API key and source
- Rate limit violations logged
- Error tracking for proxy failures
- Metrics dashboard shows BFF vs direct traffic
🔧 Troubleshooting
Common Issues
1. WebSocket Connections Failing
- Symptom: Socket.io connection refused
- Fix: Ensure WebSocket endpoint bypasses BFF (direct backend connection)
- Config: Update
NEXT_PUBLIC_WS_URLto backend URL
2. CORS Errors on Mobile
- Symptom: OPTIONS preflight fails
- Fix: Add mobile origins to CORS whitelist
- Config: Check
capacitor://localhostis allowed
3. Rate Limiting Too Aggressive
- Symptom: 429 errors during normal usage
- Fix: Adjust Nginx
limit_reqor NestJS throttler - Config: Increase burst size or rate
4. GraphQL Subscriptions Not Working
- Symptom: Subscriptions disconnect immediately
- Fix: GraphQL subscriptions need WebSocket, can't go through HTTP proxy
- Solution: Use Apollo Client with split link (HTTP for queries, WS for subscriptions)
📚 References
- Next.js API Routes Documentation
- Kong API Gateway
- Nginx Reverse Proxy Guide
- NestJS Guards
- Socket.io CORS Configuration
Last Updated: October 3, 2025
Review Date: Post-MVP Launch
Owner: Backend Team