Skip to content

Commit 5bf27f8

Browse files
authored
Merge pull request #201 from Mozez155/feat/cdn-optimization
feat: global CDN content delivery optimization
2 parents 30f8820 + 1535aee commit 5bf27f8

5 files changed

Lines changed: 167 additions & 0 deletions

File tree

backend/.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ BACKUP_INTERVAL_HOURS=24
7878
# Redis (optional — falls back to in-memory L1 cache if not set)
7979
# REDIS_URL=redis://localhost:6379
8080

81+
# CDN Configuration
82+
CDN_ENABLED=false
83+
# CDN_URL=https://cdn.example.com
84+
# CDN_SECONDARY_URL=https://cdn2.example.com
85+
# Comma-separated list of CDN edge regions
86+
CDN_REGIONS=us-east-1,eu-west-1,ap-southeast-1
87+
# Max-age for immutable static assets (seconds, default 86400 = 1 day)
88+
CDN_CACHE_MAX_AGE_S=86400
89+
8190
# Cache TTLs (seconds)
8291
CACHE_TTL_BALANCE_S=30
8392
RATE_CACHE_TTL_S=60

backend/src/cdn/index.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* CDN Optimization Module
3+
*
4+
* Provides:
5+
* - Multi-region CDN configuration
6+
* - Dynamic cache-control header strategy
7+
* - Edge computing hints (Vary, Surrogate-Control)
8+
* - CDN analytics & monitoring
9+
* - Security headers (CSP, HSTS, X-Frame-Options)
10+
* - Cost optimization (cache-hit tracking)
11+
* - Failover: primary → secondary CDN origin
12+
*
13+
* Env vars:
14+
* CDN_URL=https://cdn.example.com
15+
* CDN_SECONDARY_URL=https://cdn2.example.com
16+
* CDN_ENABLED=true
17+
* CDN_CACHE_MAX_AGE_S=86400
18+
*/
19+
20+
import logger from '../config/logger.js';
21+
22+
// ── Config ────────────────────────────────────────────────────────────────────
23+
24+
export function getCdnConfig() {
25+
return {
26+
enabled: process.env.CDN_ENABLED === 'true',
27+
primaryUrl: process.env.CDN_URL ?? null,
28+
secondaryUrl: process.env.CDN_SECONDARY_URL ?? null,
29+
maxAgeSeconds: parseInt(process.env.CDN_CACHE_MAX_AGE_S ?? '86400', 10),
30+
regions: (process.env.CDN_REGIONS ?? 'us-east-1').split(',').map(r => r.trim()),
31+
};
32+
}
33+
34+
// ── Cache strategy ────────────────────────────────────────────────────────────
35+
36+
const CACHE_PROFILES = {
37+
// Static assets with content hash — cache forever
38+
immutable: (maxAge) => `public, max-age=${maxAge}, immutable`,
39+
// API responses — short TTL, allow stale while revalidating
40+
api: () => 'public, max-age=30, stale-while-revalidate=60',
41+
// User-specific data — private, no CDN caching
42+
private: () => 'private, no-store',
43+
// HTML entry point — always revalidate
44+
html: () => 'public, max-age=0, must-revalidate',
45+
};
46+
47+
export function getCacheHeaders(profile = 'api') {
48+
const { maxAgeSeconds } = getCdnConfig();
49+
const directive = CACHE_PROFILES[profile]?.(maxAgeSeconds) ?? CACHE_PROFILES.api();
50+
return { 'Cache-Control': directive };
51+
}
52+
53+
// ── Security headers ──────────────────────────────────────────────────────────
54+
55+
export function getSecurityHeaders() {
56+
return {
57+
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',
58+
'X-Content-Type-Options': 'nosniff',
59+
'X-Frame-Options': 'DENY',
60+
'Referrer-Policy': 'strict-origin-when-cross-origin',
61+
'Permissions-Policy': 'geolocation=(), microphone=()',
62+
};
63+
}
64+
65+
// ── Asset URL resolution with failover ───────────────────────────────────────
66+
67+
let primaryFailed = false;
68+
69+
export function resolveAssetUrl(path) {
70+
const { enabled, primaryUrl, secondaryUrl } = getCdnConfig();
71+
if (!enabled || !primaryUrl) return path;
72+
const base = primaryFailed && secondaryUrl ? secondaryUrl : primaryUrl;
73+
return `${base.replace(/\/$/, '')}/${path.replace(/^\//, '')}`;
74+
}
75+
76+
export function reportCdnFailure(origin) {
77+
if (origin === 'primary') {
78+
primaryFailed = true;
79+
logger.warn('cdn.failover.activated', { fallback: getCdnConfig().secondaryUrl });
80+
}
81+
}
82+
83+
export function resetCdnFailover() {
84+
primaryFailed = false;
85+
logger.info('cdn.failover.reset');
86+
}
87+
88+
// ── Analytics & monitoring ────────────────────────────────────────────────────
89+
90+
const cdnStats = {
91+
cacheHits: 0,
92+
cacheMisses: 0,
93+
originRequests: 0,
94+
failovers: 0,
95+
byRegion: {},
96+
};
97+
98+
export function recordCdnEvent({ type, region }) {
99+
if (type === 'hit') cdnStats.cacheHits++;
100+
if (type === 'miss') { cdnStats.cacheMisses++; cdnStats.originRequests++; }
101+
if (type === 'failover') cdnStats.failovers++;
102+
if (region) cdnStats.byRegion[region] = (cdnStats.byRegion[region] ?? 0) + 1;
103+
}
104+
105+
export function getCdnStats() {
106+
const total = cdnStats.cacheHits + cdnStats.cacheMisses;
107+
return {
108+
...cdnStats,
109+
hitRate: total > 0 ? (cdnStats.cacheHits / total).toFixed(3) : null,
110+
config: getCdnConfig(),
111+
};
112+
}
113+
114+
// ── Express middleware ────────────────────────────────────────────────────────
115+
116+
/**
117+
* Attach CDN-friendly cache-control and security headers to responses.
118+
* Profile is determined by request path:
119+
* /assets/* → immutable (Vite hashed bundles)
120+
* /api/* → api
121+
* *.html → html
122+
* default → api
123+
*/
124+
export function cdnMiddleware(req, res, next) {
125+
const path = req.path;
126+
let profile = 'api';
127+
if (path.startsWith('/assets/')) profile = 'immutable';
128+
else if (path.endsWith('.html')) profile = 'html';
129+
else if (path.startsWith('/api/')) profile = 'api';
130+
131+
const cacheHeaders = getCacheHeaders(profile);
132+
const secHeaders = getSecurityHeaders();
133+
Object.assign(res, {}); // ensure res is writable
134+
res.set({ ...cacheHeaders, ...secHeaders });
135+
136+
// Edge computing hint: vary on Accept-Encoding for compression
137+
res.set('Vary', 'Accept-Encoding');
138+
139+
// Surrogate-Control for CDN-specific TTL (Fastly/Varnish)
140+
if (profile === 'immutable') {
141+
res.set('Surrogate-Control', `max-age=${getCdnConfig().maxAgeSeconds}`);
142+
}
143+
144+
next();
145+
}

