docs: Create comprehensive API Gateway architecture and security plan
Create detailed implementation plan for securing backend API while supporting web, mobile apps, WebSockets, and GraphQL. ## Current State Analysis - Backend API fully exposed to internet (security risk) - Direct API calls from browser - No infrastructure-level rate limiting - Future mobile apps need direct access - WebSocket + GraphQL endpoints require special handling ## Proposed Solutions ### Phase 1: MVP - Next.js BFF Pattern (1-2 weeks) - Next.js API routes as proxy for web app - Direct backend access for mobile (with API keys) - Internal API key authentication - WebSocket remains direct (Next.js limitation) ### Phase 2: Production - Kong Gateway (4-6 weeks post-MVP) - Centralized API gateway for all clients - Backend becomes fully internal - Advanced features: caching, monitoring, GraphQL federation ## Implementation Details **Files to Create**: - app/api/proxy/[...path]/route.ts - Generic REST proxy - app/api/graphql/route.ts - GraphQL proxy - src/common/guards/internal-api-key.guard.ts - Backend auth **Security Features**: - Internal API key for BFF → Backend communication - Mobile API key for mobile → Backend - Rate limiting: 100 req/min (web), 60 req/min (mobile) - Strict CORS configuration - Nginx reverse proxy with SSL termination **Architecture Diagrams**: - BFF pattern with Next.js (recommended for MVP) - Kong Gateway pattern (production-ready) - Mobile app integration strategy **Includes**: - Step-by-step implementation plan - Code samples for all components - Nginx configuration - Environment variable setup - Security enhancements - Performance considerations (+20-40ms latency) - Monitoring and logging - Troubleshooting guide - Deployment timeline Addresses security concerns while maintaining support for: ✅ Web app (through BFF) ✅ Mobile apps (direct with API key) ✅ Real-time WebSocket ✅ GraphQL queries and subscriptions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
942
docs/implementation-docs/API_GATEWAY_ARCHITECTURE.md
Normal file
942
docs/implementation-docs/API_GATEWAY_ARCHITECTURE.md
Normal file
@@ -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=<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**:
|
||||
```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
|
||||
Reference in New Issue
Block a user