From ef31ed35647fb2a2b6dd8e0b2f6cfa893cfae83c Mon Sep 17 00:00:00 2001 From: christiankrag Date: Sun, 14 Dec 2025 13:27:25 +0100 Subject: [PATCH] Add health endpoint for Swarm health checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /health route returns JSON status for Docker Swarm to verify container health during deployment. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- docs/cloudflare-setup.md | 380 +++++++++ docs/cloudflare-workers-roadmap.md | 591 ++++++++++++++ docs/performance-checklist.md | 753 ++++++++++++++++++ src/app/health/route.ts | 5 + .../20241214_add_phase_metadata.sql | 70 ++ 5 files changed, 1799 insertions(+) create mode 100644 docs/cloudflare-setup.md create mode 100644 docs/cloudflare-workers-roadmap.md create mode 100644 docs/performance-checklist.md create mode 100644 src/app/health/route.ts create mode 100644 supabase/migrations/20241214_add_phase_metadata.sql diff --git a/docs/cloudflare-setup.md b/docs/cloudflare-setup.md new file mode 100644 index 0000000..f1a45ac --- /dev/null +++ b/docs/cloudflare-setup.md @@ -0,0 +1,380 @@ +# Cloudflare Setup for Mylder Platform + +## Overview +Cloudflare configuration for https://mylder.io - Next.js 16 frontend with self-hosted backend (Supabase, n8n) via Dokploy/Traefik. + +**VPS:** 149.102.155.84 +**Stack:** Next.js 16 → Traefik → Cloudflare CDN + +## DNS Configuration + +### Step 1: Add Domain to Cloudflare +1. Cloudflare Dashboard → Add Site → mylder.io +2. Update nameservers at domain registrar to Cloudflare's nameservers + +### Step 2: DNS Records + +| Type | Name | Target | Proxy Status | Notes | +|------|------|--------|--------------|-------| +| A | @ | 149.102.155.84 | Proxied (orange cloud) | Main site | +| A | www | 149.102.155.84 | Proxied | WWW redirect | +| A | dokploy | 149.102.155.84 | DNS Only (grey cloud) | Dokploy dashboard - skip CDN | +| A | supabase | 149.102.155.84 | DNS Only | Supabase API - skip CDN | +| A | n8n | 149.102.155.84 | DNS Only | n8n webhooks - skip CDN | +| A | gitea | 149.102.155.84 | DNS Only | Git push/pull - skip CDN | +| A | saas | 149.102.155.84 | DNS Only | Zulip chat - skip CDN | + +**Key Principle:** +- **Proxied (CDN):** Frontend, static assets, public content +- **DNS Only:** Admin tools, APIs with authentication, real-time services + +### Step 3: Subdomain Wildcard (Optional) +If you need dynamic subdomains (e.g., tenant.mylder.io): +``` +CNAME * @ (Proxied) +``` + +## SSL/TLS Settings + +### Encryption Mode: Full (Strict) +**Path:** SSL/TLS → Overview → Full (strict) + +**Why:** Traefik already provides Let's Encrypt certificates. Full (strict) ensures end-to-end encryption: +``` +Browser → Cloudflare (encrypted) → Traefik (encrypted) → App +``` + +### Edge Certificates +**Path:** SSL/TLS → Edge Certificates + +- **Always Use HTTPS:** ON +- **Minimum TLS Version:** 1.2 +- **Opportunistic Encryption:** ON +- **TLS 1.3:** ON +- **Automatic HTTPS Rewrites:** ON +- **Certificate Transparency Monitoring:** ON + +### Origin Server +**Path:** SSL/TLS → Origin Server + +No action needed - Traefik manages Let's Encrypt certificates automatically. + +## Caching Rules + +### Default Cache Levels +**Path:** Caching → Configuration + +- **Caching Level:** Standard +- **Browser Cache TTL:** Respect Existing Headers +- **Always Online:** ON (serves stale content if origin is down) + +### Cache Rules for Next.js +**Path:** Rules → Page Rules (legacy) or Cache Rules (new)** + +#### Rule 1: Static Assets (Aggressive Caching) +``` +URL Pattern: mylder.io/_next/static/* +Settings: + - Cache Level: Cache Everything + - Edge Cache TTL: 1 year + - Browser Cache TTL: 1 year +``` + +#### Rule 2: Images (Smart Caching) +``` +URL Pattern: mylder.io/_next/image/* +Settings: + - Cache Level: Cache Everything + - Edge Cache TTL: 30 days + - Browser Cache TTL: 7 days +``` + +#### Rule 3: API Routes (No Cache) +``` +URL Pattern: mylder.io/api/* +Settings: + - Cache Level: Bypass +``` + +#### Rule 4: Dynamic Pages (Smart Cache) +``` +URL Pattern: mylder.io/* +Settings: + - Cache Level: Standard + - Edge Cache TTL: 2 hours + - Browser Cache TTL: 30 minutes +``` + +### Next.js Cache Headers +Ensure your Next.js app sets proper headers: + +```typescript +// next.config.ts +const nextConfig = { + async headers() { + return [ + { + source: '/_next/static/:path*', + headers: [ + { + key: 'Cache-Control', + value: 'public, max-age=31536000, immutable', + }, + ], + }, + { + source: '/_next/image/:path*', + headers: [ + { + key: 'Cache-Control', + value: 'public, max-age=2592000, stale-while-revalidate=86400', + }, + ], + }, + ]; + }, +}; +``` + +### Cache Purge Strategy +**Path:** Caching → Configuration → Purge Cache + +- **On Deploy:** Purge Everything (via API or Dashboard) +- **On Content Update:** Purge by Tag/URL +- **Emergency:** Purge Everything button + +**API Purge (for CI/CD):** +```bash +curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \ + -H "Authorization: Bearer {api_token}" \ + -H "Content-Type: application/json" \ + --data '{"purge_everything":true}' +``` + +## Speed Optimization + +### Auto Minify +**Path:** Speed → Optimization + +- **JavaScript:** ON +- **CSS:** ON +- **HTML:** ON + +### Brotli Compression +**Path:** Speed → Optimization + +- **Brotli:** ON (better than gzip) + +### Early Hints +**Path:** Speed → Optimization + +- **Early Hints:** ON (preload critical resources) + +### Rocket Loader +**Path:** Speed → Optimization + +- **Rocket Loader:** OFF (can conflict with React hydration) + +### Image Optimization +**Path:** Speed → Optimization → Image Resizing + +- **Polish:** Lossless (or Lossy for smaller files) +- **Mirage:** ON (lazy loading for mobile) +- **Image Resizing:** Available on Pro+ plan + +**Next.js Image Optimization:** +```typescript +// next.config.ts +const nextConfig = { + images: { + formats: ['image/avif', 'image/webp'], + deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048], + imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], + minimumCacheTTL: 2592000, // 30 days + }, +}; +``` + +## Security Settings + +### SSL/TLS +- **Always Use HTTPS:** ON +- **HSTS:** Enable with max-age=31536000, includeSubDomains + +### Firewall (WAF) +**Path:** Security → WAF + +#### Managed Rules (Free Tier) +- **Cloudflare Managed Ruleset:** ON +- **OWASP Core Ruleset:** ON + +#### Rate Limiting (Pro+ Plan) +```yaml +Rule: API Protection +If: Request URL contains /api/ +Then: Rate limit 100 requests per minute per IP +``` + +#### Bot Fight Mode (Free) +**Path:** Security → Bots + +- **Bot Fight Mode:** ON +- **Super Bot Fight Mode:** Pro+ plan + +### Security Level +**Path:** Security → Settings + +- **Security Level:** Medium (balance between security and UX) + +### Challenge Passage +**Path:** Security → Settings + +- **Challenge Passage:** 30 minutes + +## Network Settings + +### HTTP/2 & HTTP/3 +**Path:** Network + +- **HTTP/2:** ON (default) +- **HTTP/3 (QUIC):** ON (faster connections) +- **0-RTT Connection Resumption:** ON + +### WebSockets +**Path:** Network + +- **WebSockets:** ON (required for real-time features) + +### gRPC +**Path:** Network + +- **gRPC:** OFF (not needed for standard Next.js) + +## Analytics & Monitoring + +### Web Analytics +**Path:** Analytics & Logs → Web Analytics + +- Enable Cloudflare Web Analytics (privacy-friendly, no cookie banner needed) + +### Performance Monitoring +- **Core Web Vitals:** Track LCP, FID, CLS +- **Page Load Time:** Monitor at edge + +### Real User Monitoring (RUM) +Add Cloudflare Zaraz or custom analytics: +```html + + +``` + +## Subdomain Configuration + +Since you have multiple services, ensure proper routing: + +### Traefik Labels (Dokploy Docker Compose) +```yaml +services: + app: + labels: + - "traefik.enable=true" + - "traefik.http.routers.mylder.rule=Host(`mylder.io`) || Host(`www.mylder.io`)" + - "traefik.http.routers.mylder.tls=true" + - "traefik.http.routers.mylder.tls.certresolver=letsencrypt" +``` + +### WWW Redirect (Cloudflare) +**Path:** Rules → Redirect Rules + +``` +If: Hostname equals www.mylder.io +Then: Redirect to https://mylder.io$1 (301 permanent) +``` + +## Testing Checklist + +### DNS Propagation +```bash +dig mylder.io +nslookup mylder.io +``` + +### SSL Certificate +```bash +curl -I https://mylder.io +# Check for: HTTP/2 200, cf-cache-status header +``` + +### Cache Verification +```bash +curl -I https://mylder.io/_next/static/[hash]/[file].js +# Look for: cf-cache-status: HIT +``` + +### Performance +```bash +# Lighthouse +npx lighthouse https://mylder.io --view + +# WebPageTest +# Visit https://www.webpagetest.org +``` + +## Troubleshooting + +### 521 Error (Web Server Down) +- Check Traefik is running: `docker ps | grep traefik` +- Check Dokploy deployment status + +### 522 Error (Connection Timed Out) +- Verify firewall allows Cloudflare IPs +- Check VPS security groups + +### 525 Error (SSL Handshake Failed) +- Ensure SSL/TLS mode is "Full (strict)" +- Verify Traefik has valid Let's Encrypt certificate + +### Cache Not Working +- Check Cache-Control headers in response +- Verify Page Rules are active +- Clear Cloudflare cache and test + +### Mixed Content Warnings +- Enable "Automatic HTTPS Rewrites" +- Update hardcoded http:// URLs to https:// + +## Cost Optimization + +### Free Plan Limits +- 100k requests/day (~3M/month) +- Unlimited bandwidth +- Basic DDoS protection +- Shared SSL certificate +- 3 Page Rules + +### When to Upgrade to Pro ($20/month) +- Need more than 3 Page Rules +- Want Image Optimization (Polish, Mirage) +- Need custom SSL certificate +- Require advanced WAF rules +- Want priority support + +### Enterprise Features (for future) +- Cloudflare Workers (serverless at edge) +- Load Balancing +- Argo Smart Routing +- Custom WAF rules + +## Next Steps + +1. **Immediate:** DNS + SSL/TLS setup +2. **Week 1:** Caching rules + performance testing +3. **Month 1:** Monitor analytics, tune cache rules +4. **Quarter 1:** Consider Workers for edge logic (see cloudflare-workers-roadmap.md) + +## References + +- [Cloudflare DNS Docs](https://developers.cloudflare.com/dns/) +- [Next.js Cloudflare Deployment](https://nextjs.org/docs/app/building-your-application/deploying) +- [Cloudflare Cache API](https://developers.cloudflare.com/cache/) diff --git a/docs/cloudflare-workers-roadmap.md b/docs/cloudflare-workers-roadmap.md new file mode 100644 index 0000000..4b3d3ab --- /dev/null +++ b/docs/cloudflare-workers-roadmap.md @@ -0,0 +1,591 @@ +# Cloudflare Workers Roadmap for Mylder + +## Overview +Edge computing strategy for Mylder platform using Cloudflare Workers, R2, D1, and KV. + +**Philosophy:** Move compute closer to users, reduce VPS load, improve UX with sub-100ms responses. + +## Current Architecture vs Edge-First + +### Current (VPS-Centric) +``` +User → Cloudflare CDN → VPS (149.102.155.84) + ├── Next.js (Frontend) + ├── Supabase (Auth, DB) + ├── n8n (Automation) + └── Dokploy (Orchestration) +``` + +### Future (Edge-First) +``` +User → Cloudflare Workers (Edge Logic) + ├── Static Assets (CDN) + ├── Edge Auth (Workers + KV) + ├── Edge API (Workers + D1) + └── Origin (VPS for heavy compute) + ├── Supabase (Primary DB) + └── n8n (Automation) +``` + +## Workers Use Cases + +### 1. Edge Authentication (Priority: High) + +**Problem:** Every auth check hits VPS → Supabase (100-300ms) +**Solution:** Cache JWT validation at edge + +```typescript +// workers/auth-edge.ts +export default { + async fetch(request: Request, env: Env): Promise { + const token = request.headers.get('Authorization')?.split('Bearer ')[1]; + if (!token) return new Response('Unauthorized', { status: 401 }); + + // Check KV cache for user session + const cached = await env.SESSIONS.get(token); + if (cached) { + return new Response(cached, { + headers: { 'X-Auth-Source': 'edge-cache' } + }); + } + + // Fallback to Supabase origin + const response = await fetch('https://supabase.mylder.io/auth/v1/user', { + headers: { Authorization: `Bearer ${token}` } + }); + + if (response.ok) { + const user = await response.text(); + await env.SESSIONS.put(token, user, { expirationTtl: 3600 }); // 1 hour + return new Response(user, { + headers: { 'X-Auth-Source': 'origin' } + }); + } + + return response; + } +}; +``` + +**Benefits:** +- Auth checks: 300ms → 10ms (30x faster) +- Reduced Supabase load +- Better UX for logged-in users + +**Trade-offs:** +- Session invalidation complexity +- KV storage costs ($0.50/1M reads) + +### 2. API Rate Limiting (Priority: High) + +**Problem:** DDoS/abuse protection on VPS is resource-intensive +**Solution:** Rate limit at edge before hitting origin + +```typescript +// workers/rate-limiter.ts +import { RateLimiter } from '@cloudflare/workers-rate-limiter'; + +export default { + async fetch(request: Request, env: Env): Promise { + const limiter = new RateLimiter({ + namespace: env.RATE_LIMIT, + // 100 requests per 60 seconds per IP + limit: 100, + period: 60, + }); + + const ip = request.headers.get('CF-Connecting-IP') || 'unknown'; + const { success } = await limiter.limit({ key: ip }); + + if (!success) { + return new Response('Rate limit exceeded', { + status: 429, + headers: { 'Retry-After': '60' } + }); + } + + // Forward to origin + return fetch(request); + } +}; +``` + +**Benefits:** +- Block abuse before it hits VPS +- Per-IP, per-user, per-endpoint limits +- 0ms latency overhead + +**Use Cases:** +- `/api/*` endpoints +- Login attempts +- Form submissions +- Search queries + +### 3. A/B Testing at Edge (Priority: Medium) + +**Problem:** Client-side A/B testing causes layout shift (poor CLS) +**Solution:** Server-side rendering with edge variants + +```typescript +// workers/ab-test.ts +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + // Check existing variant + let variant = request.headers.get('Cookie')?.match(/variant=([AB])/)?.[1]; + + if (!variant) { + // Assign 50/50 split + variant = Math.random() < 0.5 ? 'A' : 'B'; + } + + // Rewrite request to origin with variant + const originUrl = `${url.origin}${url.pathname}?variant=${variant}`; + const response = await fetch(originUrl, request); + + // Set variant cookie + const headers = new Headers(response.headers); + headers.set('Set-Cookie', `variant=${variant}; Path=/; Max-Age=2592000`); + + return new Response(response.body, { + status: response.status, + headers + }); + } +}; +``` + +**Benefits:** +- No layout shift (CLS = 0) +- Instant variant assignment +- Analytics at edge + +**Use Cases:** +- Landing page variants +- Pricing page tests +- Feature flags + +### 4. Edge Personalization (Priority: Medium) + +**Problem:** Generic content for all users (low engagement) +**Solution:** Geo/device-specific content at edge + +```typescript +// workers/personalize.ts +export default { + async fetch(request: Request, env: Env): Promise { + const country = request.cf?.country as string; + const device = request.headers.get('User-Agent')?.includes('Mobile') + ? 'mobile' + : 'desktop'; + + // Fetch base HTML from origin + const response = await fetch(request); + const html = await response.text(); + + // Inject personalized content + const personalized = html + .replace('{{COUNTRY}}', country) + .replace('{{DEVICE}}', device) + .replace('{{CURRENCY}}', getCurrency(country)); + + return new Response(personalized, { + headers: { + 'Content-Type': 'text/html', + 'Cache-Control': 'public, max-age=60, s-maxage=3600', + 'Vary': 'CF-IPCountry, User-Agent' + } + }); + } +}; + +function getCurrency(country: string): string { + const map: Record = { + US: 'USD', GB: 'GBP', EU: 'EUR', DK: 'DKK' + }; + return map[country] || 'USD'; +} +``` + +**Benefits:** +- Geo-specific content (currency, language) +- Device-optimized markup +- Sub-50ms personalization + +**Use Cases:** +- Pricing display +- Content localization +- Feature availability + +### 5. Image Optimization & Resizing (Priority: Low) + +**Problem:** Next.js Image Optimization on VPS is CPU-intensive +**Solution:** Cloudflare Images or Workers + R2 + +```typescript +// workers/image-optimize.ts +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + const imagePath = url.pathname.replace('/img/', ''); + + // Parse resize params + const width = url.searchParams.get('w') || '1920'; + const quality = url.searchParams.get('q') || '80'; + const format = url.searchParams.get('f') || 'webp'; + + // Fetch from R2 + const object = await env.IMAGES.get(imagePath); + if (!object) return new Response('Not Found', { status: 404 }); + + // Resize at edge (requires paid Workers plan) + const resized = await fetch(`https://mylder.io/cdn-cgi/image/width=${width},quality=${quality},format=${format}/${imagePath}`); + + return new Response(resized.body, { + headers: { + 'Content-Type': `image/${format}`, + 'Cache-Control': 'public, max-age=31536000, immutable' + } + }); + } +}; +``` + +**Benefits:** +- Offload VPS CPU +- Auto WebP/AVIF conversion +- Responsive images on-demand + +**Trade-offs:** +- Requires Cloudflare Images ($5/month + $1/100k) +- Or R2 storage ($0.015/GB/month) + +### 6. Edge API Caching (Priority: Medium) + +**Problem:** Repeated API calls for same data +**Solution:** Smart caching with stale-while-revalidate + +```typescript +// workers/api-cache.ts +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const cache = caches.default; + const cacheKey = new Request(request.url, request); + + // Check cache + let response = await cache.match(cacheKey); + if (response) { + const age = Date.now() - new Date(response.headers.get('Date')!).getTime(); + + // If stale (>60s), revalidate in background + if (age > 60000) { + ctx.waitUntil(revalidate(request, cache, cacheKey)); + } + + return response; + } + + // Fetch from origin + response = await fetch(request); + + // Cache for 5 minutes + const cached = new Response(response.body, response); + cached.headers.set('Cache-Control', 'public, max-age=300, stale-while-revalidate=3600'); + ctx.waitUntil(cache.put(cacheKey, cached)); + + return response; + } +}; + +async function revalidate(request: Request, cache: Cache, key: Request) { + const fresh = await fetch(request); + await cache.put(key, fresh.clone()); +} +``` + +**Benefits:** +- API responses: 200ms → 10ms +- Stale content served instantly +- Background revalidation + +**Use Cases:** +- Public API endpoints +- User profiles +- Content feeds + +## Cloudflare R2 (Object Storage) + +### Use Cases for R2 + +**What:** S3-compatible object storage at edge (no egress fees) + +#### 1. User Uploads +```typescript +// Store user avatars, documents +await env.UPLOADS.put(`users/${userId}/avatar.jpg`, file); +``` + +**Benefits:** +- $0.015/GB/month (10x cheaper than S3) +- No egress fees (free downloads) +- Global CDN distribution + +**When to Use:** +- User-generated content (avatars, documents) +- Static assets (fonts, icons) +- Media files (videos, audio) + +**When NOT to Use:** +- Frequent small writes (use D1 instead) +- Sub-10MB total storage (use KV instead) + +#### 2. Static Asset Hosting +```typescript +// Move _next/static/* to R2 +const asset = await env.STATIC_ASSETS.get(path); +return new Response(asset.body, { + headers: { 'Cache-Control': 'public, max-age=31536000, immutable' } +}); +``` + +**Benefits:** +- Reduce VPS storage needs +- Faster global distribution +- Immutable content = perfect cache + +#### 3. Backup Storage +```typescript +// Daily Supabase backups to R2 +await env.BACKUPS.put(`supabase-${date}.sql.gz`, backup); +``` + +## Cloudflare D1 (Edge Database) + +### Use Cases for D1 + +**What:** SQLite at edge (sub-10ms queries globally) + +#### 1. Edge Metadata +```typescript +// Store user preferences, feature flags +const db = env.DB; +const result = await db.prepare( + 'SELECT theme, locale FROM user_prefs WHERE user_id = ?' +).bind(userId).first(); +``` + +**Benefits:** +- Read latency: 100ms (Supabase) → 5ms (D1) +- No VPS load for reads +- SQL interface (familiar) + +**When to Use:** +- Read-heavy data (user settings, configs) +- Small datasets (<1GB) +- Geo-distributed reads + +**When NOT to Use:** +- Write-heavy workloads (D1 eventual consistency) +- Complex joins (use Supabase instead) +- Primary data storage (Supabase is source of truth) + +#### 2. Analytics/Metrics +```typescript +// Store page views, click events +await db.prepare( + 'INSERT INTO analytics (page, user_id, timestamp) VALUES (?, ?, ?)' +).bind(page, userId, Date.now()).run(); +``` + +**Benefits:** +- High-volume writes +- Real-time aggregations +- No Supabase quota impact + +#### 3. Feature Flags +```typescript +// Enable/disable features per user +const flags = await db.prepare( + 'SELECT * FROM feature_flags WHERE active = 1' +).all(); +``` + +### D1 vs Supabase Decision Matrix + +| Feature | D1 (Edge) | Supabase (VPS) | +|---------|-----------|----------------| +| Read Latency | 5-10ms | 100-300ms | +| Write Latency | 50-100ms | 100-300ms | +| Consistency | Eventual | Strong | +| Storage Limit | 1GB | 8GB+ | +| Cost | Free (5M reads/day) | Included in VPS | +| Use Case | Cached metadata | Primary data | + +**Strategy:** D1 = edge cache, Supabase = source of truth + +## Cloudflare KV (Key-Value Store) + +### Use Cases for KV + +**What:** Global key-value store (sub-1ms reads) + +#### 1. Session Storage +```typescript +await env.SESSIONS.put(sessionId, userData, { expirationTtl: 3600 }); +``` + +**Benefits:** +- Fastest storage option (<1ms reads) +- Auto-expiration (TTL) +- Global replication + +**Cost:** $0.50/1M reads, $5/1M writes (generous free tier) + +#### 2. Configuration/Secrets +```typescript +const apiKey = await env.CONFIG.get('stripe_public_key'); +``` + +#### 3. Rate Limit Counters +```typescript +const count = await env.RATE_LIMIT.get(ip); +await env.RATE_LIMIT.put(ip, count + 1, { expirationTtl: 60 }); +``` + +## Implementation Roadmap + +### Phase 1: Foundation (Month 1) +**Goal:** Basic edge infrastructure + +- [ ] Set up Cloudflare Workers project +- [ ] Deploy edge auth validator (Worker + KV) +- [ ] Implement rate limiting for `/api/*` +- [ ] Migrate static assets to R2 +- [ ] Monitor performance impact + +**Success Metrics:** +- Auth latency: <20ms (p95) +- Static asset cache hit rate: >95% +- VPS CPU load: -20% + +### Phase 2: Optimization (Month 2-3) +**Goal:** Performance wins + +- [ ] Deploy A/B testing worker +- [ ] Implement edge personalization +- [ ] Set up D1 for user preferences +- [ ] Create stale-while-revalidate API cache +- [ ] Image optimization via R2 + Workers + +**Success Metrics:** +- LCP: <1.5s (p75) +- API latency: -50% for cached endpoints +- Global availability: 99.9% + +### Phase 3: Advanced (Month 4+) +**Goal:** Edge-first architecture + +- [ ] Move session management to edge (D1 + KV) +- [ ] Implement edge analytics (D1) +- [ ] Deploy feature flags system (D1) +- [ ] Create edge API gateway +- [ ] Multi-region failover + +**Success Metrics:** +- Edge request ratio: >70% +- Origin load: -60% +- Global latency: <100ms (p95) + +## Cost Analysis + +### Free Tier Limits +- **Workers:** 100k requests/day +- **KV:** 100k reads/day, 1k writes/day +- **R2:** 10GB storage, 1M reads/month +- **D1:** 5M reads/day, 100k writes/day + +### Paid Costs (Estimated for 1M users/month) + +| Service | Usage | Cost/Month | +|---------|-------|------------| +| Workers | 100M requests | $5 (bundled) | +| KV | 50M reads | $25 | +| R2 | 100GB storage + 500M reads | $1.50 | +| D1 | Included in Workers | $0 | +| **Total** | | **~$30/month** | + +**ROI:** Potential to downgrade VPS ($30/month savings) → break even + +## Anti-Patterns to Avoid + +### 1. Over-Caching +**Bad:** Cache everything at edge for 24 hours +**Good:** Cache static assets long, dynamic content short + +### 2. Edge as Primary DB +**Bad:** Store all user data in D1 +**Good:** D1 = cache, Supabase = source of truth + +### 3. Synchronous Edge-Origin Calls +**Bad:** Worker → fetch origin → wait → respond +**Good:** Serve stale, revalidate in background + +### 4. Ignoring Cold Starts +**Bad:** Assume Workers are instant +**Good:** Optimize bundle size (<1MB), minimize dependencies + +### 5. Complex Business Logic at Edge +**Bad:** Move entire Next.js app to Workers +**Good:** Keep heavy compute on VPS, edge for lightweight tasks + +## Monitoring & Debugging + +### Workers Analytics +**Dashboard:** Workers & Pages → Analytics + +- Request volume +- Success rate (2xx/3xx/4xx/5xx) +- CPU time (p50, p95, p99) +- Error logs + +### Tail Workers (Real-Time Logs) +```bash +wrangler tail mylder-auth-edge +``` + +### Custom Metrics +```typescript +// workers/metrics.ts +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const start = Date.now(); + const response = await fetch(request); + const duration = Date.now() - start; + + // Log to D1 analytics + ctx.waitUntil( + env.ANALYTICS.prepare( + 'INSERT INTO metrics (endpoint, duration, status) VALUES (?, ?, ?)' + ).bind(request.url, duration, response.status).run() + ); + + return response; + } +}; +``` + +## Next Steps + +1. **Prototype:** Deploy simple edge auth worker (1 day) +2. **Measure:** Compare edge vs origin performance (1 week) +3. **Iterate:** Expand to rate limiting, A/B testing (1 month) +4. **Optimize:** Move 70%+ traffic to edge (3 months) + +## References + +- [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/) +- [R2 Storage](https://developers.cloudflare.com/r2/) +- [D1 Database](https://developers.cloudflare.com/d1/) +- [Workers Examples](https://developers.cloudflare.com/workers/examples/) +- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/) diff --git a/docs/performance-checklist.md b/docs/performance-checklist.md new file mode 100644 index 0000000..b6d9aca --- /dev/null +++ b/docs/performance-checklist.md @@ -0,0 +1,753 @@ +# Performance Checklist for Mylder Platform + +## Core Web Vitals Targets + +### Production Goals (75th Percentile) + +| Metric | Target | Good | Needs Improvement | Poor | +|--------|--------|------|-------------------|------| +| **LCP** (Largest Contentful Paint) | <2.5s | <2.5s | 2.5s-4.0s | >4.0s | +| **FID** (First Input Delay) | <100ms | <100ms | 100ms-300ms | >300ms | +| **CLS** (Cumulative Layout Shift) | <0.1 | <0.1 | 0.1-0.25 | >0.25 | +| **INP** (Interaction to Next Paint) | <200ms | <200ms | 200ms-500ms | >500ms | +| **TTFB** (Time to First Byte) | <600ms | <800ms | 800ms-1800ms | >1800ms | +| **FCP** (First Contentful Paint) | <1.8s | <1.8s | 1.8s-3.0s | >3.0s | + +### Current vs Target Performance + +**Baseline (before optimization):** +``` +LCP: ~3.5s → Target: <2.5s (30% improvement needed) +FID: ~150ms → Target: <100ms +CLS: ~0.15 → Target: <0.1 +TTFB: ~800ms → Target: <600ms (edge caching) +``` + +### Measurement Tools + +#### 1. Real User Monitoring (RUM) +```typescript +// app/layout.tsx +import { SpeedInsights } from '@vercel/speed-insights/next'; +import { Analytics } from '@vercel/analytics/react'; + +export default function RootLayout({ children }) { + return ( + + + {children} + + + + + ); +} +``` + +**Alternative:** Cloudflare Web Analytics (privacy-friendly, no cookies) +```html + +``` + +#### 2. Lab Testing +```bash +# Lighthouse (local) +npx lighthouse https://mylder.io --view + +# WebPageTest (global) +# https://www.webpagetest.org + +# Chrome DevTools +# DevTools → Performance → Record → Analyze +``` + +#### 3. Continuous Monitoring +```bash +# Lighthouse CI (in CI/CD pipeline) +npm install -g @lhci/cli +lhci autorun --collect.url=https://mylder.io +``` + +## Next.js Optimization Settings + +### 1. Production Build Configuration + +```typescript +// next.config.ts +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = { + // Enable React strict mode for better error handling + reactStrictMode: true, + + // Optimize bundle size + swcMinify: true, + + // Compress output + compress: true, + + // Optimize images + images: { + formats: ['image/avif', 'image/webp'], + deviceSizes: [640, 750, 828, 1080, 1200, 1920], + imageSizes: [16, 32, 48, 64, 96, 128, 256], + minimumCacheTTL: 2592000, // 30 days + dangerouslyAllowSVG: false, + contentDispositionType: 'attachment', + remotePatterns: [ + { + protocol: 'https', + hostname: 'supabase.mylder.io', + pathname: '/storage/v1/object/**', + }, + ], + }, + + // Optimize fonts (automatic optimization) + optimizeFonts: true, + + // Experimental features + experimental: { + // Optimize CSS + optimizeCss: true, + // Tree shaking for smaller bundles + optimizePackageImports: ['@radix-ui/react-icons', 'lucide-react'], + }, + + // Custom headers for caching + async headers() { + return [ + { + source: '/:path*', + headers: [ + { + key: 'X-DNS-Prefetch-Control', + value: 'on', + }, + { + key: 'X-Frame-Options', + value: 'SAMEORIGIN', + }, + ], + }, + { + source: '/_next/static/:path*', + headers: [ + { + key: 'Cache-Control', + value: 'public, max-age=31536000, immutable', + }, + ], + }, + { + source: '/_next/image/:path*', + headers: [ + { + key: 'Cache-Control', + value: 'public, max-age=2592000, stale-while-revalidate=86400', + }, + ], + }, + ]; + }, +}; + +export default nextConfig; +``` + +### 2. Bundle Analyzer +```bash +# Install +npm install -D @next/bundle-analyzer + +# next.config.ts +import bundleAnalyzer from '@next/bundle-analyzer'; + +const withBundleAnalyzer = bundleAnalyzer({ + enabled: process.env.ANALYZE === 'true', +}); + +export default withBundleAnalyzer(nextConfig); + +# Run analysis +ANALYZE=true npm run build +``` + +**Target:** Total bundle size <500KB (gzipped) + +### 3. Code Splitting Strategies + +#### Dynamic Imports +```typescript +// app/dashboard/page.tsx +import dynamic from 'next/dynamic'; + +// Lazy load heavy components +const HeavyChart = dynamic(() => import('@/components/HeavyChart'), { + loading: () => , + ssr: false, // Client-side only if needed +}); + +export default function Dashboard() { + return ; +} +``` + +#### Route-Based Splitting +```typescript +// Automatic with App Router +app/ + dashboard/ # Separate chunk + settings/ # Separate chunk + (marketing)/ # Separate chunk (route group) +``` + +#### Vendor Splitting +```typescript +// next.config.ts +experimental: { + optimizePackageImports: [ + '@radix-ui/react-icons', + 'lucide-react', + 'date-fns', + ], +}, +``` + +### 4. Server Components (Default) +```typescript +// app/page.tsx (Server Component by default) +export default async function Home() { + // Fetch at build time or request time + const data = await fetch('https://api.mylder.io/data', { + next: { revalidate: 3600 } // ISR: 1 hour cache + }); + + return
{/* No client-side JS for this component */}
; +} +``` + +**Benefits:** +- 0KB JavaScript sent to client +- Faster initial page load +- Better SEO + +### 5. Client Components (Opt-In) +```typescript +// components/InteractiveButton.tsx +'use client'; + +import { useState } from 'react'; + +export default function InteractiveButton() { + const [count, setCount] = useState(0); + return ; +} +``` + +**Rule:** Only use 'use client' when you need: +- Interactivity (useState, useEffect) +- Browser APIs (localStorage, window) +- Event handlers (onClick, onSubmit) + +## Image Optimization + +### 1. Next.js Image Component +```typescript +import Image from 'next/image'; + +// Optimized image with automatic sizing +Hero image + +// Responsive image +Hero image +``` + +### 2. Image Format Priority +1. **AVIF** (best compression, 50% smaller than JPEG) +2. **WebP** (wide support, 30% smaller than JPEG) +3. **JPEG/PNG** (fallback) + +### 3. Cloudflare Image Optimization +```typescript +// next.config.ts +images: { + loader: 'custom', + loaderFile: './lib/cloudflare-image-loader.ts', +} + +// lib/cloudflare-image-loader.ts +export default function cloudflareLoader({ src, width, quality }) { + const params = [`width=${width}`, `quality=${quality || 75}`, 'format=auto']; + return `https://mylder.io/cdn-cgi/image/${params.join(',')}/${src}`; +} +``` + +### 4. Lazy Loading Images +```typescript +// Non-critical images (below fold) +Feature + +// Critical images (above fold, LCP) +Hero +``` + +### 5. Responsive Images Checklist +- [ ] Use Next.js Image component (automatic optimization) +- [ ] Set explicit width/height (prevent CLS) +- [ ] Use `priority` for LCP images +- [ ] Use `loading="lazy"` for below-fold images +- [ ] Provide blur placeholder for better UX +- [ ] Compress images before upload (80-85% quality) +- [ ] Use modern formats (AVIF, WebP) + +## Font Optimization + +### 1. Next.js Font Optimization (Built-In) +```typescript +// app/layout.tsx +import { Inter, Roboto_Mono } from 'next/font/google'; + +const inter = Inter({ + subsets: ['latin'], + display: 'swap', // Prevent invisible text flash + variable: '--font-inter', + preload: true, +}); + +const robotoMono = Roboto_Mono({ + subsets: ['latin'], + display: 'swap', + variable: '--font-roboto-mono', + weight: ['400', '700'], // Only load needed weights +}); + +export default function RootLayout({ children }) { + return ( + + {children} + + ); +} +``` + +**Benefits:** +- Self-hosted fonts (no external requests) +- Automatic subset optimization +- Zero layout shift (font-display: swap) + +### 2. Custom Fonts +```typescript +import localFont from 'next/font/local'; + +const myFont = localFont({ + src: './fonts/MyFont.woff2', + display: 'swap', + variable: '--font-my-font', +}); +``` + +### 3. Font Loading Best Practices +- [ ] Use only 2-3 font families max +- [ ] Load only needed weights (400, 700) +- [ ] Use `font-display: swap` +- [ ] Preload critical fonts +- [ ] Self-host fonts (no Google Fonts CDN) + +## CSS Optimization + +### 1. Tailwind CSS Production Config +```typescript +// tailwind.config.ts +import type { Config } from 'tailwindcss'; + +const config: Config = { + content: [ + './app/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + ], + theme: { + extend: {}, + }, + // Remove unused styles in production + future: { + hoverOnlyWhenSupported: true, // Better mobile UX + }, +}; + +export default config; +``` + +### 2. Critical CSS +Next.js automatically inlines critical CSS for first paint. + +### 3. CSS Module Best Practices +```typescript +// Use CSS Modules for component-specific styles +import styles from './Button.module.css'; + +export function Button() { + return ; +} +``` + +## JavaScript Optimization + +### 1. Tree Shaking +```typescript +// Good: Import only what you need +import { format } from 'date-fns'; + +// Bad: Import entire library +import * as dateFns from 'date-fns'; +``` + +### 2. Remove Unused Dependencies +```bash +# Analyze bundle +npx depcheck + +# Remove unused +npm uninstall unused-package +``` + +### 3. Use Smaller Alternatives + +| Heavy Package | Lightweight Alternative | Savings | +|---------------|------------------------|---------| +| Moment.js (232KB) | date-fns (13KB) | 95% | +| Lodash (70KB) | Lodash-es (24KB) | 66% | +| Axios (30KB) | Fetch API (native) | 100% | +| React Icons (full) | Lucide React (tree-shakeable) | 80% | + +### 4. Debounce/Throttle Heavy Operations +```typescript +// components/Search.tsx +'use client'; + +import { useState, useCallback } from 'react'; +import { debounce } from 'lodash-es/debounce'; + +export function Search() { + const [query, setQuery] = useState(''); + + const handleSearch = useCallback( + debounce(async (value: string) => { + const results = await fetch(`/api/search?q=${value}`); + // Handle results + }, 300), + [] + ); + + return handleSearch(e.target.value)} />; +} +``` + +## Caching Strategy + +### 1. Static Generation (Fastest) +```typescript +// app/blog/page.tsx +export const revalidate = false; // Static at build time + +export default async function Blog() { + const posts = await fetchPosts(); + return ; +} +``` + +### 2. Incremental Static Regeneration (ISR) +```typescript +// app/blog/[slug]/page.tsx +export const revalidate = 3600; // Revalidate every hour + +export async function generateStaticParams() { + const posts = await fetchPosts(); + return posts.map((post) => ({ slug: post.slug })); +} + +export default async function Post({ params }) { + const post = await fetchPost(params.slug); + return
; +} +``` + +### 3. Server-Side Rendering (SSR) +```typescript +// app/dashboard/page.tsx +export const dynamic = 'force-dynamic'; // Always server-render + +export default async function Dashboard() { + const data = await fetchUserData(); // Per-request + return ; +} +``` + +### 4. Client-Side Caching +```typescript +// Use SWR or React Query for client-side data fetching +'use client'; + +import useSWR from 'swr'; + +export function Profile() { + const { data, error } = useSWR('/api/user', fetcher, { + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshInterval: 60000, // 1 minute + }); + + if (error) return
Error
; + if (!data) return
Loading...
; + return
{data.name}
; +} +``` + +### 5. HTTP Cache Headers +```typescript +// app/api/data/route.ts +export async function GET() { + const data = await fetchData(); + + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400', + }, + }); +} +``` + +## Cache Invalidation Strategy + +### 1. On-Demand Revalidation +```typescript +// app/api/revalidate/route.ts +import { revalidatePath, revalidateTag } from 'next/cache'; + +export async function POST(request: Request) { + const { path, tag } = await request.json(); + + if (path) { + revalidatePath(path); + } + + if (tag) { + revalidateTag(tag); + } + + return Response.json({ revalidated: true }); +} +``` + +### 2. Webhook Triggers (n8n) +```typescript +// n8n workflow: CMS Update → POST /api/revalidate +{ + "path": "/blog", + "tag": "posts" +} +``` + +### 3. Cloudflare Cache Purge +```bash +# On deploy (CI/CD) +curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \ + -H "Authorization: Bearer {api_token}" \ + -H "Content-Type: application/json" \ + --data '{"purge_everything":true}' +``` + +### 4. Selective Purge +```bash +# Purge specific URLs +--data '{"files":["https://mylder.io/blog","https://mylder.io/about"]}' + +# Purge by tag +--data '{"tags":["blog-posts"]}' +``` + +## Performance Testing Workflow + +### 1. Pre-Deploy Checks +```bash +# Run before every deployment +npm run build # Must succeed +npm run lint # No errors +npm run typecheck # No type errors + +# Optional but recommended +ANALYZE=true npm run build # Check bundle size +npx lighthouse https://staging.mylder.io --view +``` + +### 2. Post-Deploy Verification +```bash +# Production smoke tests +curl -I https://mylder.io # Check TTFB +curl -I https://mylder.io/_next/static/[hash]/[file].js # Check caching + +# Full performance audit +npx lighthouse https://mylder.io --view + +# Real user monitoring +# Check Cloudflare Analytics or Vercel Analytics dashboard +``` + +### 3. Continuous Monitoring +```bash +# Set up alerts in Cloudflare/monitoring tool +- LCP > 3s → Alert +- Error rate > 1% → Alert +- TTFB > 1s → Alert +``` + +## Quick Wins Checklist + +### Immediate (1 day) +- [ ] Enable Cloudflare CDN (see cloudflare-setup.md) +- [ ] Add proper cache headers to static assets +- [ ] Use Next.js Image component everywhere +- [ ] Enable `reactStrictMode` and `swcMinify` +- [ ] Remove unused dependencies + +### Short-term (1 week) +- [ ] Implement ISR for blog/content pages +- [ ] Lazy load heavy components +- [ ] Optimize images (compress, convert to WebP/AVIF) +- [ ] Set up bundle analyzer +- [ ] Add loading states and skeletons (improve perceived performance) + +### Medium-term (1 month) +- [ ] Implement edge caching for auth (see cloudflare-workers-roadmap.md) +- [ ] Set up real user monitoring (RUM) +- [ ] Optimize third-party scripts (defer/async) +- [ ] Implement stale-while-revalidate for APIs +- [ ] Add Lighthouse CI to deployment pipeline + +### Long-term (3 months) +- [ ] Move 70% traffic to edge (Cloudflare Workers) +- [ ] Implement edge personalization +- [ ] Set up A/B testing at edge +- [ ] Achieve Core Web Vitals targets consistently +- [ ] Reduce VPS load by 60%+ + +## Common Performance Issues & Fixes + +### Issue: High LCP (>4s) +**Causes:** +- Large hero image not optimized +- Server response time (TTFB) too high +- Render-blocking resources + +**Fixes:** +- Use Next.js Image with `priority` for LCP image +- Enable Cloudflare CDN for faster TTFB +- Inline critical CSS +- Preload fonts + +### Issue: High CLS (>0.25) +**Causes:** +- Images without width/height +- Web fonts loading late +- Dynamic content injections + +**Fixes:** +- Set explicit dimensions on images +- Use `font-display: swap` +- Reserve space for dynamic content +- Avoid inserting content above existing content + +### Issue: High FID/INP (>300ms) +**Causes:** +- Heavy JavaScript execution +- Long tasks blocking main thread +- Unoptimized event handlers + +**Fixes:** +- Code split large bundles +- Debounce/throttle event handlers +- Use Web Workers for heavy computation +- Optimize third-party scripts + +### Issue: Slow TTFB (>1s) +**Causes:** +- Slow server response +- No CDN +- Database queries during render + +**Fixes:** +- Enable Cloudflare CDN +- Use ISR instead of SSR +- Move database queries to edge (D1) +- Implement caching layers + +## Monitoring Dashboard Setup + +### Cloudflare Web Analytics +```html + + +``` + +### Custom Performance Monitoring +```typescript +// app/layout.tsx +'use client'; + +import { useEffect } from 'react'; +import { usePathname } from 'next/navigation'; + +export function PerformanceMonitor() { + const pathname = usePathname(); + + useEffect(() => { + // Measure Web Vitals + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(console.log); + getFID(console.log); + getFCP(console.log); + getLCP(console.log); + getTTFB(console.log); + }); + }, [pathname]); + + return null; +} +``` + +## Resources + +- [Next.js Performance Docs](https://nextjs.org/docs/app/building-your-application/optimizing) +- [Web.dev Core Web Vitals](https://web.dev/vitals/) +- [Lighthouse CI](https://github.com/GoogleChrome/lighthouse-ci) +- [Cloudflare Performance](https://developers.cloudflare.com/fundamentals/speed/) diff --git a/src/app/health/route.ts b/src/app/health/route.ts new file mode 100644 index 0000000..c8c2736 --- /dev/null +++ b/src/app/health/route.ts @@ -0,0 +1,5 @@ +import { NextResponse } from 'next/server' + +export async function GET() { + return NextResponse.json({ status: 'ok', timestamp: Date.now() }) +} diff --git a/supabase/migrations/20241214_add_phase_metadata.sql b/supabase/migrations/20241214_add_phase_metadata.sql new file mode 100644 index 0000000..f8b0be4 --- /dev/null +++ b/supabase/migrations/20241214_add_phase_metadata.sql @@ -0,0 +1,70 @@ +-- Add phase_metadata column for agentic workflow tracking +ALTER TABLE projects ADD COLUMN IF NOT EXISTS phase_metadata JSONB DEFAULT '{ + "currentPhase": "think", + "completedPhases": [], + "phaseStatus": "not_started", + "activities": [], + "metrics": { + "totalDuration": 0, + "phaseDurations": {}, + "iterationCount": 0 + } +}'::jsonb; + +-- Create index for efficient querying +CREATE INDEX IF NOT EXISTS idx_projects_phase_metadata ON projects USING gin(phase_metadata); + +-- Add comment for documentation +COMMENT ON COLUMN projects.phase_metadata IS 'Stores agentic development loop state: currentPhase, completedPhases, phaseStatus, activities, metrics'; + +-- Create function to update phase +CREATE OR REPLACE FUNCTION update_project_phase( + p_project_id UUID, + p_new_phase TEXT, + p_status TEXT DEFAULT 'in_progress' +) +RETURNS JSONB AS $$ +DECLARE + v_current_metadata JSONB; + v_current_phase TEXT; + v_completed_phases JSONB; + v_new_activity JSONB; + v_updated_metadata JSONB; +BEGIN + -- Get current metadata + SELECT phase_metadata INTO v_current_metadata + FROM projects WHERE id = p_project_id; + + v_current_phase := v_current_metadata->>'currentPhase'; + v_completed_phases := COALESCE(v_current_metadata->'completedPhases', '[]'::jsonb); + + -- Add current phase to completed if transitioning + IF v_current_phase IS NOT NULL AND v_current_phase != p_new_phase THEN + v_completed_phases := v_completed_phases || to_jsonb(v_current_phase); + END IF; + + -- Create activity record + v_new_activity := jsonb_build_object( + 'id', gen_random_uuid(), + 'type', 'phase_change', + 'message', 'Transitioned to ' || p_new_phase || ' phase', + 'timestamp', now() + ); + + -- Build updated metadata + v_updated_metadata := jsonb_build_object( + 'currentPhase', p_new_phase, + 'completedPhases', v_completed_phases, + 'phaseStatus', p_status, + 'activities', (v_new_activity || COALESCE(v_current_metadata->'activities', '[]'::jsonb))[0:49], + 'metrics', COALESCE(v_current_metadata->'metrics', '{}'::jsonb) + ); + + -- Update project + UPDATE projects + SET phase_metadata = v_updated_metadata, updated_at = now() + WHERE id = p_project_id; + + RETURN v_updated_metadata; +END; +$$ LANGUAGE plpgsql;