backend/src/routes/metrics.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import express from 'express';
22
import { getSnapshot, resetMetrics } from '../monitoring/metrics.js';
3+
import { getCdnStats } from '../cdn/index.js';
34
import { checkShardHealth, getShardStats } from '../db/sharding.js';
45

56
const router = express.Router();
@@ -15,6 +16,9 @@ router.delete('/', (_req, res) => {
1516
res.json({ message: 'Metrics reset' });
1617
});
1718

19+
// GET /api/metrics/cdn — CDN analytics and config
20+
router.get('/cdn', (_req, res) => {
21+
res.json(getCdnStats());
1822
// GET /api/metrics/shards — shard pool stats
1923
router.get('/shards', (_req, res) => {
2024
res.json(getShardStats());

backend/src/server.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { auditLogger } from './security/index.js';
3434
import { getConfig } from './config/env.js';
3535
import { createRateLimiter } from './middleware/rateLimiter.js';
3636
import { performanceMiddleware } from './monitoring/middleware.js';
37+
import { cdnMiddleware } from './cdn/index.js';
3738
import {
3839
requestIdMiddleware,
3940
errorLogger,
@@ -72,6 +73,8 @@ app.use(createRateLimiter());
7273
// Performance monitoring
7374
app.use(performanceMiddleware);
7475

76+
// CDN cache-control and security headers
77+
app.use(cdnMiddleware);
7578
// Input sanitization (runs before all route handlers)
7679
app.use(sanitizeInputs);
7780

frontend/vite.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import react from '@vitejs/plugin-react';
33
import { visualizer } from 'rollup-plugin-visualizer';
44

55
export default defineConfig(({ mode }) => ({
6+
// CDN base URL: set VITE_CDN_URL in .env to serve assets from CDN
7+
base: process.env.VITE_CDN_URL ?? '/',
68
plugins: [
79
react(),
810
// Bundle analysis: generates stats.html after `npm run build`
@@ -21,6 +23,10 @@ export default defineConfig(({ mode }) => ({
2123
motion: ['framer-motion'],
2224
stellar: ['@stellar/stellar-sdk'],
2325
},
26+
// Ensure hashed filenames for immutable CDN caching
27+
entryFileNames: 'assets/[name]-[hash].js',
28+
chunkFileNames: 'assets/[name]-[hash].js',
29+
assetFileNames: 'assets/[name]-[hash][extname]',
2430
},
2531
},
2632
// Performance budget: warn if any chunk > 500 kB

0 commit comments

Comments
 (0)