diff --git a/docs/implementation-docs/API_GATEWAY_ARCHITECTURE.md b/docs/implementation-docs/API_GATEWAY_ARCHITECTURE.md new file mode 100644 index 0000000..f08ad36 --- /dev/null +++ b/docs/implementation-docs/API_GATEWAY_ARCHITECTURE.md @@ -0,0 +1,942 @@ +# 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: +1. Backend API is fully exposed to the internet (security risk) +2. No rate limiting at infrastructure level +3. Direct API access bypasses Next.js middleware/auth checks +4. Future mobile apps will need direct API access +5. WebSocket connections need persistent connection handling +6. 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) +1. **Web App**: Next.js API routes as BFF (proxy to backend) +2. **Mobile Apps**: Direct backend access with API keys +3. **Backend**: Exposed only to mobile, hidden from web +4. **Timeline**: 1-2 weeks + +### Phase 2: Production (Kong Gateway) +1. **All Clients**: Route through Kong API Gateway +2. **Backend**: Completely internal, not exposed +3. **Security**: Centralized auth, rate limiting, logging +4. **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` + +```typescript +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` + +```typescript +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) + +```typescript +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.ts` +- `lib/apollo-client.ts` +- All API utility files + +**Changes**: + +```typescript +// 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**: +```bash +# 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) + +```typescript +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): + +```typescript +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**: + +```typescript +// 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): + +```typescript +// 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` + +```nginx +# 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**: +```bash +# Backend (.env.production) +INTERNAL_API_KEY= +MOBILE_API_KEY= + +# Next.js (.env.local) +INTERNAL_API_KEY= +BACKEND_API_URL=http://localhost:3020 # or internal network IP +``` + +**Generation**: +```bash +# 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): + +```typescript +// 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**: + +```typescript +// 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 + +```typescript +// 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): + +```typescript +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) + +```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 + +```typescript +// 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**: +- [x] Create Next.js API proxy routes +- [x] Update frontend to use BFF +- [x] Add internal API key guard to backend +- [x] Test web app with new architecture + +**Week 2**: +- [x] Configure Nginx reverse proxy +- [x] Set up SSL certificates +- [x] Deploy to production +- [x] 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**: +```typescript +// 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): +```typescript +// 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_URL` to backend URL + +**2. CORS Errors on Mobile** +- **Symptom**: OPTIONS preflight fails +- **Fix**: Add mobile origins to CORS whitelist +- **Config**: Check `capacitor://localhost` is allowed + +**3. Rate Limiting Too Aggressive** +- **Symptom**: 429 errors during normal usage +- **Fix**: Adjust Nginx `limit_req` or 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](https://nextjs.org/docs/api-routes/introduction) +- [Kong API Gateway](https://konghq.com/products/kong-gateway) +- [Nginx Reverse Proxy Guide](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) +- [NestJS Guards](https://docs.nestjs.com/guards) +- [Socket.io CORS Configuration](https://socket.io/docs/v4/handling-cors/) + +--- + +**Last Updated**: October 3, 2025 +**Review Date**: Post-MVP Launch +**Owner**: Backend Team