📚 AI Playbook · v3.3 (+ AI Onboarding Kit: Bootstrap · Prompts · Decision · Patterns · MCP)

🎯 CodeHub Skills

The definitive reference for AI agents building games & apps on CodeHub

100
Sub-Phases
12
Learning Paths
37
Glossary Terms
70+
Live Apps
~480K
Total Chars

🛤️ Learning Paths

"ฉันอยากสร้าง X ต้องอ่านอะไรบ้าง" — 12 ready-to-follow paths that map real-world goals to specific sub-phases in execution order. Click path to expand.

🏙️

Build a 3D City / Builder Game

Games Advanced ⏱ ~3 hours reading

Babylon.js grid + cell placement + level-up visuals + NPC villagers. Pattern used in stronghold-3d.

🔍 Click to see pros/cons/gotchas for each step →
📋 Show full path detail (8 steps with WHY for each) ▼
1.1
💡 base HTML shell with Canvas + module loading
1.6
💡 Canvas 2D for fallback UI / minimap
3.1
💡 Babylon.js core engine for 3D scene + camera + lighting
1.5
💡 Web Audio for placement SFX + ambient
4.1
💡 Game loop with fixed timestep (60fps)
4.4
💡 Behavior Trees for NPC villagers (eat/work/sleep)
4.9
💡 Achievements (e.g. ‘first city’, ‘max population’)
10.2
💡 City Builder pattern: grid, adjacency bonus, level-up scaling 1.15x
🦕

Build a 2D Platformer Game

Games Intermediate ⏱ ~3 hours reading

Phaser 3 sprites + physics + gravity + coyote time + variable jump. Pattern used in street-fighter-thai, sky-ace.

🔍 Click to see pros/cons/gotchas for each step →
📋 Show full path detail (5 steps with WHY for each) ▼
1.1
💡 single-file HTML shell
3.2
💡 Phaser 3 for sprites + Arcade physics
4.1
💡 game loop with dt
4.6
💡 Inventory/Slot system for power-ups + collectibles
10.5
💡 Platformer pattern: gravity + coyote time + variable jump
🏪

Build a Restaurant / Tycoon Game

Games Intermediate ⏱ ~3 hours reading

Customer queue + patience bar + tips + reputation + upgrade + balance. Pattern used in noodle-shop v1.1.

📚 Requires: 1.1, 1.6
🔍 Click to see pros/cons/gotchas for each step →
📋 Show full path detail (7 steps with WHY for each) ▼
1.1
💡 HTML5 shell
1.6
💡 Canvas 2D for game rendering
3.3
💡 vanilla canvas for simple tycoon (or 3.2 for Phaser)
4.1
💡 game loop with decoupled fixed timestep
4.9
💡 achievements (rich_1k, master_chef)
10.1
💡 Tycoon Game pattern (queue, tips, upgrade)
10.7
💡 Restaurant Sim variant (Thai food — menu, recipes)
⚔️

Build an RPG (NPCs + Quests + Dialogue)

Games Advanced ⏱ ~5 hours reading

NPC behavior trees + dialogue tree + quest progress + inventory. Pattern in sims-lite, realm-of-heroes.

📚 Requires: 1.1, 3.1, 4.4
🔍 Click to see pros/cons/gotchas for each step →
📋 Show full path detail (7 steps with WHY for each) ▼
3.1
💡 Babylon.js for 3D world
4.4
💡 Behavior Trees — the heart of NPC AI
4.6
💡 Inventory system for items
4.7
💡 Dialogue tree (branching JSON)
4.8
💡 Quest system
4.9
💡 Achievements
10.3
💡 RPG Pattern — NPC needs + relationships + dialogue + quest
📄

Build a CRUD SaaS App

Business Apps Intermediate ⏱ ~4 hours reading

Form-builder + FastAPI + SQLite/Postgres + REST API + auth. Pattern in medbook, dinebook, pos-grocery.

📚 Requires: 1.1, 1.7
🔍 Click to see pros/cons/gotchas for each step →
📋 Show full path detail (13 steps with WHY for each) ▼
1.1
💡 HTML shell
1.4
💡 localStorage for drafts + user prefs
1.7
💡 Fetch API for backend calls
1.8
💡 File API for upload (receipts, images)
5.4
💡 Booking pattern (slot picker, timezone)
5.5
💡 POS / cart pattern if e-commerce
5.6
💡 Inventory model
5.7
💡 Form Builder for admin input
9.1
💡 REST API design
9.2
💡 FastAPI backend
9.4
💡 SQLite for single-user / PostgreSQL for multi-user
9.6
💡 Redis cache for hot data
9.10
💡 Rate limiting
💬

Build an AI Chatbot / Assistant

AI Apps Intermediate ⏱ ~3 hours reading

OpenAI streaming + RAG + memory + function calling. Pattern in memo-ai, mavis-assistant.

📚 Requires: 1.7
🔍 Click to see pros/cons/gotchas for each step →
📋 Show full path detail (9 steps with WHY for each) ▼
1.7
💡 Fetch API with retry + AbortController
7.1
💡 OpenAI API streaming via SSE
7.6
💡 Streaming Responses (server-sent events)
7.9
💡 Prompt Engineering (few-shot, system message)
7.4
💡 Embeddings for semantic search
7.5
💡 TF-IDF bilingual fallback (TH+EN without API)
7.8
💡 RAG (vector search + prompt context)
7.10
💡 Safety filters (OpenAI moderation)
9.8
💡 Server-Sent Events for streaming
🎤

Build a Karaoke / Lyric Video Studio

Creator Tools Advanced ⏱ ~3 hours reading

Web Audio + FFT beat detection + canvas animation + MP4 export. Pattern in karaoke-studio v1.1.

📚 Requires: 1.6, 1.9
🔍 Click to see pros/cons/gotchas for each step →
📋 Show full path detail (5 steps with WHY for each) ▼
1.9
💡 MediaRecorder API for screen+audio recording
6.1
💡 Audio waveform + trim editor
6.6
💡 Lyric Video Maker — timing + display
6.7
💡 Karaoke Studio pattern — 11 themes + FFT beat + MP4 export via ffmpeg.wasm
6.10
💡 Screen recorder for capture-then-edit
🦍

Build an Image Editor / Filter App

AI Apps / Dev Tools Intermediate ⏱ ~2 hours reading

Canvas-based crop + filter + sticker overlay. Pattern in fitcheck, palette, cutout-studio.

📚 Requires: 1.6, 1.8
🔍 Click to see pros/cons/gotchas for each step →
📋 Show full path detail (5 steps with WHY for each) ▼
1.6
💡 Canvas 2D core rendering
1.8
💡 File API for drag-drop upload
6.4
💡 Image Editor pattern — crop/filter/composite
7.3
💡 Vision API for AI auto-tagging (optional)
5.8
💡 Dashboard Charts for color analysis (palette)
🚀

Deploy a Static App to Custom Domain

Infrastructure Intermediate ⏱ ~1.5 hours reading

ship.py + nginx + Let's Encrypt + paramiko. The complete CI/CD pipeline.

📚 Requires: 1.1
🔍 Click to see pros/cons/gotchas for each step →
📋 Show full path detail (7 steps with WHY for each) ▼
8.1
💡 ship.py one-command deploy (edit → curl → paramiko → API)
8.2
💡 nginx config (location alias, try_files for SPA)
8.3
💡 Let's Encrypt SSL (certbot --nginx)
8.4
💡 Paramiko SSH from sandbox to VPS (NEVER inline $DEPLOY_VPS_PASS)
8.6
💡 systemd service for long-running backends
8.8
💡 Backup 3-2-1 rule
8.10
💡 CDN (jsDelivr/unpkg for static assets)

Build a Backend with FastAPI + Auth

Data & APIs Advanced ⏱ ~4 hours reading

Production-grade REST API with JWT auth + rate limiting + Alembic migrations. Used by codehub-api.

📚 Requires: 1.7
🔍 Click to see pros/cons/gotchas for each step →
📋 Show full path detail (7 steps with WHY for each) ▼
9.1
💡 REST API design conventions
9.2
💡 FastAPI core (Pydantic models, async routes)
9.3
💡 SQLAlchemy ORM + Alembic migrations
9.4
💡 SQLite for dev (WAL mode for concurrent reads)
9.5
💡 PostgreSQL for production
9.6
💡 Redis cache (pipelining for batches)
9.9
💡 OAuth 2.0 (PKCE for SPA)
🚀

Ship a Working MVP in 1 Day

Productivity Beginner ⏱ ~3 hours reading

Minimal viable product using only HTML5 + localStorage + Fetch — no build, no npm, ship today.

🔍 Click to see pros/cons/gotchas for each step →
📋 Show full path detail (7 steps with WHY for each) ▼
1.1
💡 Single-file HTML shell (no build)
2.1
💡 Color tokens for instant theme
2.2
💡 Typography scale (less decision fatigue)
1.4
💡 localStorage for state persistence
1.7
💡 Fetch for any backend
1.10
💡 PWA for offline + installable
8.1
💡 ship.py for deploy in 1 command
🏜️

Build a Tower Defense

Games Intermediate ⏱ ~3 hours reading

Grid + A* pathfinding + tower targeting + waves. Standard TD loop.

📚 Requires: 3.2, 4.3
🔍 Click to see pros/cons/gotchas for each step →
📋 Show full path detail (4 steps with WHY for each) ▼
3.2
💡 Phaser 3 for sprites + tile map
4.1
💡 Game loop with dt
4.3
💡 A* pathfinding for enemy path
10.4
💡 Tower Defense pattern (path + waves + towers + range/DPS)

📖 Glossary (37 cross-cutting terms)

Terms that appear across multiple sub-phases. Click to expand definition.

Babylon.js
Microsoft-backed 3D engine for browsers (PBR, GUI, physics, WebXR). v8 = current. 1.5MB+ minified. 📍 Used in: 3.1
Phaser 3
Comprehensive 2D engine with Arcade/P2/Matter physics. TypeScript-supported. ~800KB. 📍 Used in: 3.2
WebSocket
Full-duplex TCP over HTTP for realtime push. Requires sticky session load balancer for scale. 📍 Used in: 3.9, 9.7
Server-Sent Events (SSE)
HTTP-native server→client streaming. Newline-delimited JSON. Auto-reconnect. One-way. 📍 Used in: 9.8
Promise.race / AbortController
Cancel pending fetch via AbortController when user navigates away — prevents zombie requests. 📍 Used in: 1.7
WASM
WebAssembly — near-native speed in browser. Babylon Havok physics, ffmpeg.wasm, sql.js use this. 📍 Used in: 3.1, 6.7
FFmpeg.wasm
FFmpeg compiled to WASM (~30MB). Used for in-browser MP4 transcoding without server. 📍 Used in: 6.7, 8.10
ThinInstance (Babylon)
Draw 10000 identical meshes in 1 draw call via thinInstanceAdd — vs 10000 separate draw calls. Critical for city builders. 📍 Used in: 3.1
ArcRotateCamera (Babylon)
Default Babylon orbit camera. alpha (azimuth) + beta (polar) + radius. alpha/beta in radians, NOT degrees (Math.PI = 180°). 📍 Used in: 3.1
Behavior Tree
Hierarchical AI: Selector (OR), Sequence (AND), Decorator. More expressive than FSM for 3+ actions. 📍 Used in: 4.4
FSM (Finite State Machine)
State ID → allowed transitions + pre-conditions. Simpler than BT but breaks with 3+ parallel states. 📍 Used in: 3.10
ECS (Entity-Component-System)
Composition over inheritance. Entity = ID, Component = data, System = logic. Cache-friendly. 📍 Used in: 4.2
A* (A-star)
Optimal shortest-path on grids. Uses heuristic (Manhattan/Euclidean). Open set + closed set. 📍 Used in: 4.3
JPS (Jump Point Search)
A* optimization. 2-10x faster on uniform-cost grids by skipping redundant neighbor expansion. 📍 Used in: 4.3
PWA (Progressive Web App)
App installable to home screen via manifest.json + service-worker.js. Offline-first. 📍 Used in: 1.10
CORS preflight
OPTIONS request browser fires before XHR with custom headers. Adds latency. Cache with Access-Control-Max-Age. 📍 Used in: 1.7
STUN/TURN
WebRTC helpers — STUN for NAT traversal, TURN for relay when symmetric NAT. Public: stun:stun.l.google.com:19302. 📍 Used in: 3.9
JWT (JSON Web Token)
Stateless auth token (header.payload.signature). HS256 (shared secret) or RS256 (public key). OAuth 2.0 uses Bearer JWT. 📍 Used in: 9.9
PKCE
Proof Key for Code Exchange — OAuth flow for SPAs without client_secret. code_verifier → code_challenge (SHA256). 📍 Used in: 9.9
ID token vs Access token
ID token = who you are (OIDC, JWT). Access token = what you can do (may not be JWT). Different lifetimes. 📍 Used in: 9.9
HSL (Hue Saturation Lightness)
Color format for easy theming. hsl(330 81% 60%) → shift hue to get theme variants. Better than hex for dark/light modes. 📍 Used in: 1.2, 2.1
WCAG contrast ratio
Accessibility — normal text needs 4.5:1, large text 3:1. Test via Chrome DevTools accessibility panel. 📍 Used in: 2.1
Touch target 44px
Apple HIG + Material guideline — buttons should be at least 44x44px for finger-friendly mobile UX. 📍 Used in: 2.7
prefers-reduced-motion
CSS media query for users who disabled animations — respect with @media to auto-disable transforms. 📍 Used in: 2.5
subtle-crypto
Web Crypto API for client-side encryption, HMAC, ECDSA. Web Crypto is more reliable than JS crypto libs. 📍 Used in: 1.4, 9.9
IndexedDB
Browser transactional DB. Async, MB-GB scale, indexes. Use over localStorage past 5MB. 📍 Used in: 1.4, 9.4
Stripe webhooks
Stripe POSTs to your backend on payment events. Verify via Stripe-Signature header + webhook secret. 📍 Used in: 10.9
BabylonGUI vs dat.gui
Babylon ships @babylonjs/gui built-in. dat.gui requires Three. UI overlays for debug controls. 📍 Used in: 3.1, 3.4
K6 / Artillery
HTTP load testing tools. K6 = Go (faster). Artillery = Node (friendlier). Use before launch. 📍 Used in: 8.7, 9.10
FTS5 (Full-Text Search)
SQLite built-in full-text index. Substring + MATCH queries, BM25 ranking. See codehub-api /api/search. 📍 Used in: 9.4
Ship.py pipeline
CodeHub’s 5-step deploy: edit → curl-verify → paramiko → API upload → /api/cache/refresh. Single command. 📍 Used in: 8.1
Multivariate Gaussian (vision)
Cutout Studio bg-remover uses ONNX model trained on multivariate Gaussian likelihood. Not Stable Diffusion. 📍 Used in: 7.3
FFT beat detection
Karaoke Studio computes FFT_SIZE=1024 windows, RMS peaks > max*0.55 AND > local_avg*1.3, skip < 0.25s apart. 📍 Used in: 6.7
HSL → hex conversion
hsl-to-hex: hsl in degrees, hex in 6-char RGB. CSS Color Module 4 (2023) supports color-mix() for derived colors. 📍 Used in: 2.1
OpenAI moderation free < 1M tokens/month
OpenAI moderation endpoint (om-moderation-latest) is free up to 1M tokens/month — use for safety filter before content reaches user. 📍 Used in: 7.10
RequestAnimationFrame (rAF)
Browser API for ~60fps rendering. Pause when tab inactive. Not setInterval! 📍 Used in: 1.6, 3.3, 4.1
Promise.all vs Promise.allSettled
Promise.all rejects on first error. Promise.allSettled waits for all regardless. Use allSettled for graceful degradation. 📍 Used in: 1.7, 7.4

🤖 AI Onboarding Kit v3.3

ทำให้ AI agent อื่นใช้ playbook นี้ได้ทันที — copy-paste ready, zero-shot context.

🧠 5 System Prompts 🎯 9 Decision Cards 📋 5 Code Patterns 🔌 3 AI Clients 📦 1 Bootstrap

📦 One-Command Bootstrap

Copy any of these into your AI agent to give it full CodeHub knowledge.

🐚 Bash one-liner

🐍 Python loader

🔌 MCP Server (Claude/Cursor/Continue)

🧠 System Prompt Templates

Drop these into your AI agent's system message. Each template includes full CodeHub context + rules.

Full context CodeHub Expert (Full Context) ~2.5K tokens

Full CodeHub Skills v3.2 context. Use when AI agent is building anything on CodeHub.

Specialized CodeHub Game Dev (Babylon + Phaser focus) ~1.5K tokens

Specialized for game dev. Covers engines, patterns, gameplay loops.

Specialized CodeHub Backend (FastAPI + DB) ~1.2K tokens

For API/server work. FastAPI + SQLAlchemy + SQLite/Postgres + Redis.

Specialized CodeHub AI Agent (OpenAI + RAG) ~1.4K tokens

For LLM-powered apps. Streaming, RAG, embeddings, function calling.

Specialized CodeHub Deploy (ship.py + nginx + SSL) ~900 tokens

For deployment work. One-command ship.py + nginx + Let's Encrypt.

📋 Master System Message (paste into any LLM)

One canonical system message that works for any AI agent. Click Copy:

🎯 Decision Matrix — "I want to build..."

Click any goal to see recommended sub-phases, path, example app, and time estimate.

🎮 I want to build a 2D game (platformer, RPG, shooter, tower defense)
🎯 **Phaser 3** (3.2) for arcade/2D

Phaser 3 has built-in Arcade + P2 + Matter physics, scene management, and is the de facto choice for 2D web games. Pattern used in street-fighter-thai, sky-ace, realm-of-heroes.

📚 Sub-phases: 3.2, 4.1, 4.3, 4.4, 4.6, 4.8, 4.9, 10.5
🎬 Example app: street-fighter-thai (Fighting), sky-ace (Shooter), realm-of-heroes (MMORPG)
📦 Bundle: ~50KB Phaser + 200 lines game logic
🚀 Deploy: ship.py + 1 HTML file, no build
⏱ Time: 1 week
🛤️ Path: build-2d-platformer
🏙️ I want to build a 3D game (city builder, RPG, sim, FPS)
🎯 **Babylon.js** (3.1) for 3D

Babylon has PBR + Havok physics + GUI + WebXR in one bundle. thinInstance for 10000 objects. Used in stronghold-3d (city), sims-lite (life sim), fps-cops-vs-robbers (FPS), mars-colony (strategy).

📚 Sub-phases: 3.1, 1.5, 1.9, 4.1, 4.4, 4.8, 4.9, 10.2
🎬 Example app: stronghold-3d (City), sims-lite (Life Sim), fps-cops-vs-robbers (FPS)
📦 Bundle: ~1.5MB Babylon + game logic
🚀 Deploy: ship.py + Babylon loaded from CDN
⏱ Time: 2-3 weeks
🛤️ Path: build-3d-city
💤 I want a simple 2D game (under 200 entities, particles, animations)
🎯 **Vanilla Canvas 2D** (3.3) + game loop (4.1)

For < 200 entities, vanilla Canvas 2D is faster than Phaser. Zero dependencies. Used in noodle-shop (tycoon).

📚 Sub-phases: 1.6, 3.3, 4.1
🎬 Example app: noodle-shop (Restaurant Tycoon)
📦 Bundle: Zero deps + 300-500 lines
🚀 Deploy: ship.py + 1 HTML file
⏱ Time: 3-5 days
🛤️ Path: build-tycoon
🚀 I want to ship a working MVP today
🎯 **HTML5 boilerplate** (1.1) + localStorage (1.4) + ship.py (8.1)

No build step, no npm, no framework. Single file deploys in 1 command. Perfect for demo + internal tool + quick prototype.

📚 Sub-phases: 1.1, 2.1, 2.2, 1.4, 1.10, 8.1
🎬 Example app: demo-coin-flip, click-counter (CodeHub starter apps)
📦 Bundle: Single 50KB HTML
🚀 Deploy: ship.py — 30 seconds
⏱ Time: 4-8 hours
🛤️ Path: build-mvp
📄 I want to build a SaaS app (CRUD + auth + payments)
🎯 **FastAPI** (9.2) + **SQLite/Postgres** (9.4/9.5) + **OAuth PKCE** (9.9)

FastAPI gives you Pydantic validation + async + auto docs. SQLite for single-tenant / Postgres for multi-tenant. OAuth 2.0 PKCE for SPA. Pattern in medbook, dinebook, pos-grocery.

📚 Sub-phases: 1.7, 5.4, 5.5, 5.6, 5.7, 9.1, 9.2, 9.3
🎬 Example app: medbook (Booking), pos-grocery (POS), dinebook (Restaurant)
📦 Bundle: ~5MB FastAPI + SQLAlchemy + dependencies
🚀 Deploy: systemd service + nginx reverse proxy
⏱ Time: 2-4 weeks
🛤️ Path: build-crud-saas
🤖 I want to build an AI chatbot or assistant
🎯 **OpenAI gpt-4o-mini** (7.1) + **Streaming SSE** (7.6) + **RAG** (7.8)

gpt-4o-mini is 15x cheaper than gpt-4o for 95% of use cases. Streaming reduces TTFB from 8s to 0.4s. RAG prevents hallucination with top-k=3-5 retrieval. Pattern in memo-ai, mavis-assistant.

📚 Sub-phases: 1.7, 7.1, 7.4, 7.5, 7.6, 7.7, 7.8, 7.9
🎬 Example app: memo-ai (Chat with memory), mavis-assistant (Function calling AI)
📦 Bundle: OpenAI API + ~100KB frontend
🚀 Deploy: Static SPA + API key in env
⏱ Time: 1-2 weeks
🛤️ Path: build-ai-chatbot
🎤 I want to build a lyric video / karaoke studio
🎯 **Web Audio** (1.5) + **Canvas animations** + **ffmpeg.wasm** (6.7)

Web Audio API + FFT beat detection + Canvas 2D lyric overlay + ffmpeg.wasm for MP4 export. Pattern in karaoke-studio v1.1 (11 themes, beat detection, MP4 export).

📚 Sub-phases: 1.5, 1.9, 6.1, 6.6, 6.7, 6.10
🎬 Example app: karaoke-studio (11 themes + beat + MP4 export)
📦 Bundle: ~30MB ffmpeg.wasm + ~50KB app
🚀 Deploy: ship.py + CDN ffmpeg
⏱ Time: 2-3 weeks
🛤️ Path: build-karaoke
🦍 I want to build an image editor / filter app
🎯 **Canvas 2D** (1.6) + **File API** (1.8) + **filters** (6.4)

Canvas 2D for crop/filter + File API for upload/download + Vision API (7.3) for AI auto-tagging. Pattern in fitcheck, palette, cutout-studio.

📚 Sub-phases: 1.6, 1.8, 6.4, 7.3, 5.8
🎬 Example app: fitcheck (3D model viewer), palette (Color extraction)
📦 Bundle: ~50KB app + Canvas API
🚀 Deploy: ship.py
⏱ Time: 1-2 weeks
🛤️ Path: build-image-tool
🚀 I have a working app, need to deploy it to a custom domain
🎯 **ship.py** (8.1) + **nginx** (8.2) + **Let's Encrypt** (8.3)

ship.py is the entire deploy pipeline used by all 73 CodeHub apps. One command: edit → curl-verify → paramiko → API upload → cache refresh. Pattern deployed 73+ apps.

📚 Sub-phases: 8.1, 8.2, 8.3, 8.4
🎬 Example app: 73 apps via ship.py (karaoke-studio, stronghold-3d, etc.)
📦 Bundle: ship.py (~150 lines)
🚀 Deploy: ship.py + nginx config
⏱ Time: 30 minutes
🛤️ Path: deploy-static-app

📋 Copy-Paste Code Patterns

Production-ready boilerplates used in real CodeHub apps. Each pattern is < 50 lines.

Babylon.js Scene Bootstrap 3.1 35 lines

Full Babylon.js scene with camera, lighting, ground, player

FastAPI + SQLite + Auth 9.2, 9.3, 9.4, 9.9 45 lines

REST API with FastAPI, SQLAlchemy, JWT auth, CRUD

PWA Setup (manifest + service worker) 1.10 30 lines

Make any web app installable + offline-capable

OpenAI Streaming Chat 7.1, 7.6 35 lines

Streaming OpenAI chat with abort control + error handling

Versioned localStorage with Migration 1.4 35 lines

Save/load with auto-migration across versions

🔌 Connect AI Clients (Claude Desktop / Cursor / VSCode)

Add CodeHub Skills as an MCP server in your AI editor. After setup, your AI can call search_subphases(), get_learning_path(), etc. directly.

Claude Desktop:
Edit ~/Library/Application Support/Claude/claude_desktop_config.json:
```json
{
  "mcpServers": {
    "codehub": {
      "command": "python3",
      "args": ["-m", "codehub_mcp_server"]
    }
  }
}
```
Cursor IDE:
Cursor Settings → MCP → Add new server:
- Name: codehub
- Command: python3 -m codehub_mcp_server
VSCode Continue:
Add to config.json:
```json
{
  "mcpServers": [{
    "name": "codehub",
    "command": "python3",
    "args": ["-m", "codehub_mcp_server"]
  }]
}
```

All 100 Sub-Phases

100 / 100

Click any sub-phase to expand its full pros/cons/gotchas analysis. Use search, tags, or matrix view to navigate.

Tags:
1.1 🌐 HTML5 Boilerplate Foundations Beginner ⏱ 5 min

Single-file HTML (CSS+JS inline). No build step, no npm, CDN-only deps. Fastest path to a working app.

🎯 When to use: ท최ดด 1000 line; prototype; demo; internal tool; offline-able; static hosting
✅ Pros
• Loads in 80-200ms with no dependency graph; nginx serves index.html in one TCP roundtrip
• Works file:// → no install needed for offline demos / training / fieldwork
• CDN-only deps = zero supply-chain npm risk (no postinstall scripts to audit)
• Debug = what user sees; Chrome DevTools shows identical HTML/CSS/JS, no source-map confusion
• Works on 10-year-old browsers that dropped React 16+ (e.g. Smart TV firmware)
⚠️ Cons
• DOM lookups O(n²) past 5k elements — use canvas or virtualize for tables/lists
• No bundled minification = 2-5x larger payload vs Webpack/Vite; ~150KB vs ~30KB
• Global scope pollution; each <script> shares window unless using IIFE or modules
• Manual asset versioning required (add ?v=Date.now() to bust iOS Safari's 7-day cache)
• No first-class TS = IDE type hints weak; refactor across 10 files = manual find/replace
⚡ Gotchas
• iOS Safari caches static HTML for 7 days even with Cache-Control: no-cache; append ?v=Date.now() to URL
• CSP blocks inline <script> unless you add 'unsafe-inline' or use external src=.js files
• type=module defers script loading — DOMContentLoaded fires before all modules imported
• CORS preflight (OPTIONS) fires on every XHR with custom headers; 5x latency hit per request
• Touch handlers need {passive:false} to call preventDefault() — default is passive:true in Chrome
• Async <script> executes after DOMContentLoaded without explicit await (use defer instead)
• Old Android (Stock browser) can choke on > 50 <script> tags; bundle to one file
• fetch() doesn't send cookies cross-origin without credentials: 'include' (default is omit)
🚫 Anti-patterns
• Inline event handlers (onclick=) in HTML — use addEventListener with delegation for 100+ listeners
• Mixing inline <style> with external CSS — specificity wars and no cache benefit
• Using document.write() inside async code — wipes the entire DOM tree
• Loading jQuery to do querySelectorAll — 30KB for what querySelector does in 1 line
🔀 Alternatives
• Vite + React (5x more deps, faster DX)
• Next.js (SSR + routing overkill for 5-page apps)
1.2 🎨 CSS Variables & Design Tokens Foundations Beginner ⏱ 30 min

:root variables (--accent, --bg, --text) for theme switching via data-theme. 1-line theme change.

🎯 When to use: multi-theme; white-label; design system; > 5 reusable components
✅ Pros
• Brand color change = 1 line of CSS (no JS, no rebuild, no React Context re-render)
• Cascade-aware: child components can override --accent without specificity war
• live-edit in Chrome DevTools updates entire UI immediately; no build step
• Theme persistence via single localStorage key + one documentElement.dataset write
⚠️ Cons
• IE 11 doesn't support (use PostCSS plugin if IE is required; rare in 2026)
• Specificity tied to element where var() is used — can't override in deep selectors without :where()
• Huge variable lists hurt readability — use namespaced vars like --color-text-primary, not --text1
• No compile-time checks — typo in var(--accnet) silently fails to var() fallback
⚡ Gotchas
• var() needs a fallback for missing tokens: var(--accent, #ec4899) — prevents flash of unstyled
• HSL > hex for theming — hue/lightness adjustable per-theme: hsl(240 10% 5%) → hsl(0 0% 98%)
• CSS @property registers vars with type so calc() and animation work (CSS Properties API)
• Cascade inheritance only for inherited properties — background-color needs explicit inheritance
• Theme flash on page load — inline a <script> in <head> setting data-theme BEFORE render to avoid FOUC
• Tokens nested in @layer have lower specificity; check ordering if your tokens don't apply
• Color-mix() in CSS Color Module 4 (2023) gives you derived tokens: --accent-fade: color-mix(in srgb, var(--accent) 30%, transparent)
🚫 Anti-patterns
• var() in @keyframes without @property — animations may not interpolate
• Using JS to mutate :root vars on every render — garbage collection churn, use class toggles
• Token names tied to values: --blue-500 — ties you to one brand; use --color-primary instead
• Mixing pixel and rem units in spacing tokens — visual rhythm broken across zoom levels
🔀 Alternatives
• Sass variables (compile-time, no runtime theme switch)
• Tailwind theme (utility class explosion, harder to debug)
1.3 🧩 Vanilla JS Module Pattern (IIFE) Foundations Intermediate ⏱ 15 min

IIFE returning public API. Encapsulates private vars without bundler. ESM alternative for legacy browsers.

🎯 When to use: single-file apps needing organization; legacy jQuery-era code; pre-ESM
✅ Pros
• Encapsulation without bundler — private vars hidden from window scope
• Familiar pattern from jQuery era — readable for devs transitioning from jQuery plugins
• Single-file deploy — 1 HTTP request for all logic vs 50 for ESM modules
⚠️ Cons
• Static structure — can't be reloaded or hot-swapped without state loss
• No tree-shaking — entire IIFE loads even if you use 1 method
• Method names exposed on public API can't be minified to 1 letter (defeats size optimization)
• Hard to test in isolation — everything is one closure
⚡ Gotchas
• Arrow function inherits lexical this — inside IIFE, this === window in non-strict, undefined in strict
• Private functions hidden from devtools — hard to inspect state mid-debug
• Use 'use strict' at top of IIFE to catch ReferenceError on assignment to undeclared vars
• Closures hold references — memory leaks if you store DOM nodes in long-lived IIFE
• Can't use import/export — use Revealing Module Pattern (assign all public methods to const obj = {...})
• Destructuring the public API binds references: const { init } = MyApp; — can't replace later
🚫 Anti-patterns
• IIFE returning object literal when you need polymorphism — use class
• Storing DOM references in IIFE closure — leaks when DOM removed; use WeakMap
• Hiding async functions behind sync API — caller doesn't know to await
• Mixing CommonJS require() with IIFE — module systems fight each other
🔀 Alternatives
• ESM (modern, tree-shakable)
• CommonJS (Node.js only)
• AMD (deprecated)
1.4 💾 localStorage Save/Load + Migration Foundations Intermediate ⏱ 1 hour

JSON.stringify + version field. Fallback chain: v3 → v2 → v1. Foundation for offline-first apps.

🎯 When to use: game saves; form drafts; user preferences; offline-first apps
✅ Pros
• Instant persistence — no async, no network; perfect for game saves and form drafts
• Zero latency reads — synchronous getItem() < 1ms vs IndexedDB async
• Free and abundant — 5-10MB per origin (Chrome), no auth, no server cost
• Survives reloads, browser restarts, OS updates — even when offline
⚠️ Cons
• 5-10MB limit per origin (Chrome) — 5MB warning at 5MB, 0KB after QuotaExceededError
• NOT encrypted — user can open DevTools and read all saves; never store PII/passwords
• Not synced across devices — user on iPhone won't see iPad save
• iOS Private Browsing = cleared when tab closes (no warning, silent data loss)
• Synchronous — blocks main thread; writing 1MB JSON freezes UI 50-200ms
⚡ Gotchas
• ALWAYS try/catch JSON.parse — corrupted data throws SyntaxError, uncaught = blank page
• Map, Set, Date, undefined, NaN, Infinity don't survive JSON.stringify — convert manually
• iOS Safari localStorage cleared in Private mode silently; detect via try/catch on setItem
• QuotaExceededError on save — clear old versions, then save again with retry
• Browser sync between tabs via 'storage' event — useful but different origin policies apply
• localStorage vs sessionStorage — lastCleared on tab close, but shared across same-tab windows
• Cache-Control headers don't affect localStorage — only affects HTTP cache
🚫 Anti-patterns
• Saving on every change without debounce — 100ms of work per keystroke kills performance
• Storing Date objects directly — they serialize to strings, not Date instances on load
• Catching SyntaxError and silently returning null — user loses save without warning
• Migration code that REQUIRES a specific old format — if you skip v1, v3 loaders break
🔀 Alternatives
• IndexedDB (async, MB-GB, structured queries)
• sessionStorage (per-tab, ephemeral)
• Cookies (HTTP-only, server-readable)
1.5 🔊 Web Audio API (Synthesized SFX) Foundations Intermediate ⏱ 2 hours

OscillatorNode + GainNode for procedural SFX without audio files. Tiny code, no CDN, real-time pitch.

🎯 When to use: เกุ่มต้องก춌 SFX < 100 distinct; load time < 1s; prototype; license-clean assets
✅ Pros
• Zero file load — sounds generated on-demand, instant playback, no CDN needed
• Real-time pitch/rhythm control — sweep, envelope, LFO without resampling
• License-free — no copyrighted sample packs, no .mp3 / .wav files in your bundle
• Tiny code — 50 lines replaces 10MB of .mp3 files; perfect for game jam prototypes
⚠️ Cons
• Synthetic = not realistic — can't produce pro-quality instruments, voice, drums
• iOS Safari AudioContext starts SUSPENDED — must resume() inside a click/tap handler first
• Polyphony capped at ~32 simultaneous voices on mobile — explosion SFX stack up and clip
• iOS audio plays through earpiece (not speaker) until user has a session of 7s+ — silent phones!
⚡ Gotchas
• AudioContext starts in 'suspended' state on iOS — resume() MUST be in user gesture handler (click)
• Always add envelope to prevent click/pop on note start: gain.linearRampToValueAtTime() from 0
• A4 = 440Hz, A5 = 880Hz — musical pitch uses A=440 reference; semitone = 2^(1/12) ratio
• createOscillator().connect(gain).connect(ctx.destination) — wrong connection = silent output
• Pre-create oscillators when sound is needed, .start() on trigger — don't create per-frame
• Webkit prefix still needed for older Safari: webkitAudioContext fallback
• Frequency below 20Hz is sub-bass; humans don't perceive direction — use stereo panning via StereoPannerNode
🚫 Anti-patterns
• Creating AudioContext per sound — destroys Chrome's audio thread; one context per page
• Using .start(0) without envelope — causes loud click/pop on every note
• Setting osc.frequency.value = 0 — undefined behavior; use exponentialRampToValueAtTime
• Looping sample-rate mismatch — buffer source for sampled, oscillator for synth
🔀 Alternatives
• Tone.js (heavier, music library, 100KB)
• Howler.js (file-based, 12KB)
• HTMLAudioElement (file-based, limited)
1.6 🧵 Canvas 2D Rendering Foundations Intermediate ⏱ 3 hours

Draw primitives + read pixels. ~60fps for ~10000 entities. Foundation for games, charts, image editors.

🎯 When to use: custom visualization; image editor; simple game; chart library
✅ Pros
• Native, no dependencies — every browser ships Canvas 2D since IE 9
• Pixel-level control — read pixels (getImageData) for image processing, color picking
• Image export — toBlob() converts to PNG/JPEG; canvas.toDataURL() for inline img
• 60fps for 10k entities on desktop; 1k entities on mobile; predictable perf
⚠️ Cons
• clear+redraw every frame — no scene graph; you manage state yourself
• No native hit-testing — implement spatial index (R-tree, grid) for click detection
• Text rendering weak — subpixel, kerning, RTL all janky vs DOM; use DOM overlays
• Memory spike on large canvases — 4K canvas = 64MB GPU RAM
⚡ Gotchas
• devicePixelRatio — canvas.width = displayWidth * dpr, then ctx.scale(dpr, dpr) for crispness on HiDPI
• ctx.save()/restore() stack — always pair them; leaking pushes is a common bug
• globalCompositeOperation='lighter' for glow/particles — add colors without darkening background
• ctx.measureText() before fillText — prevents layout thrash, lets you right-align text
• Image smoothing off for pixel art: ctx.imageSmoothingEnabled = false
• Touch events fired with same coordinates as mouse — use pointer events for unified handling
• OffscreenCanvas (2018+) lets you render in Web Worker — main thread stays at 60fps for UI
🚫 Anti-patterns
• Allocating new objects per frame (new Path2D(), new Image) — garbage collection stutter
• Calling fillText without ctx.font set — default font is 10px sans-serif, often tiny
• Using canvas as the only UI — a11y nightmare, can't select text or use screen readers
• Drawing at non-integer coordinates — causes blurry text/edges; use Math.floor()
🔀 Alternatives
• WebGL/WebGPU (3D, 10x more code)
• SVG (DOM-based, scales, slow > 1000 elements)
• PixiJS (WebGL wrapper, faster but +400KB)
1.7 📡 Fetch API + JSON + Error Handling Foundations Beginner ⏱ 30 min

Promise-based HTTP. Headers, body, params, 4xx/5xx, CORS, retry. Foundation for all REST APIs.

🎯 When to use: every FE app talking to REST API; config; analytics events; file uploads
✅ Pros
• Native — no jQuery $.ajax needed; works in all modern browsers and Node 18+
• Streaming via ReadableStream — stream OpenAI SSE, large file downloads without memory spike
• AbortController for cancellation — user navigates away → cancel pending requests
• Auto JSON parsing with one .json() call; auto text with .text()
⚠️ Cons
• Does NOT throw on 4xx/5xx — you must check res.ok and throw manually; common bug
• Body can only be read once — calling res.json() twice = error; cache the parsed result
• No built-in retry — implement exponential backoff yourself; many libs do this badly
• No request timeout by default — if server hangs, fetch() hangs forever; use AbortController
⚡ Gotchas
• AbortController for cancellation — setTimeout(ctrl.abort, 10000) for 10s timeout
• CORS preflight (OPTIONS) on custom headers — add 'Content-Type: application/json' triggers it
• Cache default is 'default' not 'no-store' — GET requests may return stale data; use cache: 'no-store'
• credentials: 'include' for cross-origin cookies — default is 'same-origin' (no cookies sent)
• POST body is FormData/Blob/JSON.stringify/URLSearchParams — each has different Content-Type
• fetch() rejects on network error, NOT on HTTP 4xx/5xx — always check res.ok first
• Response.json() throws on non-JSON — wrap in try/catch to fall back to .text()
🚫 Anti-patterns
• fetch('/api/x').then(r => r.json()) without checking r.ok — silently swallows 500 errors
• Setting Content-Type manually for FormData — browser sets it with boundary; manual = broken
• Catching all errors and returning null — user sees nothing; log + show error UI
• Retry without backoff — 1000 concurrent retries when server recovers = thundering herd
🔀 Alternatives
• Axios (40KB, interceptors, older API)
• ky (3KB, modern, retry built-in)
• XHR (legacy, sync mode, no streaming)
1.8 📁 File API (Upload/Download/Drag) Foundations Beginner ⏱ 30 min

Read file from input/drop, create Blob, download via invisible <a>. Foundation for editors, importers.

🎯 When to use: image/video editor; file converter; CSV import; drag-drop upload; offline tools
✅ Pros
• No server needed — process files 100% client-side; privacy guaranteed
• Instant preview — file -> blob URL -> <img> in 1ms, no upload roundtrip
• Drag-drop UX — native, no library; users expect it from desktop OS
• Read as text/buffer/stream — CSV.parse(text), binary parsing, streaming large files
⚠️ Cons
• RAM spike — file.arrayBuffer() loads entire file into memory; 100MB image = 100MB+ heap
• No chunk upload built-in — must slice manually: file.slice(start, end) for resume uploads
• Drag-drop on mobile = awkward — no hover state, no file path, only camera roll
• URL.createObjectURL leaks — must revokeObjectURL when done or memory grows over time
⚡ Gotchas
• URL.revokeObjectURL() after img.onload — otherwise blob stays in memory until tab close
• Drag-and-drop requires preventDefault() on BOTH dragover AND drop events — missing one = broken
• file.type is unreliable — 'image/jpeg' on .jpg but empty for files without extension
• file.arrayBuffer() vs file.stream() — arrayBuffer for small files, stream for > 100MB
• file.text() decodes as UTF-8 — notepad-saved Thai files might be TIS-620, need TextDecoder
• <input type='file' accept='.jpg,.png'> — still allows ALL files via OS dialog; validate file.type
• Download via <a download='filename'> + URL.createObjectURL(blob) — don't append to body
🚫 Anti-patterns
• Loading entire file as base64 dataURL — 33% larger, blocks main thread, no streaming
• Not revoking object URLs — memory leak per drag-drop; user opens 10 files = 10 blobs in RAM
• Trusting file.name for security — user can name malware.exe to myimage.jpg; check file.type
• Sync processing of large files — use Web Worker + OffscreenCanvas for > 5MB images
🔀 Alternatives
• Dropzone.js (50KB, more features)
• Uppy (200KB, resumable uploads)
• FormData + server (when you need to upload)
1.9 MediaRecorder API Foundations Advanced ⏱ 3 hours

Record MediaStream to webm blob. getUserMedia, getDisplayMedia, canvas.captureStream. Foundation for video editor + screen recorder.

🎯 When to use: video editor; screen recorder; karaoke export; webcam capture; audio transcription
✅ Pros
• Browser-native — no ffmpeg.wasm for simple WebM recording; smaller bundle
• GPU-accelerated — canvas.captureStream uses compositor; 4K@60fps on modern GPUs
• Multi-stream mix — audio + video + screen capture in 1 MediaStream; karaoke-studio uses this
• Codec choices — vp8/vp9/h264, opus/vorbis, depending on browser; pick by mimeType
⚠️ Cons
• WebM only on most browsers — Safari ships mp4 only since v14.1; mp4 is iOS-friendly
• Audio/video sync drift — mic + screen can drift 200ms+ over 5 min; resync in post
• RAM spike during long recordings — chunks pile up in JS array; stream to disk in worker
• No pause/resume API — must stop(), save, then start() a new recorder (joins are janky)
⚡ Gotchas
• Safari 16+ supports video/mp4 mimeType — use feature detection: MediaRecorder.isTypeSupported('video/mp4; codecs=avc1')
• canvas.captureStream(30) for 30fps — without param, captures only when canvas changes (bandwidth saver)
• Multi-track via MediaStream — add tracks together: new MediaStream([...video.getTracks(), ...audio.getTracks()])
• dataavailable fires per timeslice — use {timeslice: 1000} for 1s chunks; otherwise fires only on stop
• Stop on last dataavailable — recorder.requestData() then recorder.stop() ensures all chunks saved
• getDisplayMedia({audio:true}) for system audio — Chrome only; Firefox/Safari don't support yet
• Camera LED always on — cannot disable via JS (security); UX requires user consent prompt
🚫 Anti-patterns
• Recording to single blob — memory spike; use timeslice + chunks array
• Setting videoBitsPerSecond too high — 50Mbps on 720p = garbage quality; use 2-5 Mbps for screen
• Not awaiting all dataavailable events — stop() then immediately download() misses last chunk
• Recording with mic + no mute check — captures room noise; show visual feedback when recording
🔀 Alternatives
• WebCodecs API (low-level, 10x more code, more control)
• WebRTC (peer-to-peer, real-time)
• ffmpeg.wasm (cross-codec, +30MB bundle)
1.10 📱 Service Workers (PWA) Foundations Advanced ⏱ 1 day

Background script proxying network. Offline, installable, push notifications, background sync.

🎯 When to use: every app user reuses; offline required; installable; push notifications
✅ Pros
• Offline — cached resources work without network; perfect for travel/commute apps
• Installable — user can add to home screen; feels like native app, no app store
• Push notifications — re-engage users even when tab is closed (needs server-side push)
• Background sync — queue actions offline, replay when network returns
⚠️ Cons
• Dev experience tough — cache invalidation is the hard part; one wrong strategy = stale forever
• No DOM access — fetch + postMessage only; can't update UI from SW directly
• HTTPS required (except localhost) — extra setup; certbot for production
• First load requires network — SW only intercepts on subsequent visits unless you precache
⚡ Gotchas
• HTTPS required except localhost — use http://localhost for dev or self-signed for testing
• manifest.json + 192/512px icons required for installable — missing icon = no install prompt
• Change service-worker.js filename to force update — e.g. sw-v2.js; browser byte-checks content
• Cache-first for static assets, network-first for API, stale-while-revalidate for HTML — hybrid
• Update flow: skipWaiting() + clients.claim() to activate new SW immediately; otherwise old runs until all tabs close
• iOS Safari PWA support limited — no push, no background sync; cache works but no install badge
• SW scope is path-based — /sw.js only controls /pages/*; put SW at root for full control
🚫 Anti-patterns
• Cache-first for API responses — user sees stale data; use network-first with cache fallback
• Versioning SW by content hash, not path — hash changes force update but path-based is simpler
• Precaching 100MB of assets — first load takes 30s; precache only critical (< 5MB)
• Using SW for non-network things (localStorage) — no access; use IndexedDB or postMessage
🔀 Alternatives
• AppCache (deprecated 2016)
• Workbox (Google lib, 50KB, full toolbox)
• Native apps (App Store, no web discoverability)
2.1 🎨 Color Tokens (Semantic Palette) Design System Intermediate ⏱ 1 hour

Map visual colors (gray-500, blue-600) to semantic roles (text-primary, surface-raised) so theme switching and white-labeling become data-attribute changes.

🎯 When to use: Multi-theme product; white-label SaaS; design system spanning > 1 app; you need dark mode; brand refresh every 6 months
✅ Pros
• Theme switch = 1-line CSS change (`data-theme=dark`) — zero JS, zero rebuild;
• Word 'primary' survives a brand refresh where 'blue' would force a 200-file rename;
• WCAG contrast checks happen against semantic roles, not raw values — 1 audit covers every theme;
• White-label fork from 100 → 1 codebase: client picks theme JSON at deploy time;
• Designers and devs speak the same vocabulary in Figma Variables + code (token name parity);
• Accessibility audits become mechanical: scan token→pair pairs, not 5000 hex literals.
⚠️ Cons
• Indirection tax — juniors grep 'primary' and miss the actual value;
• Token overlap ('accent' vs 'brand') confuses teams if not enforced in lint;
• HSL/oklch math is required to derive shades from one base — wrong math = ugly ramps;
• Tailwind already does this — adopting tokens on top adds a layer you must maintain;
• Print/email fallbacks still need raw hex — tokens leak into non-browser contexts.
⚡ Gotchas
• Pure #000 on pure #fff hits WCAG AAA but feels harsh — use #0a0a14 / #f5f5f7 for softer contrast that still passes AA (4.5:1);
• HSL hue rotation for variant shades shifts color identity — use oklch or HSL with locked hue; rotate lightness only for a true ramp;
• Form controls (input, select, checkbox) ignore CSS background by default — explicit `accent-color` and `color-scheme: dark` are required;
• iOS Safari auto-applies blue/yellow tint to inputs in dark mode unless `color-scheme: dark` is set on `:root`;
• Images baked in light mode look like flashlights in dark — apply `filter: invert(1) hue-rotate(180deg)` to <img> or use SVG;
• Charts (Chart.js, D3) hardcode colors — read tokens via getComputedStyle, don't import CSS into JS;
• Transparent tokens (`--surface: hsl(... / 50%)`) need a separate `--surface-solid` for accessibility overlays or backgrounds stack visibly;
• Shadow tokens don't theme — shadows look wrong on dark bg unless `--shadow-color` is also a token (alpha-included).
🚫 Anti-patterns
• Naming tokens by color: `--blue-500` instead of `--color-primary-500` — they become useless the moment brand changes;
• Defining 50 shades when the ramp needs 5 (50/100/200/500/900) — every shade is a future decision you forgot;
• Storing tokens in JS only — sync drift between Figma and code; use CSS custom props or Style Dictionary;
• Using hex when HSL/oklch would let you derive variants with one knob (lightness);
• Skipping the 'danger/warning/info/success' family because 'the app never shows errors' — it will, in week 3.
🔀 Alternatives
• 1.2 (raw CSS variables without semantic naming)
• Tailwind config (built-in but JS-bound)
• Style Dictionary (multi-platform export)
2.2 🔤 Typography Scale (Modular) Design System Beginner ⏱ 30 min

Pick a ratio (1.250 / 1.333 / 1.414 / 1.5) and generate a type ramp xs→5xl with consistent visual rhythm — stops decision fatigue and survives redesigns.

🎯 When to use: UI with > 3 distinct text sizes; design system spanning multiple pages; need consistent hierarchy in marketing + product UI; brand guidelines exist
✅ Pros
• Visual hierarchy becomes automatic — designers can't accidentally pick a font-size that breaks the rhythm;
• Math-driven ratio means you can add a new size (text-6xl) by multiplying, not guessing;
• Tailwind/utility users get the ramp for free — text-sm/base/lg/xl/2xl matches the scale;
• Line-height couples to font-size (tight for display, loose for body) — readability stays correct at every step;
• Type tokens survive brand pivots: change ratio, regenerate ramp, ship in 30 min instead of 3 days;
• Accessibility audits key off font-size only — semantic tokens let a11y scripts verify 16px minimum automatically.
⚠️ Cons
• 1.25 (Major Third) feels tight at large sizes; 1.5 feels airy — one ratio doesn't serve display + body;
• Body must stay ≥ 16px or hit iOS auto-zoom on focus — ratios that start at 14px fail mobile;
• Variable fonts can collapse the ramp into one weight axis — adopting scales duplicates what's already there;
• Line-length (measure) is more important than size — a perfect scale with 200-char lines still fails reading tests;
• Asian/CJK fonts have wider footprints — Latin scale numbers look cramped on Thai/Chinese body text.
⚡ Gotchas
• Headings < 16px on mobile trigger iOS Safari auto-zoom on input focus — keep input text ≥ 16px even if h6 is smaller;
• rem scales with user browser font setting — a 1.5 ratio on a user-set 20px base ≠ the design; test with browser zoom 200%;
• font-family swap (FOUT) reflows text — reserve width with `size-adjust` + `ascent-override` @font-face descriptors;
• Variable font weight axis (wght) needs explicit `font-variation-settings: 'wght' 600` — `font-weight: 600` alone may not work;
• Letter-spacing tightens on display sizes, loosens on captions — apply per token (`--tracking-tight: -0.02em`);
• Korean/Thai have no ligatures — `font-feature-settings: 'liga' 1` enables complex shaping only where supported;
• Don't mix serif display + sans body without a reason — two families need 2× the work in variable font licensing;
• Tabular numerals (`font-variant-numeric: tabular-nums`) matter for prices, timers, tables — sans tables look jittery on resize.
🚫 Anti-patterns
• Using random pixel values (13px, 17px, 23px) — kills rhythm and confuses the next dev who has to extend it;
• Picking a scale ratio without testing display sizes — 1.25 makes text-6xl too small for hero headlines;
• Setting body < 16px on mobile — iOS Safari auto-zooms inputs and breaks layout;
• Mixing units (px + rem + em) in one scale — rem for everything is the 2024 default;
• Ignoring measure (line-length): 80 chars max for body, 30-40 for captions — perfect scale with bad measure still reads badly.
🔀 Alternatives
• Tailwind text-* utilities (built-in 1.25 ramp)
• Type Scale (type-scale.com) for visual math
• Utopia.fyi (fluid clamp() generator)
2.3 📏 Spacing Tokens (8pt Grid) Design System Beginner ⏱ 15 min

All margin/padding/gap values from a fixed scale (4/8/12/16/24/32/48/64/96) so every gap is divisible by 4 — pixel-snaps cleanly on retina and aligns across teams.

🎯 When to use: Multi-dev team; design system; pixel-perfect mockups in Figma; want pixel-perfect comparison to design files
✅ Pros
• All values divisible by 4 → pixel-snaps to whole pixels on 1x, 2x, 3x displays (no fuzzy 1.5px gaps);
• Dev ↔ designer parity: Figma '8pt grid' plugin produces the exact same token names as CSS variables;
• Conversion to rem (16px base) becomes trivial: --space-4:1rem; --space-8:2rem; user-zoom respects scale;
• Visual rhythm reads as intentional, not accidental — every gap has a reason because the scale is finite;
• Tailwind's 4/8/16/24/32/48 ramp matches this 1:1 — migrating tokens to utilities is zero-work;
• Catches rogue 13px / 17px / 23px paddings in code review — lint rule `margin: ! \d+(px)` rejects them.
⚠️ Cons
• 8px is too coarse for tight UI (button icon gap of 6px would round to 4 or 8 and feel wrong);
• Half-step tokens (--space-1: 4px, --space-2: 8px) still leak — some teams need 6px for icon-text baselines;
• Some components naturally break the grid (1px borders, 0.5px hairlines) — tokens don't replace those;
• Figma grids are 8pt but engineering reality includes iOS safe-area insets (34pt) that are off-grid;
• Adopting on top of an existing codebase means sed-replacing 1000+ literals — migration cost is real.
⚡ Gotchas
• Padding inside small buttons (< 32px tall) needs --space-2 (8px), not --space-4 (16px) — the scale serves size, not just visual rhythm;
• iOS safe-area-inset-* (notch, home indicator) returns 34pt / 21pt — off-grid by design; wrap in `env(safe-area-inset-bottom)` not raw token;
• border-radius follows its own scale (--radius-sm/md/lg/pill) — coupling it to spacing makes pills wrong (9999px ≠ 64);
• Stacked gaps with margin collapse: use `gap` on flex/grid (no collapse) instead of margin + padding pairs;
• Negative margins for overlapping layouts (`margin-left: -16px`) need negative tokens (`--space-neg-4: -1rem`) or raw values;
• Letter-spacing creates optical spacing — don't tighten scale to compensate for tracked text;
• Charts: bar gap ≠ card gap ≠ list-item gap — domain-specific scale (--chart-bar-gap: 4px) sometimes beats global;
• Vertical rhythm via `line-height: 1.5 * font-size` should hit grid — at 16px base, 24px line = --space-6, perfect alignment.
🚫 Anti-patterns
• Random 13/17/23 values — the whole point of the scale is to forbid these;
• One token per layout (`--hero-padding: 87px`) — bespoke values break the system;
• Mixing px and rem in one scale — pick rem (user-zoom-aware) and commit;
• Defining --space-3, --space-5, --space-7 to fill gaps — every gap is a decision you have to defend;
• Skipping radius/elevation tokens because 'spacing is enough' — those are separate scales that share the grid concept.
🔀 Alternatives
• Tailwind 4/8/16/24/32 default scale
• Material Design 4dp/8dp grid
• Bootstrap 5 spacing utilities
2.4 🧱 Component Library Design System Intermediate ⏱ 4 hours

Build Button, Card, Modal, Input, Toast, Chip, Tabs, Tooltip once with variants (size/state/elevation) and reuse — atoms become the team's shared vocabulary.

🎯 When to use: App > 3 pages; team ≥ 2 devs; any project that ships v2+; you find yourself copy-pasting the same button class
✅ Pros
• Write button once, use 1000× — accessibility (focus ring, ARIA, keyboard) lives in one place, not 47 places;
• Visual regression tests (Percy/Chromatic) lock one Button.png — every change is reviewed against baseline;
• Onboarding new devs: teach 8 components, they ship features; don't teach 800 CSS rules;
• Bug fix in one component fixes every screen — saves 10× the work a fragmented codebase costs;
• Storybook/Histoire dev mode gives QA/designers a sandbox without engineering deploying;
• Theme/variant via data-attributes (data-variant=danger, data-size=sm) — JS reads CSS, not the other way around.
⚠️ Cons
• Up-front cost: 4–8 hours per component is normal before any page uses it — feels unproductive;
• Over-abstraction: a 'universal' Form component that handles 12 use-cases becomes a 600-line monster nobody dares touch;
• Variant explosion: Button × 5 sizes × 5 variants × 3 states = 75 CSS permutations to maintain;
• Lock-in to choices made early (border vs shadow elevation) — pivoting is a 2-day rewrite, not a 1-hour swap;
• Component-only thinking misses composition — a Modal is fine but a Modal+Toast+Form might have layout bugs you never tested.
⚡ Gotchas
• Use native `<dialog>` for modals — gets focus trap, ESC-to-close, backdrop for free; `showModal()` + `::backdrop` CSS;
• Toast notifications need an aria-live region (`role=status` or `aria-live=polite`) — without it, screen readers miss them;
• Tabs use `role=tablist` + `role=tab` + `aria-selected` + arrow-key navigation — manual is 30 lines, library is 1 import;
• Inputs need explicit `<label for>` — placeholder-as-label fails accessibility audits and disappears on focus;
• Disabled buttons (`disabled`) lose focusability — for 'loading' states use `aria-busy=true` + visually disable but keep focusable;
• `<button>` vs `<a>`: use button for actions (no URL change), anchor for navigation (right-click → new tab works);
• Focus rings: never `outline: none` without replacement — `:focus-visible` ring with 2px solid + offset is the modern default;
• Shadow elevation: keep depth consistent — 1 layer of shadow + border beats 3 nested shadows that look muddy on retina.
🚫 Anti-patterns
• Component-per-pixel-figma: one component per mockup without extracting shared atoms (3 button styles become 3 components that should be 1 with variants);
• Wrapper-only components: `<Card><Card.Body><Card.Title>` when a plain `<div class=card>` with utility classes is enough;
• JS-driven everything: a button that needs JS to toggle a class is a `<details>` or checkbox under the hood;
• Inconsistent naming: Btn/Button/PrimaryButton in same codebase — pick one (Button is the React-y default) and lint;
• No prop API contract: component accepts 12 string props that should be 2 enums — over-flexibility is over-bug-surface.
🔀 Alternatives
• Headless UI / Radix (unstyled accessible primitives)
• shadcn/ui (copy-paste component recipes)
• Storybook (visual dev environment for any lib)
2.5 Animation Library (Easing + Duration Tokens) Design System Intermediate ⏱ 1 hour

Tokenize easing curves (ease-out for enter, ease-in for exit) and durations (100/200/300/500ms) so every motion across the app feels consistent and respects prefers-reduced-motion.

🎯 When to use: UI feels static; want premium feel; need visual feedback on hover/click; modals/sheets/toasts in/out
✅ Pros
• GPU-accelerated transforms (translate/scale/rotate) + opacity hit 60fps even on mid-range Android — width/height/margin animations don't;
• Tokens for duration/easing mean every component speaks the same motion language — no awkward 173ms transitions;
• `prefers-reduced-motion` media query is 1 line to disable — a11y audit passes, vestibular-disorder users are safe;
• Easing > duration for perceived quality: same 250ms with `cubic-bezier(0.16,1,0.3,1)` feels 2× more premium than `ease`;
• Web Animations API (WAAPI) lets you pause/seek/reverse without state libraries — `element.animate(keyframes, options)`;
• Reduced motion fallback (duration: 0.01ms) doesn't lose state — opacity:0 element still appears, just instantly.
⚠️ Cons
• Bouncy animations on primary CTAs delay action by 200ms — every delay is a UX tax on users in a hurry;
• backdrop-filter + transform combo on Safari can drop to 15fps — test on real iPhone, not just Mac Safari;
• Auto-playing animations on page load violate WCAG 2.2.2 — pause/play controls required for > 5s loops;
• CSS @keyframes can't be triggered programmatically without adding/removing classes — JS event → class → animation is brittle;
• Stagger animations (> 5 children with delays) pile up in compositor memory — laptops throttle after ~50 concurrent.
⚡ Gotchas
• Animate `transform` and `opacity` only — they run on the compositor; animating `top/left/width/height` triggers layout every frame;
• `will-change: transform` is a hint, not a guarantee — overuse creates new compositor layers that exhaust GPU memory (200MB+);
• `cubic-bezier(0.16, 1, 0.3, 1)` ('ease-out-expo') for enter, `cubic-bezier(0.7, 0, 0.84, 0)` ('ease-in-quart') for exit — symmetric ease looks mechanical;
• `prefers-reduced-motion: reduce` must set animation-duration to ~0.01ms (not 0 — animation events may not fire at 0);
• iOS Safari pauses CSS animations when tab is backgrounded — resume by listening to `visibilitychange` and re-applying class;
• Transform on SVG elements uses different origin than HTML — `transform-origin: center` works, but nested SVG transforms compound;
• Stagger via `transition-delay: calc(var(--i) * 50ms)` with `--i: 0..n` set inline — cleaner than nth-child arithmetic;
• Scroll-driven animations (CSS Scroll-Linked Animations, 2024+) require `animation-timeline: scroll()` — not yet in Safari stable.
🚫 Anti-patterns
• Animating `box-shadow` for hover effects — triggers paint; animate `background` color or use a pre-rendered shadow layer;
• Same duration for enter and exit (250ms in, 250ms out) — feels slower because user perceives exit first; use 200ms in / 150ms out;
• Long animation chains: button press → ripple → modal open → toast slide → page transition = 1.5s of motion before user sees content;
• Bounce/easing on text (letters wobble) — distracts from reading; reserve motion for spatial changes;
• No reduced-motion fallback — a vestibular-disorder user literally cannot use your app, and an a11y audit will fail.
🔀 Alternatives
• Framer Motion / Motion One (JS-driven, larger)
• GSAP (timeline-based, expensive license)
• Pure CSS @keyframes (zero-dep, what we recommend)
2.6 🎯 Icon System (Emoji + SVG) Design System Beginner ⏱ 15 min

Pick emoji for zero-byte speed or inline SVG for crisp controllable icons — never both in one UI; currentColor in SVG makes them themable with no extra classes.

🎯 When to use: Every UI; nav menus; status indicators; button labels; social/CTA icons
✅ Pros
• Emoji cost 0 bytes and inherit system font (no font-load blocking) — fastest possible icon;
• Inline SVG with `fill='currentColor'` or `stroke='currentColor'` inherits text color — theme switching just works;
• SVG scales to any size without raster artifacts; emoji at 96px+ looks pixely on some OS;
• SVG `<path>` data is text — grep-able, version-controllable, optimizable via SVGO;
• Single SVG sprite (1 HTTP request) reused via `<svg><use href=#icon-home>` beats 50 icon requests;
• Stroke-width consistency (1.5 / 2) across an icon family reads as designed-by-one-hand; mixing weights looks amateur.
⚠️ Cons
• Emoji render differently across OS — 🍑 on iOS ≠ Windows ≠ Android (color emoji vs text); brand suffers;
• Inline SVG bloats HTML — 30 icons inline = 30KB in DOM (sprite + use is 30KB once, then 100B per use);
• Emoji in <input> or <button> text aligns weirdly — `vertical-align: -0.1em` fixes most cases;
• Lucide/Phosphor add 50–200KB — for 10 icons, hand-rolled inline SVG is smaller;
• Emoji accessibility: screen readers read '🎉' as 'party popper' which is rarely the intent — wrap with `<span aria-label>`.
⚡ Gotchas
• Apple Color Emoji vs Segoe UI Emoji vs Noto Color Emoji — same codepoint renders 3 different glyphs; test on each;
• SVG `width=20 height=20` attributes are CSS-overridable but the SVG won't resize if it has a viewBox without preserveAspectRatio tweak;
• `<svg>` inside `<button>` is fine, but the SVG must have `aria-hidden=true` so screen readers don't announce 'image';
• Stroke alignment: 24×24 grid with 2px stroke at center means visual weight differs from 2px stroke on 16×16; scale icons together;
• Some emoji have hidden variation selectors (️ U+FE0F) that change text → emoji presentation — copy-paste from emoji keyboards, not codepoints;
• Animated SVG icons (`<animate>` element inside SVG) trigger compositor differently than CSS — sometimes jankier on iOS;
• Icon font ligatures (`:before { content: '\e001' }`) lose to inline SVG on accessibility — no text fallback if font fails;
• `<use href='#icon'>` requires CORS-friendly URLs for cross-origin SVGs — same-origin sprites work without headers.
🚫 Anti-patterns
• Mixing emoji + SVG in the same UI (🎵 for play but SVG for pause) — visual chaos; pick one and commit;
• Icon font (FontAwesome) for 5 icons when inline SVG is 1KB total — 80KB of font for 5 glyphs is wasteful;
• Random stroke widths (1px outline icon next to 3px filled icon) — pick a weight (1.5 / 2) and apply via stroke-width CSS var;
• Color-locking icons (`fill='#000'`) — theming breaks; use `fill='currentColor'` and let CSS cascade;
• Emoji-as-button: `👍` to 'like' is OS-dependent and screenshotted differently by every user — design a button, not an emoji vote.
🔀 Alternatives
• Lucide (1500+ SVG icons, tree-shakeable)
• Phosphor Icons (6 weights, flexible family)
• Heroicons (Tailwind-made, MIT)
• Material Symbols (Google, variable axes)
2.7 📱 Responsive Breakpoints (Mobile-first) Design System Intermediate ⏱ 1 hour

Define min-width breakpoints (640/768/1024/1280) and write styles for mobile, then layer on `min-width` overrides — desktop styles inherit unless contradicted.

🎯 When to use: Public-facing app; mobile traffic > 30%; SaaS that signs up users on phones; any B2C product
✅ Pros
• Mobile-first forces performance discipline — base CSS is what phones download; desktop gets progressive enhancement;
• min-width queries are easier to reason about: 'at 768px AND UP, show 2 columns' beats 'at < 1024px, show 1 column';
• Tailwind/Bootstrap defaults match this (sm/md/lg/xl/2xl) — no custom breakpoint debate;
• Container queries (2023+) extend this to components — `.card { @container (min-width: 400px) { flex-direction: row } }`;
• One CSS file works across 320px iPhone SE → 4K monitor — no UA sniffing, no separate m.example.com;
• Touch targets ≥ 44×44 px (Apple HIG) become a checklist item at mobile breakpoint, not an afterthought.
⚠️ Cons
• min-width cascades can hide desktop bugs — dev laptop at 1280px sees both, but a 1366px user sees overflow;
• Tablet portrait (768px) is awkward — too narrow for desktop, too wide for phone layout;
• Container queries need polyfill on iOS 15 (works in 16+) — feature-detect with @supports;
• Hover-only interactions (`:hover` reveals menu) break on touch — must pair with `:focus-within` or click;
• Bandwidth cost: base mobile CSS is small, but if you ship desktop images with `display:none` on mobile, they still download.
⚡ Gotchas
• Viewport meta tag: `<meta name=viewport content='width=device-width, initial-scale=1'>` — without it, mobile browsers render at 980px and zoom out;
• iOS Safari 100vh = URL bar height + bottom bar — use `100dvh` (dynamic viewport) or `min-height: calc(100vh - env(safe-area-inset-bottom))`;
• Touch target size: 44×44 px minimum (Apple) / 48×48 dp (Android) — buttons that LOOK 32px but have larger padding still fail if click area is small;
• `<input>` font-size < 16px triggers iOS auto-zoom on focus — even on a desktop viewport in mobile Safari;
• Landscape phone (568px height) breaks 100vh layouts — test both orientations, not just portrait;
• Landscape iPad split-view (320px / 1024px) needs container queries, not viewport media queries;
• Hover states on touch devices get 'sticky' (tap shows hover, doesn't dismiss until tap elsewhere) — `@media (hover: hover)` gates them;
• Save-data header (`@media (prefers-reduced-data: reduce)`) lets users opt out of heavy assets — serve 1× images instead of 2×;
• 1px CSS ≠ 1 device pixel — retina is 3× or 4×; use `image-set()` or `srcset` for bitmap assets.
🚫 Anti-patterns
• Desktop-first (max-width) queries — reverses the cascade and adds cognitive load;
• Custom breakpoints that don't match Tailwind/Bootstrap (sm=620 instead of 640) — engineers will guess wrong;
• Hiding content with `display:none` instead of `loading=lazy` + responsive images — wastes bandwidth on phones;
• Designing for one device (iPhone 14 Pro) — test on cheapest Android in your analytics top-5 countries;
• Forgetting landscape phone — 568px tall × 100vh header + footer + content = 0 scrollable pixels.
🔀 Alternatives
• Tailwind sm/md/lg/xl/2xl (utility-driven)
• Bootstrap breakpoints (5 standard steps)
• Container Queries (component-level, 2023+)
2.8 🌙 Dark Mode (prefers-color-scheme + Manual Toggle) Design System Intermediate ⏱ 1 hour

Read system preference via `prefers-color-scheme`, let user override via toggle, persist choice in localStorage — three sources of truth must be reconciled in order: explicit > saved > system.

🎯 When to use: Long-session apps (editor, dashboard, chat); users read in bed; OLED phones (battery savings); professional tool users expect it
✅ Pros
• OLED phones save 30–60% battery on true-black UI — measurable drain reduction;
• Reduces eye strain in low-light reading — users stay in app 25% longer (medium-published 2019 study);
• `prefers-color-scheme` respects OS-level accessibility settings — honors user choice automatically;
• Manual override + localStorage = 3 layers (system, saved, explicit) — UI always shows what user picked;
• Implementation is mostly CSS — `@media (prefers-color-scheme: dark) { :root { ... } }` works in all modern browsers;
• Dark mode often reveals bad contrast — building it forces an a11y audit you should have done anyway.
⚠️ Cons
• Every color must be defined twice (light + dark) — 2× design work, 2× QA surface, 2× screenshot tests;
• Chart libraries (Chart.js, D3) hardcode colors — read CSS vars at chart creation, but redraw on theme change is non-trivial;
• Images baked in light mode look wrong in dark — need `prefers-color-scheme` swap, filter inversion, or two assets;
• Form controls have OS-specific styles — `color-scheme: dark` is needed to make selects/inputs render dark on iOS;
• Pure #000 / pure #fff fail the 'looks right' test — true design requires off-black (#0a0a14) and off-white (#f5f5f7).
⚡ Gotchas
• `color-scheme: dark light` on `:root` makes form controls, scrollbars, and `<input type=date>` pick up dark — without it, iOS Safari shows white inputs on dark bg;
• Pure `#000` looks like a hole — use `hsl(240 10% 5%)` or `#0a0a14`; pure `#fff` is harsh on eyes — `#f5f5f7` is friendlier;
• Shadows are invisible on dark — switch to borders or colored glow (`box-shadow: 0 0 0 1px rgba(255,255,255,.1)`) for elevation in dark mode;
• Images: `<picture>` with `<source media=(prefers-color-scheme: dark) srcset=dark.png>` swaps assets; CSS `filter: invert(1) hue-rotate(180deg)` is hackier but 0 extra HTTP;
• System preference can change at runtime (OS theme switch) — listen with `matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ...)`;
• Persist explicit user choice in localStorage with key `theme`; resolve order: localStorage > system > fallback light;
• Flash of wrong theme (FOWT) on page load — inline a tiny `<script>` in `<head>` that sets `data-theme` before CSS parses;
• Charts using canvas need to be redrawn when theme changes — listen for theme-change custom event and call chart.update();
• Code blocks (syntax highlighting) need a separate dark theme — Shiki/Prism both ship light + dark, switch via attribute selector.
🚫 Anti-patterns
• Auto-detect only (no manual toggle) — users on shared computers can't override; respect their preference;
• Using pure #000 — looks like a void; use off-black for depth;
• Two completely different palettes (light = blue, dark = orange) — defeats the purpose of theming; keep semantic roles consistent;
• Forgetting to test form controls — iOS Safari defaults to white inputs on dark bg unless `color-scheme: dark`;
• Persisting in sessionStorage — survives the session, vanishes on tab close; localStorage is the right tier for theme.
🔀 Alternatives
• next-themes (React)
• Theme UI (JS-driven theming)
• Manual CSS-only (what we recommend for vanilla projects)
2.9 💎 Glassmorphism (Frosted Glass Overlays) Design System Beginner ⏱ 20 min

Use `backdrop-filter: blur()` + semi-transparent background + 1px white border for premium frosted-glass panels that hint at content behind — a Material/iOS design staple.

🎯 When to use: Overlays on busy backgrounds (3D scene, video, photo); iOS-style sheets/modals; hero sections with content underneath; premium product positioning
✅ Pros
• Implies depth/hierarchy without literal shadows — UI reads as layered, modern, intentional;
• `backdrop-filter: blur()` runs on GPU (compositor layer) — keeps main thread free for app logic;
• Border highlight (1px rgba(255,255,255,.18)) sells the glass illusion more than blur radius does;
• Native iOS / macOS use this exact pattern — users have trained visual literacy;
• Content behind tells users 'there's more here' — better than opaque modals that trap focus;
• Pairs naturally with dark mode (10% white over dark = subtle, premium feel).
⚠️ Cons
• Performance: `backdrop-filter` triggers a new compositor layer per element — 20 glass panels = 20 layers, GPU memory spikes;
• Low-end Android (pre-2020 Mali GPUs) can drop to 5–10 fps when 5+ glass elements are visible;
• Contrast nightmare: white-on-light-photo text disappears; always pair with text-shadow or text on a separate dark band;
• Safari needs `-webkit-backdrop-filter` AND modern syntax — older Safari (pre-18) renders wrong;
• Print styles: `backdrop-filter` is invisible in PDF/print — explicit fallback bg color required for `@media print`.
⚡ Gotchas
• Always declare `-webkit-backdrop-filter` before `backdrop-filter` — Safari < 18 ignores unprefixed; `-moz` was never needed;
• Blur radius sweet spot is 8–16px; > 24px looks fake (gaussian stack overflows) and tanks perf;
• Background opacity must be 5–15% on dark bg, 40–60% on light bg — too transparent = invisible, too opaque = not glass;
• Add `border: 1px solid rgba(255,255,255,.18)` — without it, glass edge looks ragged against varying backgrounds;
• iOS notch + safe-area: glass sheet must include `padding-bottom: env(safe-area-inset-bottom)` or home indicator overlaps;
• `backdrop-filter` doesn't blur elements ABOVE the glass — only those below in stacking order;
• Detect low-end devices: `@media (prefers-reduced-transparency: reduce) { .glass { backdrop-filter: none; background: var(--color-surface); } }` (Safari);
• Inside a transformed parent (`transform: translateZ(0)`), backdrop-filter can fail entirely — promote to its own layer;
🚫 Anti-patterns
• Glass over glass (nested panels) — multiple compositor layers stack and slow to single-digit fps;
• Glass containing dense text without a contrast layer — a11y fails on WCAG 1.4.3 (4.5:1);
• Blur radius 40+ — looks like an Instagram filter, not a design choice;
• Mixing glass with flat opaque panels in same UI — visual inconsistency, looks accidental;
• Applying glass to entire body bg — wipes GPU memory on first paint, scroll stutters for 3 seconds.
🔀 Alternatives
• Solid card with shadow (cheaper, more accessible)
• Frosted image (manual gaussian via filter:blur)
• Native iOS UIVisualEffectView (Apple-only)
2.10 📐 Layout Primitives (Grid, Flex, Stack, Cluster) Design System Beginner ⏱ 1 hour

Compose layouts from 4 named primitives — Grid (2D), Flex (1D), Stack (vertical w/ consistent gap), Cluster (horizontal wrap) — instead of inventing per-component magic numbers.

🎯 When to use: Every layout; any time you're tempted to write `position: absolute` to align two things; you want RTL support for free
✅ Pros
• Grid for 2D, Flex for 1D — picking the right one upfront saves hours of `justify-content: center` guessing;
• `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` is responsive without a single media query — 1 to N columns automatically;
• Stack primitive (`> * + * { margin-top: var(--gap) }`) gives vertical rhythm with zero magic numbers;
• Container queries (`@container`) make components responsive to their parent, not viewport — cards in sidebars vs main column both work;
• Grid + flex are RTL-ready by default (`direction: rtl` flips everything) — no `transform: scaleX(-1)` hacks;
• aspect-ratio (2021+) eliminates padding-bottom hack for 16:9 / 1:1 / 4:3 containers — `aspect-ratio: 16/9;` and done.
⚠️ Cons
• Grid + flex syntax has a learning curve — `place-items: center` vs `justify-items + align-items` confuses new devs;
• Container queries (2023+) need fallback for older Safari — `@supports (container-type: inline-size)` gates the rule;
• Subgrid (Firefox-first, partial Chrome/Safari 2024+) isn't universal — nested grids can't share tracks reliably yet;
• Stack via `> * + * { margin-top }` doesn't work with pseudo-elements that already have margin — manual override needed;
• IE 11 doesn't support grid at all — irrelevant for modern apps, but legacy enterprise clients will break.
⚡ Gotchas
• `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` collapses empty tracks — use `auto-fill` if you want fixed columns even when empty;
• Flex `gap` (2021+) replaces margin-between-children — but `gap` on flexbox needs Chromium 84+, Safari 14.1+;
• `place-items: center` is shorthand for `align-items + justify-items` — saves 14 chars, reads better;
• Container queries need `container-type: inline-size` on parent + `@container (min-width: 400px)` on child; without the type declaration, nothing works;
• Stack via `> * + * { margin-top }` only applies between adjacent children — pseudo-elements like `::before` still get the margin if they exist;
• `min-height: 100dvh` (dynamic viewport) handles mobile URL-bar collapse better than `100vh` — supported in iOS Safari 15.4+;
• `aspect-ratio` overrides `width`/`height` if both are set — set width:100% and aspect-ratio, not width+height;
• Negative margins on grid items can leak outside the container — use `overflow: hidden` on parent or grid gaps > 0;
• `place-content: center` centers the entire grid within its container (different from `place-items` which centers items within their cells).
🚫 Anti-patterns
• Float-based layouts (legacy `float: left`) — never; flex/grid subsume every float use case;
• `position: absolute` for centering — flex `place-items: center` is 1 line and respects content size;
• Nested flex inside flex inside flex (> 4 deep) — refactor to grid or extract a primitive;
• Magic numbers (`margin-top: 17px` 'to make it line up') — every magic number is a future bug;
• Setting both `width` and `height` on an image inside flex — use `object-fit: cover` and let aspect-ratio handle the container.
🔀 Alternatives
• Flexbox (1D only)
• CSS Grid (2D, what we use)
• Container Queries (component-responsive)
• Subgrid (Firefox-first, partial)
• CSS `display: contents` (deprecated, accessibility risk)
3.1 🧊 Babylon.js (Full-Stack 3D Engine) Game Engines Advanced ⏱ 1 week

Full 3D engine — PBR + Havok physics + particles + GUI + WebXR — 1.5MB dep replaces 4 Three addons.

🎯 When to use: 3D city builder (stronghold-3d); NPC sim (sims-lite); FPS (fps-cops-vs-robbers); MMORPG (realm-of-heroes); Mars strategy (mars-colony); idle RPG (sj88-rpg-idle-3d)
✅ Pros
• PBR + Havok + particle editor + GUI + post-FX in 1.5MB → zero glue vs Three where you'd compose 3 addons
• Playground at playground.babylonjs.com = live-edit IDE → share bug-repro URL in 30s (no JSFiddle setup)
• thinInstanceAdd draws 10000 identical cubes in 1 draw call → stronghold-3d tiles 100x100 grid at 60fps
• WebXR first-class — fps-cops-vs-robbers ships VR mode in 50 lines vs Three's @three/xr + manual session glue
• ArcRotateCamera + Inspector baked-in → no need to wire OrbitControls or dat.gui separately
• v8 (2024) ships Havok IK for character animation → no three-stdlib AnimationMixer ladder
⚠️ Cons
• 1.5MB+ minified ≈ 10x Three → mobile first-paint hurts if not code-split per route
• API surface ~5x Three → ramp-up 1 week vs 3 days; niche questions get fewer SO answers
• PBR + shadows = thermal throttling on iPhone 11 / low-end Android → must swap to StandardMaterial
• Inspector = +200KB; forgetting tree-shake ships it in prod and slows boot 300ms
• BabylonGUI 2D ≠ DOM → no screen-reader / IME for Thai-Arabic; mix HTML overlay for inputs
• Havok v2 = WASM 3MB lazy-loaded → first physics step shows 100ms hitch unless preloaded
⚡ Gotchas
• freezeWorldMatrix() after placing static meshes — without it Babylon recomputes world transforms each frame costing 5-15ms on 1000-mesh scenes (stronghold-3d lesson)
• thinInstanceAdd (NOT createInstance) for > 100 identical objects — instance API spawns N draw calls, thin packs into 1 buffer + matrix refresh
• ArcRotateCamera alpha/beta are radians; setting alpha=Math.PI flips 180° not 360° — use lerp(t,0,1) for smooth tween
• PBR materials need .env cubemap — without it metallic surfaces render black; load via CubeTexture from .dds
• Havok WASM needs await HavokPhysics() before first PhysicsAggregate — otherwise physics silently no-ops
• PointerEvent fires on both canvas AND DOM HUD → wire scene.onPointerObservable to avoid double-input in realm-of-heroes
• Calling scene.render() manually inside runRenderLoop = double-render → 30fps instead of 60fps; let engine own the loop
• iOS Safari drops WebGL context after 30min background → listen to engine.onContextLostObservable and rebuild scene
🚫 Anti-patterns
• DON'T `import * as BABYLON` in TS — use named imports `import { Engine, Scene } from '@babylonjs/core'` tree-shakes ~30%
• DON'T allocate Mesh per bullet — pool 50 meshes; stronghold-3d reuses them across 10k spawns
• DON'T run physics in JS thread for > 200 bodies — enable Havok WASM (sims-lite NPC crowds)
• DON'T use StandardMaterial outdoor — PBR + IBL only; Standard gives plastic look in sunlight
• DON'T skip scene.skipPointerMovePicking = true with 1000+ pickables — picking = 5ms hit-test per frame
🔀 Alternatives
• 3.4 Three.js (smaller, no physics)
• 3.7 WebGL (raw GPU)
• 3.8 WebGPU (next-gen)
3.2 🥊 Phaser 3 (2D Game Engine) Game Engines Intermediate ⏱ 3 days

Comprehensive 2D engine — sprites, Arcade/P2/Matter physics, scene lifecycle, animations, tilemaps, sound — 800KB.

🎯 When to use: 2D fighter (street-fighter-thai); 2D shooter (sky-ace); tower defense; platformer; RPG (realm-of-heroes top-down); mobile canvas game
✅ Pros
• 3 physics engines (Arcade/P2/Matter) ship built-in → Arcade for platformers, Matter for ragdoll — no addon hunt
• Scene lifecycle (init/preload/create/update) forces clean state separation → realm-of-heroes uses MenuScene/GameScene/UIScene stack
• First-class TypeScript definitions → IDE autocomplete vs Phaser 2 JS-only — saves 1 hour per session
• WebGL + Canvas renderers auto-fallback → iOS Safari 14 with WebGL bug falls back to Canvas without code change
• Animation editor exports JSON → designer-friendly; Phaser parses sprite-sheet atlas (TexturePacker / Aseprite) in 3 lines
• Camera effects (shake/flash/fade) + Tween manager built-in → no GSAP dep for game juice
⚠️ Cons
• 800KB minified ≈ 8x PixiJS → hurts mobile LCP; use Phaser MINIMAL build to drop to 400KB
• API has 3 layers (Game/Scene/GameObject) → junior dev writes boilerplate; Arc/sprite confusion
• Arcade physics is AABB-only → circles/rotated bodies need P2 or Matter (3x CPU cost)
• No built-in 9-slice — must use third-party plugin or roll your own (Pixel-Game dev curse)
• Matter.js has 200KB weight — sky-ace ships Matter only for ragdoll debris, Arcade for gameplay
• No first-class module system before v3.60 — code-split per scene requires import map or bundler hacks
⚡ Gotchas
• Call this.physics.add.collider(player, layer) AFTER physics.world.enable() in create() — setup order matters; sky-ace silent fall-through if reversed
• Scene shutdown must call this.events.off() on listeners → otherwise memory leak + double-fire after scene restart
• Scale.FIT + parent centered div causes 1px black bar on iOS notch — use Scale.RESIZE + safe-area-inset CSS
• TexturePacker atlas needs fixedSize:false + padding:2 → otherwise Phaser bleed artifacts in sky-ace tileset
• Phaser.Loader.LOCKING = false in create() OR preload() blocks forever on slow CDN — set timeout: 15000
• iOS Safari blocks AudioContext until user gesture → street-fighter-thai unlocks sound on first tap in MainScene.create
• Matter debug graphics = +20% CPU — enable only in dev (Phaser config `physics.matter.debug: false` in prod)
• Tilemap collision misses if tileset imported with wrong pixel scale — match Tiled's tilewidth to texture frame size exactly
🚫 Anti-patterns
• DON'T update in render() callback — use update(time, delta) for frame-time-independent physics (street-fighter-thai rule)
• DON'T load assets in create() — use preload() + Loader; otherwise first-frame flickers with missing textures
• DON'T put DOM <button> over Phaser canvas for menu — use Phaser text objects OR full DOM scene with Phaser.Game destroyed
• DON'T allocate new Phaser.Game per scene transition — single Game instance with scene.start() is 5x faster
• DON'T use Matter for platformer — Arcade AABB is 10x faster and 95% sufficient (sky-ace precedent)
🔀 Alternatives
• 3.3 Vanilla Canvas (no dep)
• 3.6 PixiJS (render-only, faster)
• 3.5 Kaboom (simpler)
3.3 📦 Vanilla Canvas 2D (Zero-Dep 2D) Game Engines Intermediate ⏱ 2 days

Pure Canvas 2D API + manual game loop. 0KB dep. Best for <200 entities with simple physics (tycoon, particles, UI tools).

🎯 When to use: Tycoon (noodle-shop customer queue); particle effects; data viz; simple click-game < 200 entities; tools (palette color picker); tile-map RPG under 50x50
✅ Pros
• 0KB dep → loads in <50ms on slow 3G; ideal for CodeHub static landing widgets
• Full control of draw order/blend modes/transforms — no engine abstraction tax
• Debug = what user sees; no source-map or framework error noise in console (noodle-shop dev speed)
• Works file:// → offline / Codepen / jsFiddle without install — pasta-shop demo runs from USB
• Canvas 2D matured in all browsers — even iOS Safari 13+ supports Path2D, OffscreenCanvas
• No vendor lock-in; HTML/CSS overlay for HUD = free a11y + IME + screen-reader for Thai
⚠️ Cons
• DOM-free a11y → screen readers miss canvas content; need offscreen <canvas> mirrored to DOM (noodle-shop tax)
• No scene mgmt → 1000-line games become spaghetti; manual update/render dispatch
• Canvas 2D < WebGL perf at > 500 draw calls/frame; drawImage with rotation = 0.3ms each on mid-mobile
• Manual asset versioning; cache bust with ?v=Date.now() to defeat iOS Safari 7-day HTML cache
• No spatial index — collision check O(n²) past 200 entities; need quadtree or grid broadphase
• devicePixelRatio must scale canvas resolution — without it retina = blurry (most common bug)
⚡ Gotchas
• Set canvas.width = w * devicePixelRatio AND ctx.scale(dpr, dpr) — otherwise retina blurry
• requestAnimationFrame NOT setInterval — rAF pauses when tab backgrounded (saves battery + matches display Hz)
• Clear with ctx.clearRect(0,0,w,h) NOT ctx.fillRect(white) — clearRect is 3x faster on iOS Safari
• ctx.save()/restore() pairing inside a draw call must balance — otherwise transform stack corrupts (noodle-shop lesson)
• ctx.measureText() returns cached width — call once at boot for HUD text, not every frame (tycoon dashboard)
• Touch events need {passive:false} to call e.preventDefault() → without it scroll fights drag
• OffscreenCanvas.transferToImageBitmap() for worker rendering — Chrome only but 10x faster for image processing
• Hit-test for clicks: track mouse pos + button-down + entity list — never use canvas's hit-region API (Safari buggy)
🚫 Anti-patterns
• DON'T draw all entities every frame without dirty-rect tracking — re-use offscreen canvas for static layers (noodle-shop kitchen tiles)
• DON'T put game state in DOM <input> hidden fields — use plain JS object + localStorage; DOM read/write is 10x slower per frame
• DON'T call canvas.toDataURL() per frame — it stalls 50ms; only call on save event
• DON'T use ctx.drawImage with imageElement cached wrong — pre-decode via img.decode() promise before first draw
• DON'T ignore ResizeObserver → canvas keeps stale size on phone rotate; obs + resize handler fixes 90% of mobile reports
🔀 Alternatives
• 3.2 Phaser (scenes + physics)
• 3.6 PixiJS (WebGL perf)
• 3.5 Kaboom (component)
3.4 🌐 Three.js (Lightweight 3D Renderer) Game Engines Intermediate ⏱ 3 days

Smallest mainstream 3D renderer (~150KB) — declarative scene graph + WebGL/WebGPU backends — focused on rendering, NOT game features.

🎯 When to use: 3D model viewer (fitcheck — clothes try-on); product configurator; data viz (3D bar chart); AR/VR preview; minimalist portfolio 3D; CSS3DRenderer hybrid UI
✅ Pros
• ~150KB core (~10x smaller than Babylon) → perfect for fitcheck embed + product configurator first-paint
• Declarative scene graph → `scene.add(mesh)` reads like document.appendChild; junior-friendly
• Massive ecosystem: @three/xr, three-stdlib (OrbitControls, GLTFLoader, EffectComposer) — Babylon equivalent needs custom code
• r150+ ships WebGPURenderer flag — switch to GPU when navigator.gpu exists, fallback WebGL automatically (fitcheck pattern)
• BufferGeometry is the only API (Geometry deprecated in r125) — flat Float32Array is cache-friendly and serializable
• Examples at threejs.org cover 90% of use cases — copy-paste + tweak faster than Babylon docs
⚠️ Cons
• No physics, no GUI, no particles in core → must compose 3+ addons (Cannon-es + lil-gui + tsparticles) = 400KB total
• Materials are thin wrappers over GLSL — PBR with env map = 20 lines setup; Babylon is 3 lines
• WebXR requires @three/xr with manual session.requestSession + frame loop glue — Babylon does it in 1 helper
• Inspector = external dep (three-inspect) not built-in — debugging prod scene = print transforms to console
• Mobile post-processing is brutal — EffectComposer + Bloom tanks to 20fps on iPhone 11; bake into textures
• TypeScript types are community-maintained — some addon .d.ts files lag releases by weeks
⚡ Gotchas
• renderer.setPixelRatio(Math.min(devicePixelRatio, 2)) — without cap, retina = 4x GPU fillrate = 15fps mobile
• Use BufferGeometry NOT Geometry — Geometry was removed in r125; migrate legacy code immediately
• renderer.outputColorSpace = THREE.SRGBColorSpace (r152+) — without it PBR textures wash out / too bright (fitcheck bug)
• renderer.toneMapping = THREE.ACESFilmicToneMapping for PBR — default linear looks chalky
• GLTFLoader needs DRACO + KTX2 loaders configured OR .glb inflates 10x — fitcheck preloads DRACO wasm
• Dispose resources explicitly: geometry.dispose(), material.dispose(), texture.dispose() — otherwise GPU mem leaks 5MB/min
• WebGLRenderer + ShaderMaterial: don't forget to set uniforms.needsUpdate = true — silent no-op is common
• Resize handler must update camera.aspect + renderer.setSize — otherwise stretched on rotate
🚫 Anti-patterns
• DON'T use MeshBasicMaterial for outdoor scene — no lighting; use MeshStandardMaterial or MeshPhysicalMaterial (fitcheck rule)
• DON'T allocate new Vector3() inside render loop — reuse pool of 8-10 vectors; GC pauses cause 5-10ms hitches
• DON'T load multiple GLTFs sequentially — use GLTFLoader.loadAsync in parallel Promise.all (fitcheck boots 6 models in 2s not 12s)
• DON'T trust Euler rotation order — default 'XYZ' vs 'YXZ' gives gimbal-lock; set explicitly: rotation.order = 'YXZ' for FPS-style cameras
• DON'T skip OrbitControls.update() in render loop — damping breaks when not called each frame (fitcheck lesson)
🔀 Alternatives
• 3.1 Babylon (batteries-included)
• 3.7 WebGL (raw)
• 3.8 WebGPU (next-gen)
3.5 🕹️ Kaboom.js / Kaplay (Arcade 2D) Game Engines Beginner ⏱ 4 hours

~80KB component-based arcade 2D — add([rect(), pos(), body()]) chain syntax — game-jam-grade simplicity.

🎯 When to use: Game jam 48h prototypes; tiny arcade (Pong/Snake/Mario-clone under 200 LOC); kids' coding lesson (codekids-thai); quick landing-page game
✅ Pros
• ~80KB minified ≈ 1/10 Phaser → instant load on mobile 3G; perfect for codekids-thai lesson embed
• Component chain syntax: add([rect(50,50), pos(50,50), color(0,255,0), body(), area(), 'player']) reads like a sentence
• Built-in scene() with key/state pattern → 10-line platformer prototype vs 100 in Phaser
• Zero-config input: keyDown('left', () => ...) → keyboard + touch auto-detected (codekids tablets)
• Built-in onUpdate/onDraw lifecycle → no manual requestAnimationFrame management
• Re-exported as `kaplay` since 2024 — old `kaboom` still works via shim but migrate to kaplay()
⚠️ Cons
• Kaplay (formerly Kaboom) had 2 API breaking changes in 18 months → upgrade tax every quarter
• No first-class save system → must roll localStorage wrapper; no migration framework
• Performance modest past 100 entities — no spatial hash, no GPU batching beyond default sprite atlas
• TypeScript support is partial — many component types are `any`; IDE autocomplete weak (codekids friction)
• Community is shrinking vs Phaser — fewer StackOverflow answers, fewer plugins, fewer devs hiring for it
• No mobile build pipeline (Cordova/Capacitor) maintained — fine for web, dead for native
⚡ Gotchas
• Use `import kaplay from 'kaplay'` NOT `kaboom` — Kaboom renamed 2024, old package is deprecated
• Component order matters in add([...]) — body() before area() throws 'area not attached'
• loadSprite('player', 'p.png') must happen inside a scene's onLoad — not at module top-level
• onUpdate() callback receives deltaTime but it's already dt-multiplied in v3000+ — don't multiply again (silent 2x speed)
• Touch input on iPad needs touchToMouse polyfill OR scene('main', () => { onTouchStart(... ) }) — auto-detect misses some
• Pos components store x/y as numbers — for big worlds use camPos()/camScale() for camera, don't translate every entity
• outline() component needs a parent color() — without it outline default color is black on black = invisible
• debug.inspect = true in dev shows entity tree but adds 5% CPU — disable in prod
🚫 Anti-patterns
• DON'T use Kaboom for anything beyond 2 weeks of dev — the API churn + community decay will burn you at v3 update (codekids avoided)
• DON'T nest scenes 3+ deep — Kaplay's scene stack uses global state; bugs at depth 3+ are untraceable
• DON'T store save state in kaboom's getData()/setData() — those are session-scoped; use localStorage wrapper
• DON'T use kaboom() global init in module — import kaplay and call const k = kaplay({...}) for testability
• DON'T mix Kaboom + Phaser — they fight over canvas/context; pick one
🔀 Alternatives
• 3.2 Phaser (production)
• 3.3 Vanilla Canvas (no dep)
• 3.6 PixiJS (render-only)
3.6 🖼️ PixiJS (Fastest 2D WebGL Renderer) Game Engines Intermediate ⏱ 2 days

WebGL-accelerated 2D renderer — fastest sprite batcher in JS — filter library + mesh/particle — render-only (no physics).

🎯 When to use: Performance-critical 2D > 1000 sprites (particle storm, map editor, character creator); isometric MMORPG (realm-of-heroes zoom-out); data-viz dashboard with 5k nodes
✅ Pros
• WebGL sprite batching = 5000 sprites at 60fps on mobile; Phaser Canvas renderer tops at ~500
• Filter library: Blur/ColorMatrix/Displacement built-in — particle glow / CRT effect in 3 lines
• v8 async init: `await app.init({...})` — clearer than v7 callback chain; tree-shake friendly ESM
• ParticleContainer for > 100 sprites of same texture — 1 draw call regardless of count (5x faster)
• Mesh + NineSlicePlane for procedural UI — saves Photoshop round-trips in pixel-perfect HUDs
• Renderer-agnostic: WebGL, WebGPU (v8 alpha), Canvas — future-proof your 2D investment
⚠️ Cons
• Render-only — no scene management, no physics, no input handling (must add EventEmitter separately)
• ~400KB minified → bigger than Phaser MINIMAL but faster; trade size for FPS
• v8 API is async (await app.init) — migration from v7 sync API = lots of await refactors
• No game loop helper — must wire your own ticker (gsap, raf, or own loop); Phaser ships one
• Filter pipeline = 1 GPU pass per filter; stacking 3+ filters tanks to 20fps on iPhone 11
• Texture atlas must be in specific format (Pixi v8 prefers Spine-style JSON, not TexturePacker default)
⚡ Gotchas
• v8 (2024) is async — `await app.init({width,height})` then `document.body.appendChild(app.canvas)`; v7 sync is GONE
• Use ParticleContainer (not Container) for > 100 sprites of same base texture — single draw call vs 100
• app.canvas has 2x devicePixelRatio already — set CSS width/height in half-pixel units to avoid blur
• Filters stack = each filter = 1 framebuffer ping-pong; cache filter result in RenderTexture if static
• Texture.from('url') caches by URL — clear with Texture.removeFromCache on memory warning
• InteractionManager (v7) → FederatedEvents (v8) — on('pointerdown') API changed; migration = search/replace
• Bundle size: pixi.js (full) is 400KB; @pixi/sprite + @pixi/app is 200KB — choose per use case
• WebGL context loss on Android Chrome tab background → listen to app.renderer.on('contextlost') and rebuild textures
🚫 Anti-patterns
• DON'T put 10000 individual Sprites in a Container — wrap in ParticleContainer or batch by texture for 1 draw call
• DON'T mix PIXI.Text + HTML <input> in same layout — different sizing model; use Pixi v8 Text or full DOM
• DON'T apply Filter to entire stage — apply only to specific sprites; full-stage filter is GPU bottleneck
• DON'T resize canvas via CSS only — must also call app.renderer.resize(w,h) to update buffer (v8) or canvas.width/height (v7)
• DON'T forget app.destroy() on unmount — Pixi leaks WebGL context + GPU buffers; React useEffect cleanup mandatory
🔀 Alternatives
• 3.2 Phaser (physics included)
• 3.3 Vanilla Canvas (no dep)
• 3.5 Kaboom (simpler)
3.7 🎆 WebGL / WebGL2 (Custom GLSL Shaders) Game Engines Advanced ⏱ 2 weeks

Raw GPU API — hand-write GLSL vertex/fragment shaders — max performance for particle storms, fluid sim, post-FX — no engine.

🎯 When to use: 10k+ particles with physics-on-GPU; custom fluid/water/fire shader; post-process (bloom/DoF/SSR); audio visualizer; shadertoy-style art
✅ Pros
• GPU compute (WebGL2 transform feedback / WebGPU compute) — 100k particles updated in <1ms on RTX 2060
• Full creative control — fragment shaders can paint anything: raymarching, signed-distance fields, NPR stylization
• WebGL 2 in 96% browsers (2024) — VS WebGPU 18%; WebGL2 is today's safe low-level target
• GLSL ES 3.00 vs WebGL1 1.00 — uniform blocks, instanced drawing, texelFetch — modern features available
• ShaderToy migration is 1:1 — copy any .glsl shader, paste into <script id='vs'>, compile, render to fullscreen quad
• Three.js ShaderMaterial = on-ramp: use Three's renderer + camera + raw ShaderMaterial → keep conveniences, drop into GLSL when needed
⚠️ Cons
• GLSL is a real language — uniforms, varyings, attributes, precision qualifiers — 2-week ramp for non-graphics dev
• Debugging is brutal — no breakpoints; print uniforms as color and read pixels; Spector.js / RenderDoc are external
• Context loss handling mandatory — Android Chrome tab-switch drops context; must rebuild all shaders
• No scene graph — you build VAO/VBO/uniform setup; 500 LOC for what Three does in 5
• Mobile GPU varies wildly — Adreno 640 vs Apple A14 = 10x perf gap; must profile on real devices
• WebGL doesn't multithread — heavy CPU physics + WebGL render = jank; move physics to worker (sims-lite pattern)
⚡ Gotchas
• WebGL2 = 96% browsers, WebGL1 = 99% — for new code use WebGL2 (`canvas.getContext('webgl2')`) but feature-detect fallback (Three.js does this)
• Context loss: listen `canvas.addEventListener('webglcontextlost', e => { e.preventDefault(); rebuild(); })` — must preventDefault to recover
• Precision qualifiers matter: mediump in fragment shader is fast but 16-bit float; highp required for positions, lowp for color tints
• Don't allocate textures per frame — pool or use single dynamic texture with texSubImage2D updates
• Uniform location lookups are slow — `gl.getUniformLocation(p, 'u')` once at boot, cache the location; per-frame lookup = 5ms tax
• Instanced drawing (gl.drawArraysInstanced) — render 10000 cubes in 1 draw call instead of 10000; Three's instancedMesh wraps this
• Use float textures (EXT_color_buffer_float) for post-process ping-pong — required for HDR bloom in WebGL2
• Shader compile is async — call `gl.getShaderParameter(s, gl.COMPILE_STATUS)` and check `gl.getShaderInfoLog(s)` for errors; silent fail = black screen
🚫 Anti-patterns
• DON'T write vertex shader from scratch for triangle mesh — Three.js BufferGeometry + ShaderMaterial gives you the boilerplate; raw WebGL only when at GPU limits
• DON'T use `vec3` for colors in fragment shader — use `vec4` with alpha for blending; vec3 alpha defaults are wrong
• DON'T skip `gl.flush()` after multiple draw calls if reading pixels back — driver may reorder; explicit barrier for readPixels
• DON'T enable blending if you don't need it — `gl.disable(gl.BLEND)` saves 30% GPU on opaque-only scenes
• DON'T forget to set `gl.viewport(0, 0, canvas.width, canvas.height)` on resize — without it renders stretch on HiDPI
🔀 Alternatives
• 3.4 Three.js (engine wrapper)
• 3.8 WebGPU (next-gen)
• 3.1 Babylon (full game)
3.8 WebGPU (Next-Gen GPU API) Game Engines Advanced ⏱ 3 weeks

WebGL successor — compute shaders on web, render pipelines, multi-thread via async — 2024 stable in Chrome/Edge, partial in Safari/Firefox.

🎯 When to use: Compute-heavy: ML inference in browser; raytracing preview; physics-on-GPU (1M particles); large terrain LOD streaming; shader-heavy NFT art
✅ Pros
• Compute shaders = real GPGPU on web — 1M particle sim at 60fps on M1 MacBook; WebGL needs transform feedback dance
• Render pipeline objects pre-compiled — switching shader = 0ms vs WebGL's re-link cost (5-15ms)
• Bind groups = explicit resource layout — GPU drivers don't have to guess; faster + safer
• Async by design — adapter/device init returns Promise; no more blocking gl.flush() in worker threads
• WGSL = real typed language — no GLSL precision footguns; tooling (naga) validates at compile
• Three.js r150+ has WebGPURenderer (experimental) — try with `await renderer.init()` and same scene graph as WebGL
⚠️ Cons
• Browser support 2024: Chrome 113+/Edge ✓; Safari TP ✓; Firefox behind flag → production = double-maintain WebGL fallback
• API surface 3x WebGL — bind groups, pipeline layouts, queue, command encoder — steep ramp 3+ weeks
• WGSL still has churn (v0.10 in 2023 → v0.13 in 2024) — port code between versions; no v1.0 yet
• Compute shader debugging = Spector.js only; no shader-level breakpoints; black screen = infinite loop risk
• Mobile GPU drivers less mature than WebGL — Adreno/Mali sometimes return wrong results; ship CPU fallback
• Documentation still sparse vs WebGL — Mozilla MDN WebGPU pages added 2024 but community answers 10x fewer than WebGL
⚡ Gotchas
• Feature-detect: `if (!navigator.gpu) return fallbackToWebGL()` — never assume; on iOS Safari 17.4+ TP only
• requestAdapter() can return null on low-end Android — check, then requestDevice(); null device = no GPU
• Bind groups must match @group(N) in WGSL exactly — order matters; mismatch = validation error + silent fail
• Queue writes are batched — multiple writeBuffer + 1 submit() = best perf; per-frame submit = driver overload
• Texture format matters: 'bgra8unorm' on Apple Silicon vs 'rgba8unorm' on AMD — query preferredCanvasFormat
• Compute shader workgroup size is a multiple of 64 — pass @workgroup_size(64); use numthreads(64,1,1) in HLSL-translated WGSL
• Async pipeline creation = first frame after compile is slow (100ms+) — precompile at scene init or loading screen
• Lost device (adapter too hot) — listen to device.lost.then(info => recreateDevice(info)); WebGL doesn't have this lifecycle
🚫 Anti-patterns
• DON'T assume WebGPU available — always ship WebGL fallback (or Babylon/Three handles it); iOS users = Safari = broken in 2024
• DON'T create one big bind group for whole scene — split per logical resource (camera, lights, materials); bind groups are cheap
• DON'T submit command encoder per object — batch all draws into 1 encoder pass; submit() = GPU sync barrier
• DON'T use WGSL f32 for indices — use u32; f32 index buffer = validation error + driver panic
• DON'T load compute shader from string at runtime — precompile to .wgsl binary at build step; runtime parse = 200ms hitch
🔀 Alternatives
• 3.7 WebGL (universal)
• 3.1 Babylon (engines WebGPU flag)
3.9 🌐 WebSocket Multiplayer (Realtime 2-Way) Game Engines Advanced ⏱ 3 days

Bidirectional TCP-over-HTTP socket — server required — low-latency realtime state sync for multiplayer games, collab editors, chat.

🎯 When to use: MMORPG (realm-of-heroes); realtime collab whiteboard; co-op tycoon; trading card game; spectator chat; live leaderboard; turn-based battle server-authoritative
✅ Pros
• Full-duplex persistent connection — server can push anytime (chat message, monster spawn); HTTP request/response can't
• Sub-100ms latency on same-region — feels instant for click-to-action in realm-of-heroes PvP
• Single TCP socket multiplexes many logical channels (combat/chat/inventory) — one auth, many features
• Standard browser API — no library needed for basic client; battle-tested for 20+ years
• WebSocket Secure (wss://) = same TLS as HTTPS — works through corporate proxies that block UDP
• Server can be Node.js / Python / Go / Elixir — pick your stack; spec is implementation-agnostic
⚠️ Cons
• Server required — minimum Node ws library OR paid service (Pusher/Ably); DevOps cost
• State sync is YOUR problem — engine doesn't auto-reconcile; client A says 'I moved', client B says 'I'm at', who wins = server arbitration
• Connection drops require reconnect + state resync — handle 3G subway tunnels gracefully (mobile game rule)
• No built-in auth — token in first message, validate server-side; never trust client messages (cheating)
• WebSocket frames have overhead — for chat text, SSE may be simpler; for binary video, WebRTC
• Scale: 10k concurrent sockets per server is easy; 100k needs Redis pub/sub + sticky sessions (CF/Traefik)
⚡ Gotchas
• Reconnect with exponential backoff: `setTimeout(reconnect, Math.min(30000, 1000 * 2**attempt))` — never re-attempt fixed-interval (DOSes server when it comes back up)
• Heartbeat ping/pong every 25s — many proxies (Cloudflare, AWS ALB) drop idle connections at 30-60s; pong frame keeps them alive
• JSON.stringify every message is 10x slower than binary — for high-frequency updates (60fps positions) use MessagePack or Protobuf
• Send a `seq` number per outbound message — server echos; client drops out-of-order packets; otherwise race conditions on state
• On reconnect, send `lastSeq` to server; server replays missed events; OR full state-snapshot if `lastSeq < server.bufferStart`
• WebRTC for true P2P (no server) is different API — use for voice; WebSocket for game state; don't mix
• iOS Safari 14 background → WS closes after 30s — listen for visibilitychange + force reconnect on visible
• Don't put WebSocket in main bundle — code-split via dynamic import(); saves 30KB initial JS for users who never join multiplayer
🚫 Anti-patterns
• DON'T trust client positions — server must validate move speed/collision; otherwise realm-of-heroes PvP = teleport hack
• DON'T broadcast all messages to all players — use rooms/channels; realm-of-heroes area-based subscribe = 90% bandwidth saved
• DON'T do heavy computation on every WS message — batch process every 16ms (60fps) tick; per-message handler = CPU spike
• DON'T forget to handle 'binary' message type — ws.onmessage event has .data as Blob or ArrayBuffer; decode based on typeof
• DON'T use ws.send() in render loop without throttling — at 60fps you flood socket; aggregate 50ms snapshots
🔀 Alternatives
• 9.7 Fetch/AJAX (one-shot)
• WebRTC (P2P)
• SSE (server-push only)
3.10 🔄 Game State Machine (FSM) Game Engines Intermediate ⏱ 2 hours

Finite State Machine — entity has 1 state at a time (IDLE/WALK/ATTACK) — guards on transitions prevent invalid combos (e.g., JUMP→JUMP without landing).

🎯 When to use: Entity with > 3 behaviors (NPC villager, enemy AI, player combat); UI flow (loading→menu→game→pause→gameover); boss phase manager; AI reactions; matchmaking states
✅ Pros
• States + transitions = visual diagram → stronghold-3d villagers: IDLE→WANDER→WORK→SLEEP all in one PNG
• Guards prevent invalid combos (JUMP only when onGround) — bug class 'attack while dead' impossible by design
• onEnter/onExit hooks = clean lifecycle — play anim once on ATTACK enter, stop sound on DEATH exit
• Trivial to debug — log state transitions; replay shows exactly when AI broke (sims-lite NPC debugging)
• Maps directly to Unity Mecanim / Unreal StateTree — port to native engines is 1:1
• String state IDs survive JSON save/load — int enums break if you reorder (street-fighter-thai rule)
⚠️ Cons
• Parallel states need Hierarchical FSM (HFSM) — basic FSM can't express 'WALK while CARRYING while HUNGRY' simultaneously
• Boilerplate per state — 5 states × {enter, exit, update, transition} = 20 funcs; behavior tree collapses some
• Transitions can grow — 5x5 matrix = 25 cells; many are 'never'; still need to declare to fail-safe
• No memory of past states — 'just attacked 3 times in a row' needs state counter on top of FSM
• String state IDs typo-prone — `setState('ATTAK')` silently fails if not guarded by Set check
• Doesn't model goal-directed AI well — 'go find food' needs BT planner on top; FSM is reactive not proactive
⚡ Gotchas
• Use string IDs not numeric enums — `STATES.IDLE = 'idle'`. Numeric enums reorder when JSON load gives wrong value (street-fighter-thai lesson)
• Guard functions return boolean: `{ idle: { walk: () => true, attack: m => m.inRange && m.stamina > 0 } }` — guards checked in order
• Map<state, transitions> not object literal — Map preserves insertion order across browsers; {} order is spec-but-implementation-leaky
• Pre-condition check inside guard, NOT inside setState — setState should be dumb; guards do the validation
• onExit called BEFORE onEnter of new state — order matters for anim cleanup (sims-lite: stop walk anim before start attack anim)
• Don't transition to same state — `if (next === this.current) return`; otherwise onEnter fires twice causing anim restart glitch
• State machine for UI screens is global singleton, not per-entity — `class GameState { state = 'menu' }` lives at root
• Save/load: serialize current state ID + counters; on resume, force onEnter to re-init timers (don't trust restored timer values)
🚫 Anti-patterns
• DON'T use numeric enums for state — `STATES.IDLE = 0`; reorder JSON = wrong state. Use string literals
• DON'T mutate current state directly — always go through setState(next); otherwise onEnter/onExit skipped, anim desync
• DON'T put data inside state object — state ID is just a string; counters/inventory live in entity data (separation of concerns)
• DON'T nest 5+ levels of FSM — flatten with sub-states or switch to Behavior Tree; deep nesting = spaghetti
• DON'T forget to handle 'interrupted' transitions — if AI was WALK and you set to ATTACK, the WALK onExit must reset pathfinding
🔀 Alternatives
• 4.4 Behavior Tree (more expressive)
• 4.2 ECS (data-oriented)
• 1.3 Module Pattern (state holder)
4.1 ⏱️ Game Loop (60fps + Delta Time) Game Patterns Intermediate ⏱ 2 hours

Update with dt seconds (NOT per-frame constants). Decouple sim Hz from render Hz via fixed-step accumulator.

🎯 When to use: ทุกเกมที่มี animation/movement/physics | Every game with animation, movement or physics; sims with time scale
✅ Pros
• Frame-rate independent — same physics outcome on 60Hz phone and 144Hz monitor because dt normalizes time
• Decoupled sim/render Hz — physics ticks at fixed 60Hz while render interpolates at any monitor refresh
• Pause/slow-mo trivial — multiply DT by 0 (pause) or 0.5 (slow-mo); no special code paths needed
• Deterministic replays — fixed DT + seeded RNG = byte-identical playback for netcode or sharing
• Tab-background safe — clamp dt to 0.25s prevents physics explosion when user returns from background tab
⚠️ Cons
• Accumulator math non-obvious — beginners copy-paste without understanding why spiral-of-death happens
• Multiple update calls per render wastes CPU on slow devices — 144Hz render + 60Hz sim = 2.4x physics calls
• Float drift over long sessions — hours 12+ see sub-millisecond desync, breaks frame-perfect games
• Time scaling breaks input if DT not normalized — character moves slower when paused mid-jump
• Interpolation adds 1-frame visual lag — visible at 30Hz render; competitive fighting games skip interpolation
⚡ Gotchas
• ALWAYS clamp dt to ~0.25s — tab background return can deliver 5+ seconds dt = physics blow-up on first tick
• requestAnimationFrame pauses when tab hidden — never trust frame count for time, always use performance.now()
• Fixed timestep (Glenn Fiedler's pattern) prevents non-determinism across framerates — without it, replays differ
• Accumulator pattern: `while (acc >= DT) { update(DT); acc -= DT; }` — if overshoot, queue leftover for next frame
• Decimal time (ms) accumulates float error over 24h — use modular snapshots or reset delta periodically
• Audio scheduling must use AudioContext.currentTime not rAF — clock skew between audio and game thread possible
• Slow-mo: scale BOTH the realDt input AND the DT reference, else physics desyncs from rendered position
• First frame dt = 0 — guard with `if (last === 0) last = now` or physics does nothing on frame 1
🚫 Anti-patterns
• Update everything per-frame without dt — character speed differs wildly on 60Hz vs 144Hz monitors
• Use setInterval instead of requestAnimationFrame — drifts, runs in background tabs, can't pause cleanly
• Clamp dt to 'make it look right' instead of fixing physics step — hides bug, breaks determinism
• Skip interpolation when rendering at fixed Hz — visible stutter every 16ms on 30Hz devices
• Trust wall-clock for game time — Date.now() jumps on NTP sync; use performance.now() monotonic clock
🔀 Alternatives
• 4.2 ECS (system tick order matters)
• 1.4 localStorage (debounce save on dt accumulation)
4.2 🧩 Entity-Component System (ECS) Game Patterns Advanced ⏱ 3 days

Entity (just an ID) + Component (pure data) + System (pure logic). Compose behaviors by mixing components.

🎯 When to use: Game engine, buff/debuff systems, particle systems, simulation > 100 entities | เกมที่ entity หลายร้อย+ และต้องการ mix behaviors แบบ runtime
✅ Pros
• Composition > inheritance — add Burnable component to ANY entity (player, NPC, tree) without class hierarchy change
• Cache-friendly SoA storage — components in contiguous TypedArrays, CPU prefetch works, 10-100x faster than OOP
• Parallel systems — pure-function systems run on Web Workers without locks (Unity DOTS proves this at scale)
• Hot-swap behaviors — add/remove Shielded component at runtime, no recompile, designer-friendly inspector
• Reuse across archetypes — same Move system iterates player and enemy, no code duplication
• Deterministic for netcode — systems read all components, no method dispatch variance across machines
⚠️ Cons
• Boilerplate — every behavior = component type + system function + iteration code, 3 places to edit per change
• Debug trail scattered — entity 'the player' is 5 entries across component maps, not one object to inspect
• Premature for small games — < 100 entities: just use class instances, easier to grep, faster to ship
• Query complexity — 'find all entities with Position AND Velocity' requires iteration, not free
• Refactor cost — moving data between components breaks every save file that referenced the old structure
• Event passing awkward — entity A needs to talk to entity B across system boundaries, no direct method call
⚡ Gotchas
• Components MUST be pure data — never put methods on component objects (Unity DOTS rule, breaks archetype SoA)
• Systems MUST be pure logic — read components, write components, no side effects outside assigned components
• Entity ID is opaque token — UUID or int, never reuse after delete (stale references in saves silently corrupt)
• Sparse set vs archetype storage — sparse flexible, archetype cache-fast (10-100x); Unity DOTS uses archetypes
• Don't store entity references in components — store IDs; entity refs break serialization and create cycles
• System execution order matters — Physics → Movement → Animation → Render → UI; explicit pipeline, not implicit
• Serialization = list components per entity — `[{id, comps: [Pos, Vel, Health]}]` round-trips through JSON
• Archetype churn is expensive — adding/removing a component moves entity to new archetype group; batch changes
🚫 Anti-patterns
• Inheritance 'GameObject extends Entity' — defeats composition; go back to OOP if you need this
• Systems that touch DOM directly — side effects across ticks = race conditions, debug hell
• Components holding references to other entities — breaks serialization, creates GC cycles, refactor nightmare
• Method on component object — turns component into mini-class, defeats data-oriented design and SoA caching
• Single mega-system doing everything — splits physics/render coupling into god-object, hard to parallelize
🔀 Alternatives
• OOP inheritance (simpler for <100 entities)
• 4.4 Behavior Trees (orthogonal; can be a system)
4.3 🗺️ Pathfinding A* Game Patterns Intermediate ⏱ 1 day

Shortest path on grid via open/closed sets + admissible heuristic. Optimal when heuristic never overestimates.

🎯 When to use: NPC path, tower defense creep path, RTS unit move, tile-based game AI | เกมที่ entity เดินบน grid และต้องการ shortest path
✅ Pros
• Optimal with admissible heuristic — Manhattan 4-dir, Octile 8-dir; never returns longer path than true optimum
• Composable heuristics — swap Manhattan→Euclidean→Octile based on movement cost model without algorithm change
• JPS (Jump Point Search) — 2-10x faster by skipping symmetric neighbors, same optimality, same API
• Binary heap priority queue — openSet stays O(log n) per insert/extract vs naive array O(n)
• Cache-able per query — re-plan only on grid change, not every move; flow field pre-compute for 1000+ units
• Tie-breaking for speed — prefer goal-direction when f-scores tie, 2-3x speedup without losing optimality
⚠️ Cons
• Memory O(n²) worst case — 1000x1000 grid = 1M nodes × 100B = 100MB; chunk grid or use hierarchical A*
• Re-planning on dynamic obstacles expensive — D* Lite handles but adds complexity; consider flow fields for static
• Not smooth on grid — paths have 8-way zigzag, need string-pulling / funnel algorithm post-process
• CPU spike on first plan — pre-compute flow fields for 1000+ unit RTS games (Tom Clancy's EndWar pattern)
• Heuristic tuning is project-specific — wrong heuristic = non-optimal path or 10x slower search
• Tile cost changes break admissibility — cost=2 tile + Manhattan = overestimates goal, returns non-optimal
⚡ Gotchas
• Manhattan for 4-dir movement — Octile `(D*(dx+dy) + (D2-2D)*min(dx,dy))` for 8-dir where D2=√2
• Binary heap (min-heap) for openSet — `Array.shift()` is O(n), heap extract is O(log n); 100x difference at 10k nodes
• f = g + h; g = actual cost so far; h = estimated cost to goal; h must NEVER overestimate (admissible)
• Closed set ≠ 'visited' — it's 'we found OPTIMAL g for this node'; revisit if better g found later
• Tie-breaking: prefer nodes with smaller h when f ties — pulls search toward goal, 2-3x speedup
• Path reconstruction via parent pointers — null parent = start; chain back to start to get path array
• Diagonal corner-cutting forbidden when EITHER adjacent cardinal is blocked — prevents squeezing through walls
• Reset closed set between queries — reusing across queries with different goals = suboptimal paths
🚫 Anti-patterns
• Store node object in openSet without hashing — O(n²) linear scan per pop, kills performance at 1000+ nodes
• Use heuristic = 0 — becomes Dijkstra, 5-10x slower than A* with good heuristic
• Forget to rebuild path via parent pointers — silent null returns, NPC stands still with no error
• Re-plan full A* every frame — use path cache, invalidate only on map change; 100x perf improvement
• Misuse Euclidean on grid — overestimates for 8-dir movement, returns SUBOPTIMAL path (silent bug)
🔀 Alternatives
• Dijkstra (slower but always optimal)
• JPS Jump Point Search (10x faster)
• Flow fields (better for 1000+ units)
4.4 🌳 Behavior Trees (NPC AI) Game Patterns Advanced ⏱ 1 week

Hierarchical nodes: Selector (OR), Sequence (AND), Decorator. Designer-editable visual tree for NPC decisions.

🎯 When to use: NPC AI หลาย actions, Boss AI phases, Squad tactics, complex reactive behavior | Boss/เควส NPC ที่มีหลาย state
✅ Pros
• Visual structure = designer edits without code — JSON export/import, Unreal/Unity plugins, no engineering round-trip
• Composable nodes — Inverter/Repeater/Cooldown/UntilFail decorators instead of writing each pattern
• Reuse across 100 NPC archetypes — same 'Flee' subtree in goblin and dragon via shared subtree reference
• Event-driven beats tick-every-frame — 100x less CPU when NPC idle (subscribe to OnTakeDamage, not tick)
• Decoupled from entity data — blackboard pattern separates tree structure from entity state
• Reactive to external events — 'OnSeeEnemy' instantly triggers attack branch without polling
⚠️ Cons
• Tick overhead even when no change — solution: external trigger list + tick only on event subscription
• Stack depth errors — recursion depth = tree depth, > 1000 levels crashes; iterative traversal for deep trees
• State machine in disguise — pure tick-every-frame trees are FSM with extra steps; events needed for value
• Debugging harder than FSM — 'why did goblin walk into wall?' = trace parent chain through tree state
• Designer learning curve — Sequence vs Selector semantics ≠ intuitive AND/OR; 1-day training needed
• Blackboard naming conflicts — two NPC trees reading same 'target' key cross-contaminate state
⚡ Gotchas
• Selector (OR): tick children left-to-right, first SUCCESS/RUNNING wins; if all FAIL → return FAILURE
• Sequence (AND): tick children left-to-right, any FAILURE/RUNNING short-circuits; all SUCCESS → SUCCESS
• Decorators wrap single child: Inverter (Success↔Failure), Repeater (loop N times), UntilFail (loop until FAIL)
• Action returns 3 states: Success / Failure / Running — Running means 'still working, tick me next frame'
• Event-driven > tick-every-frame — subscribe to events, queue triggers, tick on wake; 100x less CPU
• Blackboard = shared state — namespace by entity or global; conflicts are silent bugs, version key prefixes help
• Priority order in Selector matters — cheap checks FIRST (cooldown check before pathfind), 10x speedup
• Re-entry mid-tick breaks — Sequence returns Running then immediate re-tick = state corruption; protect with mutex
🚫 Anti-patterns
• Tick the tree every frame for ALL NPCs regardless — wastes CPU; use event trigger + wake list
• Action node reads entity state directly — couples tree to data shape; use blackboard for testability
• Sequence with no Failure path — entire NPC stuck forever once first child fails (deadlock)
• Same tree INSTANCE shared across entities — blackboard reads cross-contaminate; clone or per-entity BB
• Decorator recursion without base case — Repeater with no Failure returns = infinite loop, tab freezes
🔀 Alternatives
• FSM (simpler for <5 states)
• GOAP planning (more flexible but slower)
• Utility AI (numerical scoring, harder to visualize)
4.5 🎲 Procedural Generation Game Patterns Intermediate ⏱ 3 days

Random content via seeded RNG. Same seed = identical output. Combine algorithms for natural-feeling variety.

🎯 When to use: Roguelike levels, sandbox worlds, test data, daily challenges, shareable seeds | เกมที่ต้องการ infinite content
✅ Pros
• Infinite content without storage — 1KB seed generates GB of world; procedural saves disk, ships bigger games
• Shareable seeds — 'try seed 8675309' = reproducible competition (Spelunky Daily Challenge, No Man's Sky portals)
• Always-fresh gameplay — no memorization reduces boredom in roguelikes; 1000 hours vs 10 hours
• Combined algorithms — Perlin + Voronoi + Poisson gives natural-feeling output (terrain/biomes/trees)
• Easy unit testing — fixed seed = deterministic output = regression-testable; CI golden fixtures
• Player perception of 'more game' — 1000 generated levels feel like 1000 hand-crafted to most players
⚠️ Cons
• Quality variance — bad seeds produce broken/unfun levels; validation pass mandatory, not optional
• Combinatorial explosion — 100 rooms × 10 templates = 1000 cache slots; LRU cache or compute-on-demand
• AI-slop feel — humans detect patterns in 5-10 minutes; pure noise feels hollow, needs curation
• Designer override needed — hand-crafted set pieces must override algorithm (boss rooms, story beats)
• Performance — 100k tile Perlin = 50ms freeze on slow phones; pre-compute or Web Worker offload
• Seed bug reproducibility — wrong RNG = different output, hard to debug without exact seed string
⚡ Gotchas
• ALWAYS seed RNG (mulberry32 / sfc32) — Math.random() is unseedable in browsers; same seed MUST give same output
• Validate generated output — connected rooms? spawn reachable? boss arena exists? no isolated islands?
• Combine techniques: Perlin for heightmap + Poisson for tree placement + Voronoi for biome boundaries
• Layer noise frequencies — 1 octave smooth, 8 octaves realistic but 8x cost; tune to content type
• Player-progression fairness — generation must adapt to player level (Angry Birds stage difficulty curve)
• Determinism needs consistent iteration order — never Set, always sorted Array or insertion-ordered Map
• RNG state pollution — each generator reseeds with own seed, not reuse parent RNG (coupling bug)
• Output quality check: count dead-ends, isolated islands, unreachable regions; regenerate if fail
🚫 Anti-patterns
• Pure Math.random() — unseedable, breaks shareable seeds, no reproducible bugs
• Single algorithm for everything — samey output, players spot pattern in minutes; layer techniques
• No validation pass — ship broken levels; players find seeds that crash or trap them
• Generation on render thread — main thread freezes on Perlin compute; Web Worker or pre-compute
• Same RNG instance threaded through parallel async — race conditions in output, hard-to-repro bugs
🔀 Alternatives
• Wave Function Collapse (constraint-based, better for tilesets)
• Hand-authored levels (quality ceiling higher but 100x dev cost)
4.6 🎒 Inventory System (Slot/Stack) Game Patterns Intermediate ⏱ 4 hours

Slot-based array with maxStack per item, stack-first then empty-slot fill, Object.freeze templates.

🎯 When to use: RPG inventory, tycoon resource stock, survival crafting, equipment loadout | เกมที่ player เก็บของได้หลายชิ้น
✅ Pros
• Clear state — `slots[i] = {itemId, qty}` debuggable in DevTools, JSON-serializable for save
• Bounded memory — max slots cap prevents OOM from drop-spam exploit; 20 slots × 50B = 1KB
• Stack semantics match player mental model — 99 potions = 1 stack slot, not 99 slots
• Easy serialization — `JSON.stringify(slots)` round-trips through localStorage, no Map/Set gotchas
• Drop/swap semantics obvious — null slot = empty, swap = exchange pointers, partial-stack split
• UI flexibility — same data drives grid, list, hotbar, mini-map representations without change
⚠️ Cons
• Slot limit feels artificial — 20-slot bag + 100 herbs = frustrating without 'upgrade bag' mechanic
• Partial-stack logic verbose — 50 arrows into 2 stacks of 30+20 + empty slot = 3 branches to handle
• Drag-drop on touch unreliable — HTML5 DnD fires dragend on iOS inconsistently; need click-to-move fallback
• Object.freeze hurts perf in hot loops — copy-on-read vs reference trade-off; freeze templates only
• Sort/filter requires stable sort — Array.sort mutates + locale-aware; deep copy before sort
• Crafting dependencies — recipe needs 3x item X but inventory has 1 stack of 30 = ambiguous 'remove 3' vs 'split'
⚡ Gotchas
• Always Object.freeze the item catalog — mutating ITEMS.potion.heal corrupts ALL inventories referencing it
• HTML5 DnD: dragstart requires event.dataTransfer.setData — iOS Safari fires twice, may need dedup
• Max stack per item type — gold stacks 999, swords stack 1, configurable per template (not hardcoded)
• Auto-stack on add — check existing stacks first before consuming empty slots; 99 herb add = no new slot
• Partial fill: qty=150, maxStack=99 → fills stack A (99), starts stack B (51), succeeds with 2 slots used
• Drop semantics: held item in cursor slot, not in inventory until drop event fires (atomic)
• Sort stability: compare by itemId first, rarity second, qty desc third — localeCompare for names
• Save serialization: array of slots smaller than Map — always array for storage, convert at runtime if needed
🚫 Anti-patterns
• Mutate the global ITEMS catalog at runtime — all inventories reference same object, one bug spreads
• Use plain object as slot — `{itemId, qty}` lacks maxStack metadata, separate lookup fragile
• Trust HTML5 DnD without fallbacks — mobile web fails 20% of time, need click-to-move or touch handlers
• One slot per item, no stacking — UI wastes 20 slots on 99 herbs; player rage-quits
• Allow negative qty on subtract — silent overflow corrupts save file; clamp at 0 minimum
🔀 Alternatives
• Tetris-style tetris inventory (fixed shape)
• Tetris/tetromino-bag (Tetris-like shape constraints)
• Weight-based (no slots, total weight cap)
4.7 💬 Dialogue System (Branching) Game Patterns Intermediate ⏱ 3 hours

JSON tree of nodes, conditions = function refs evaluated lazily, side effects on choice.

🎯 When to use: NPC dialogues, quest giver text, tutorial steps, branching story | เกมที่มี NPC คุยกับ player และเลือกตอบได้
✅ Pros
• Writers edit JSON — no engineer round-trip per line change; non-programmers ship content
• Localization trivial — `data[locale][nodeId].text` swap per language; 10x faster than string extraction
• Variable substitution — `{{playerName}}` resolves from blackboard; no template engine needed
• Conditions as function refs — `{condition: c => c.hasQuest('dragon')}` evaluated lazily at display time
• Save minimal — just `{currentId, history[]}` round-trips full conversation state in <100B
• Branching depth matches JSON nesting — designers see tree structure visually, no mental translation
⚠️ Cons
• State management — back button must pop history stack, or player stuck mid-conversation
• Variable interpolation risk — raw text injection = XSS if text from user; escape HTML in renderText
• Memory: deep JSON tree parses each conversation; 1000-node tree = 500KB; lazy-load by chapter
• Side effects (giveItem, startQuest) scattered in choices — hard to audit, hard to test in isolation
• No undo of destructive choice — chose 'kill NPC' without save = reload required; checkpoint system needed
• Localization drift — `start` updated in EN, missing in TH silently; lint script catches missing keys
⚡ Gotchas
• Unique ID per node — auto-rename `node_001`, `node_002`; never trust author-provided IDs (typos)
• Save state = `{currentId, history: [n1, n2, n3]}` — minimal payload; reset history on conversation end
• Conditions evaluated LAZILY — `fn(blackboard)` runs at choice-display time, not parse time (fresh state)
• Speaker attribution: each node has `speaker: 'npc'|'narrator'|'player'` for bubble color/position
• Typewriter effect needs `await sleep(30)` per char — frame-budget on long lines (100 chars = 3s)
• Variable interpolation: `{{player.name}}` lookup; escape HTML to prevent XSS (`<` → `&lt;`)
• Endless loop detection: `currentId === lastChoice.nextId` cycle guard, force-progress after N
• Localization keys not nested in `text` — `node.text` is final string, `node.textKey` is i18n lookup key
🚫 Anti-patterns
• Hardcode dialogue in JS strings — writers can't edit without redeploy; productivity killer
• Skip history stack — 'back' button impossible to implement, players rage-quit
• Evaluate conditions at JSON parse time — side effects fire BEFORE player sees choice
• One mega-tree for entire game — 10,000 nodes slow to parse, slow to grep, merge conflicts
• Mutate JSON tree at runtime — save file references by ID, but ID now means different text (silent bug)
🔀 Alternatives
• Yarn Spinner / Ink markup (designer-friendly syntax)
• YAML linear scripts (no branching)
• Visual novel engines (Ren'Py/Twine)
4.8 📜 Quest System Game Patterns Intermediate ⏱ 2 hours

Mission = objective + progress + reward. ONE central trigger function mutates all quest state.

🎯 When to use: RPG/tycoon/MMO with missions, daily challenges, tutorial steps, milestones | เกมที่ player ทำภารกิจเพื่อ reward
✅ Pros
• Engagement loop — short quests = dopamine hits = retention metric, proven across MMOs and mobile games
• Centralized trigger — ONE `updateQuestProgress(type, amount)` updates 50 quests, no scattered logic
• Reward immediacy — gold/exp given on `complete()` not on next scene load, players notice delay otherwise
• Composable objectives — `gather_wood` + `kill_orc` chain to `defeat_boss` via prereqs field
• UI auto-render — `activeQuests.map(q => renderHUD(q))` always in sync with state, no manual sync
• Analytics-friendly — `analytics.track('quest_complete', {id, time})` metrics out of box for tuning
⚠️ Cons
• State per quest — 100 active quests × 10 fields = memory footprint; compact schema needed
• Trigger coordination — `kill_orc` fires from combat, gathering from collection, must debounce duplicate fires
• Save/load serialization complexity — Map<id, Quest> needs toJSON/fromJSON, version migration on schema change
• Race conditions — two simultaneous triggers (pickup + quest complete) = double reward if not atomic
• Prerequisite chain explosion — quest A needs B needs C needs D = linear lock-in, frustrating if stuck
• Failure UX — player can't complete quest, no error message, just stuck; need progress hint UI
⚡ Gotchas
• Central trigger: `updateQuestProgress(type, amount)` is the ONLY function that mutates quest state
• Reward MUST fire IMMEDIATE — don't batch until quest turn-in, players notice delay = bad UX
• Quest type enum: 'gather', 'kill', 'deliver', 'talk', 'reach' — single string per quest, dispatch in switch
• Progress capped at target — `Math.min(target, progress + amount)` prevents overflow on event spam
• Auto-complete when progress >= target — single check in updateQuestProgress, no race condition possible
• Quest chains: `prereqs: ['quest_id_1', 'quest_id_2']` blocks UI button until done; gate accept()
• Save format: Array.from(activeQuests), not Map — JSON-friendly; load: `new Map(loaded.map(q => [q.id, q]))`
• Side objectives (optional) — separate `bonus` field, doesn't block main quest completion (engagement boost)
🚫 Anti-patterns
• Update quest state from 10 different places — race conditions, double-credit bugs, debug nightmare
• Reward on next scene load — player confused if gold doesn't appear; trust lost, churn up
• Linear quest chains with no alternative — 1 stuck quest = entire game blocked; offer 2-3 parallel paths
• No progress indicator — 'kill 5 orcs' with no counter = player forgets they have quest, abandons
• Hardcoded quest IDs in code — refactor breaks save files; use string const table exported from data
🔀 Alternatives
• Achievement system (4.9 — longer-term, less structured)
• Daily challenges (timer-based subset)
• MMO quest trackers (more complex UI)
4.9 🏆 Achievement System Game Patterns Beginner ⏱ 1 hour

Discrete accomplishments with idempotent unlock check. Categories for UI grouping. Event-triggered evaluation.

🎯 When to use: ทุกเกมที่ต้องการ replayability | Games with long-term progression, completionist goals, daily streaks
✅ Pros
• Idempotent check — `if (state.achs.includes(id)) return` prevents double-unlock toast spam
• Long-term goals — 'Reach level 50' runs in background of normal play, no separate UI to manage
• Replayability hook — completionist goes for 100% on second playthrough, proven retention driver
• Trivial serialization — `state.achs = ['first_blood', 'rich_1k']` is 1 line of JSON round-trip
• Cheap to evaluate — check on event (kill, levelup), not per-frame; 100 achievements = <1ms total
• Social proof — 'X% of players have this' badge drives engagement, visible leaderboard psychology
⚠️ Cons
• Users ignore achievements — 70% never look at the screen, dev effort vs retention unclear
• Notification spam — unlocking 5 at once buries important feedback; queue + throttle needed
• Edge cases — offline progress should still unlock if criteria met; reconcile on first tick
• Cheating — localStorage editable, 'achievements' not authoritative; server validation for competitive
• Tracking the untrackable — 'be polite to NPCs' needs behavior classification, ML or heuristics
• ROI unclear — dev time vs retention lift often negative; instrument and measure before expanding
⚡ Gotchas
• `if (state.achs.includes(id)) return` — idempotency is THE bug to avoid; toast fires 5x/sec otherwise
• Categories: 'combat', 'economy', 'social', 'exploration', 'meta' — UI grouping for 100+ entries
• Notification queue: enqueue, show 1 per 3s, not all at once; queue length cap (drop oldest)
• Server-validated achievements for competitive games — Steam, GOG, PSN each have own APIs, dup effort
• Date-based achievements — 'play 7 days in a row' needs streak counter, not just count; reset on miss
• Hidden achievements — `visible: false` for surprise reveals, surfaces only on unlock (engagement)
• Cross-platform sync — Steam achievements ≠ PSN achievements, write platform adapter layer
• Test achievement triggers with debug menu — manual playthrough 100x is too slow; CLI unlock-all for QA
🚫 Anti-patterns
• No idempotency check — toast fires 5x per second, log flooded, player dismisses notification
• Award on frame tick — wastes CPU; award on event (kill/levelup/quest_complete), batch check on save
• Hardcoded English strings — no localization, Thai players see 'Kill 100 enemies' in wrong script
• 100 achievements for trivial actions — 'walk 1 step' = meaningless padding; player tunes out
• Server trusts client achievement data — cheaters unlock everything in 30s; sign with HMAC
🔀 Alternatives
• Quest system (4.8 — structured, has progress)
• Steam achievements (server-validated, dup platform work)
• Daily challenges (timer-based subset)
4.10 🔄 Save Migration Game Patterns Intermediate ⏱ 1 hour

Multi-version backward-compat with version field. Forward-only migrations. Default values for missing fields.

🎯 When to use: เกมที่ ship หลายเวอร์ชัน, mobile apps with updates, browser games with patches | Any app persisted across deploys
✅ Pros
• User keeps progress across updates — biggest retention metric in mobile games (Angry Birds saved years)
• Forward + backward compat — old save loads in new build, new save loads in old build (with defaults)
• Version field is mandatory — every save records `_ver` integer, never trust absent (defensive)
• Migration is one-way per upgrade — v1→v2→v3, no skipped steps; idempotent if re-run
• Default values for missing fields — `data.quests ||= []` handles partial saves, never crashes
• Easy rollback — keep migration code in branches, revert = revert migration + restore old schema
⚠️ Cons
• Migration code per version — 5 versions × 100 fields = 500 lines of transform functions
• Bug amplification — bad migration corrupts ALL saves, not just new ones; CI fixtures MANDATORY
• Testing matrix — must test v1, v2, v3, v4, v5 loads in v6 build = combinatorial; automate
• Storage fragmentation — old format fields linger forever, can't GC; compression helps
• Non-deterministic migrations — Date.now() in migration = different output per run, save file unstable
• Schema vs data confusion — adding UI field ≠ save field; schema migration ≠ data migration, separate
⚡ Gotchas
• Keep `_ver` field always — never trust `parseInt(undefined)` defaults, must default to 1 explicitly
• One-way migration: v1 → v2 (never v2 → v1) — idempotent if you re-run; chain migrations in order
• Forward compat: new code reads old save + applies migrations to current version; old code reads new = defaults
• Backward compat: old code reads new save via `data.foo ?? defaultValue` everywhere; defensive reads
• Default values via `??` not `||` — `gold=0` and `name=''` are valid, `||` substitutes them wrong
• Test all version loads in CI — automated fixtures v1.json...v5.json + load test on each PR
• Never mutate the input — clone first; shared object reference = silent corruption of localStorage
• Migration failures should NOT crash — log + fallback to fresh save + report bug via analytics
🚫 Anti-patterns
• Skip versioning — one day you ship 'remove field X', every existing save crashes on load
• Mutate input data during migration — original localStorage value corrupted, cannot retry
• Use `||` instead of `??` for defaults — `gold=0` becomes `gold=defaultValue`, save data lost
• Migration that depends on time/random — output differs per load, save file unstable across reloads
• No testing for old version loads — ship update, all players with old save crash, 1-star reviews
🔀 Alternatives
• IndexedDB (more storage, async, complex API)
• Server-side DB migrations (Liquibase/Flyway)
• Backward-compat only (read latest, ignore older)
5.1 📋 Kanban Board Business Apps Intermediate ⏱ 4 hours

Lists of cards with drag-drop, multi-board, WIP limits, filters. FlowBoard คือ reference implementation ใน CodeHub.

🎯 When to use: Project mgmt (sprint board), GTD personal workflow, content pipeline (blog post stages), sales pipeline lite, learning roadmap (flowboard base)
✅ Pros
• Visual columns = spatial memory (ตาเห็น state ทันที ไม่ต้อง filter เหมือน table)
• Drag-drop เป็น natural mental model (physical sticky notes) = user เรียนรู้ 0 doc
• localStorage + JSON export = offline-first, ไม่ต้อง server, GDPR-safe โดย default
• WIP limits per column = บังคับให้ team focus (ตามหลัก Kanban ของ Toyota)
• FlowBoard architecture = DOM diff เฉพาะการ์ดที่ย้าย (move 200 cards = 1 repaint ไม่ใช่ 200)
• Tag/label บนการ์ด = filter SQL-like (multi-tag AND/OR) โดยไม่ต้อง query engine
⚠️ Cons
• HTML5 Drag-and-Drop API เก่า (2009 spec) = ไม่มี touch support, ghost image customization จำกัด
• Sortable.js (~40KB) ตอบโจทย์ mobile แต่ add bundle 1 lib = ต้องชั่งน้ำหนัก
• Real-time sync 5+ user = conflict (2 คนลากการ์ดเดียวกัน) ต้อง OT/CRDT layer
• Board > 1000 cards = DOM heavy, scroll lag (ใช้ virtual list แทน แต่ DnD กับ virtual scroll ขัดกัน)
• Save state ใน localStorage 5-10MB limit = board ใหญ่พร้อม description ยาว quota error
• Multi-board ใน URL hash (board#id) = SEO ไม่ได้, share link ต้อง snapshot state
⚡ Gotchas
• iOS Safari: HTML5 DnD touch broken ใช้ Sortable.js หรือ pointer events (touchstart + touchmove)
• dragstart setData('text/plain', id) เพื่อส่ง id ของการ์ด มิงั้น drop event ไม่รู้ว่าลากอะไร
• drop target = column ไม่ใช่การ์ดอื่น — ใช้ closest('.list') หา column id
• Animation: transition: transform 200ms บนการ์ดที่ย้าย ไม่ใช่ 200ms ทั้งหมด
• Optimistic UI: อัปเดต state ก่อน save แล้ว rollback ถ้า server fail (UX feel snappy)
• Undo: เก็บ ops stack {type:'move',cardId,from,to} ไม่ใช่ snapshot ทั้ง board (1KB vs 500KB)
• Mobile drag: dragover ต้อง preventDefault() มิงั้น drop ไม่ fire (Chrome quirk)
• localStorage quota: ใช้ IndexedDB เมื่อ board > 2MB (description ยาว + attachments)
🚫 Anti-patterns
• Re-render ทั้ง board ทุกครั้งที่ย้ายการ์ด — ใช้ DOM diff (Immer + key-based render) ลด reflow 100x
• เก็บ state ใน DOM data-attribute — DOM = render layer, state = JS object เท่านั้น
• Bind dragend แทน drop เพื่อบันทึก — ถ้า drop นอก valid target state จะ corrupt
• ใช้ <iframe> แยกต่อ board เพื่อ isolate CSS — overkill, ใช้ Shadow DOM หรือ scope CSS
• Save ทันทีทุก mousemove (debounce 0) — ใช้ throttle 500ms หรือ save ตอน drop เสร็จ
🔀 Alternatives
• Trello (closed, paid)
• Notion (heavy, multi-tool)
• Jira (overkill, dev-focused)
5.2 📅 Calendar (ICS Export) Business Apps Intermediate ⏱ 1 day

Month/week view + iCal (.ics) export + RFC 5545 recurrence. Use UTC storage + local display ตามหลัก iCalendar.

🎯 When to use: Schedule view, booking time slots, content calendar, lesson planner, event RSVP, medbook/flowboard ใช้พื้นฐานนี้
✅ Pros
• iCal .ics = text file 50-200 lines เปิดได้ใน Apple Calendar, Google Calendar, Outlook (universal)
• Month grid view = scan 30+ วันใน 1 หน้าจอ (better than list view เมื่อ date-dense)
• RFC 5545 RRULE string = 'FREQ=WEEKLY;BYDAY=MO,WE,FR' = recurrence 1 บรรทัด (no library)
• UTC storage + local display = หลีกเลี่ยง DST bug ที่พบบ่อยที่สุดใน calendar app
• UID field per event = email server ใช้ update/delete specific event โดยไม่ซ้ำ
• WebCal subscription (webcal://) = subscribe 1 ครั้ง, calendar auto-update ทุกครั้งที่ server เปลี่ยน
⚠️ Cons
• RFC 5545 recurrence 50+ rules (RRULE exceptions, RDATE, EXDATE) — implement เองเจ็บปวด
• iOS WKWebView parse .ics ไม่ได้ (ต้อง download → open in Calendar)
• DST: 'America/New_York' เปลี่ยนวัน — ใช้ 'TZID' หรือ UTC + local convert (ซับซ้อน)
• Calendar app แต่ละเจ้า render .ics ต่างกัน (Outlook มี escape character ต่างจาก Google)
• All-day event = DTSTART;VALUE=DATE ไม่มีเวลา แต่ถ้าใส่ 'T000000' บาง app แสดงเป็น 24-hour event
• VEVENT ไม่มี attendee RSVP built-in (ต้องใช้ ATTENDEE + iTIP protocol — heavy)
⚡ Gotchas
• DTSTART ต้องเป็น UTC 'YYYYMMDDTHHMMSSZ' แล้ว client app convert local — ห้ามเก็บ local time
• iCal line fold: บรรทัดยาว 75 octets ต้อง fold ด้วย CRLF + space (RFC 5545 section 3.1)
• Text escape: comma, semicolon, backslash ใน SUMMARY/DESCRIPTION ต้อง escape (\, \; \\ )
• UID: unique per event — ใช้ `${event.id}@yourdomain.com` ไม่ใช่ random เพราะ update ต้อง match UID
• Recurrence ID (RECURRENCE-ID) = reschedule 1 instance โดยไม่ทำลาย RRULE ทั้งชุด
• DTSTAMP = เวลาที่สร้างไฟล์ (not event time) — ใช้ track version ตอน subscribe update
• iOS 17+: .ics จาก webcal:// จะ open Calendar โดยตรง ไม่ต้อง download ก่อน
• Thai Buddhist year = +543 (พ.ศ. 2567 = ค.ศ. 2024) — convert ก่อน serialize ไม่งั้น import ผิดปี
🚫 Anti-patterns
• เก็บ local time ใน database — DST + timezone change = event shift ±1 ชม. ทุกปี, ใช้ UTC + IANA tz string
• Parse RRULE เอง (regex) — ใช้ rrule.js (~10KB) ที่ RFC-compliant, edge case 50+ rules
• สร้าง .ics ด้วย JSON template แล้ว stringify — escape character ผิด, ใช้ library ics (npm) ดีกว่า
• Embed HTML ใน DESCRIPTION แบบ inline — ใช้ X-ALT-DESC property (RFC 9265) แทน
• Save ไฟล์ .ics แล้ว expect browser 'add to calendar' button เด้ง — ต้อง data:text/calendar URL หรือ webcal:// scheme
🔀 Alternatives
• FullCalendar (heavy, ~200KB)
• react-big-calendar (React-only)
• Luxon (date math, no UI)
5.3 🤝 CRM (Pipeline) Business Apps Intermediate ⏱ 2 days

Contacts + deals + activities + stages + weighted pipeline value. People-crm บน CodeHub ใช้พื้นฐานนี้

🎯 When to use: Sales pipeline, freelancer client tracking, real-estate lead mgmt, partnership outreach, B2B SaaS early-stage (< 1000 contacts)
✅ Pros
• Pipeline stages = probability-weighted forecast (Lead 5% → Won 100%) = predict revenue ง่ายขึ้น
• Single customer view (contact + deals + activities) = no context switching between tools
• localStorage / IndexedDB = PII never leaves device (GDPR Art. 25 'data minimization by default')
• Activity timeline = chronological log (email/call/meeting) ให้ context ก่อน follow-up
• People-crm pattern: 1 contact ↔ N deals ↔ N activities = normalized schema, query ง่าย
• Tag system (VIP, decision-maker, churned) = segmentation โดยไม่ต้องสร้าง custom field
⚠️ Cons
• PII = sensitive (email/phone/address) เก็บใน localStorage = XSS ขโมยได้ทั้งก้อน, encrypt at rest
• GDPR/CCPA: 'right to be forgotten' = ต้องลบ contact + deals + activities ทั้งหมด 1-shot (cascade delete)
• Email integration (Gmail/Outlook OAuth) = token refresh + scope approval + rate limit 250/day/user
• Bulk import 1000+ contacts = duplicate detection fuzzy match (Levenshtein) ไม่งั้น DB ซ้ำซ้อน
• Lead scoring manual rules ขยายไม่ได้ — 5+ signal ต้อง decision tree หรือ ML model
• Multi-user 5+ = row-level lock conflict (2 คนแก้ contact เดียวกัน) ต้อง version field
⚡ Gotchas
• Encrypt at rest: crypto.subtle.encrypt AES-GCM ด้วย PBKDF2-derived key จาก user passphrase (ไม่ใช่ password)
• PII field mask: แสดง 'som***@gm***.com' ใน list view, full value ใน detail modal (UI pattern)
• Cascade delete: ลบ contact → confirm modal + delete deals + activities (3 queries ใน transaction)
• Import CSV: detect duplicate ด้วย email (lowercase + trim) ก่อน insert, แสดง 'merge?' modal
• Activity type enum: 'call', 'email', 'meeting', 'note' — enum ไม่ใช่ free-text เพื่อ filter ทำงาน
• Deal currency: JPY ไม่มี decimal, KWD 3 decimals — ใช้ BigInt cents แทน float (avoid 0.1+0.2 bug)
• iOS Safari: IndexedDB persist ต้อง navigator.storage.persist() ไม่งั้น iOS ลบเมื่อ storage pressure
• Search: client-side < 10k contacts = Fuse.js fuzzy, > 10k ต้อง server (Algolia/Meilisearch)
🚫 Anti-patterns
• เก็บ password ใน localStorage plaintext — ใช้ Web Crypto API + PBKDF2 100k+ iterations
• Allow export ทุก field เป็น CSV โดย default — PII leak risk, opt-in + audit log
• ใช้ <table> แสดง 10k rows — virtualize (react-window) หรือ paginate 50/page + filter
• Activity log = free-text textarea — enum + structured fields (outcome/duration) เพื่อ analytics
• Build custom 'tags system' เป็น many-to-many เต็มรูปแบบ — start ใช้ comma-separated string แล้ว migrate ทีหลัง
🔀 Alternatives
• HubSpot (heavy, paid)
• Notion CRM (general-purpose)
• Twenty (open source, server needed)
5.4 📆 Booking System Business Apps Intermediate ⏱ 3 hours

Slot picker + confirmation + 24h reminder + no-show handling. CodeHub มี medbook/dinebook/cospace/fitbook เป็น reference 4 ตัว

🎯 When to use: Restaurant (dinebook), clinic (medbook), co-working (cospace), gym (fitbook), spa (glowbook), tutor, salon
✅ Pros
• Grid view 7 days × 8 slots = scan 56 options ใน 1 หน้าจอ (faster than calendar picker)
• 24h + 1h reminder (2-stage) = no-show rate ลด 35-50% (Hospitality industry data)
• UTC storage + user TZ display = same booking shows 09:00 BKK ในไทย, 02:00 UTC ใน US
• Conflict detection: server-side double-book check (SELECT FOR UPDATE) ก่อน INSERT
• Web Push reminder = user ไม่ต้อง install app, แค่ allow notification ครั้งเดียว
• Slot duration adaptive: 15/30/60 min ตาม service type (massage 60, lab 15, consult 30)
⚠️ Cons
• No-show 5-15% baseline (industry avg) — ต้อง prepay/deposit เพื่อลด, แต่ flow ซับซ้อนขึ้น
• Time zone = ปวดหัวตลอด: server UTC, client local, DST changes ±1h, user travel across tz
• Concurrency: 2 user จอง slot เดียวกัน = race condition, ต้อง DB lock หรือ optimistic versioning
• Cancellation policy: refund logic (full 24h+ before, 50% 4-24h, 0% < 4h) = business rule ต่างกันทุกที่
• Resource utilization: idle slot = lost revenue, dynamic pricing (off-peak discount) ต้อง ML/heuristic
• Recurring booking (every Monday 9am × 12 weeks) = series + exceptions + skip-holiday logic
⚡ Gotchas
• Web Push: ต้อง service worker + VAPID key + user gesture ก่อน subscribe, iOS 16.4+ only
• Reminder timing: Date.now() + leadTime เป็น wall clock — server time skew ทำให้ notification เพี้ยน
• Slot generation: query existing bookings → filter out taken → return free slots (O(n) per request)
• Cross-day booking: spa 8pm จบ 10pm = next day, render edge case บน month view
• Capacity > 1: yoga class 20 seats, 18 booked → 2 free, decrement atomic ไม่งั้น overbook
• Past slot disable: เปรียบเทียบ slot < now() ก่อน render, แต่ 'today 23:00' ต้อง timezone-aware
• Time picker 24h vs 12h: locale 'th-TH' = 24h, 'en-US' = 12h AM/PM, ใช้ Intl.DateTimeFormat
• Email confirm: SPF/DKIM/DMARC record ของ domain = inbox ไม่ใช่ spam 100%, ตรวจก่อน go-live
🚫 Anti-patterns
• เก็บ booking ใน localStorage — 2 device = 2 state, sync conflict, ใช้ server (Postgres/Supabase)
• Confirm booking ทันทีหลังกด submit โดยไม่ conflict-check — double-book bug บ่อยที่สุดใน booking system
• Reminder setTimeout 1 ชม. ก่อน — ถ้า tab close = reminder หาย, ใช้ server-side cron + push
• ใช้ <input type='datetime-local'> เป็น slot picker — UX แย่, สร้าง grid เอง + custom click handler
• Assume timezone จาก IP — VPN/CDN ผิด, ใช้ Intl.DateTimeFormat().resolvedOptions().timeZone จาก browser
🔀 Alternatives
• Cal.com (open source, heavy)
• Calendly (paid, vendor lock)
• Spine (medical only)
5.5 💳 POS (Point of Sale) Business Apps Intermediate ⏱ 6 hours

Cart + barcode scan + tax/VAT per region + receipt print/PDF + cash/card prompt. Noodle-shop + pos-grocery เป็น reference บน CodeHub

🎯 When to use: Restaurant table-side order, retail checkout, kiosk self-order, food truck, pop-up store, noodle-shop / pos-grocery เป็น 2 ตัวอย่างจริง
✅ Pros
• Cart เก็บ localStorage = tab crash ไม่เสีย order (recovery 1-click restore)
• Barcode input via <input> + 'rapid mode' = 60-80 items/min (faster than touch screen picker)
• Tax per region: TH 7% VAT, US state tax (5-10%), EU 19% MwSt = lookup table ไม่ใช่ hardcode
• Receipt 2 output: thermal printer (ESC/POS 58/80mm) + PDF email backup = redundancy
• Offline-first = ต่อเน็ตหลุดก็ขายได้, sync queue เมื่อกลับมา online (Supabase/Firebase)
• Keyboard shortcut: F1=search, F2=pay-cash, F3=pay-card, F12=close-shift = cashier speed +40%
⚠️ Cons
• Tax calculation ผิด = legal liability (audit) — เก็บ rate + rounding rule + tax ID ต่อ transaction
• Hardware integration: barcode scanner = HID keyboard input (ต้อง focus <input>), receipt printer = WebUSB/Serial
• PCI DSS scope: รับบัตรเครดิต = tokenize ผ่าน Stripe/Omise (ห้ามเก็บ PAN ใน localStorage เด็ดขาด)
• Refund/exchange: original transaction id lookup + negative line items + restock (inventory sync)
• Shift close: cash count vs system total = ±฿5 acceptable, ±฿100 = investigation (audit trail)
• Offline sync conflict: 2 cashier same product sale while offline = negative stock, alert on sync
⚡ Gotchas
• iOS 17+: WebUSB block ใน Safari (เฉพาะ Chrome Android/Desktop) — fallback Bluetooth ESC/POS printer
• Tax rounding: TH 7% on 99.50 = 6.965 → display 6.97 vs round-down 6.96 (POS choice), เลือก 1 วิธีทั้งระบบ
• Receipt font: thermal printer 58mm = 32 chars/line, 80mm = 42 chars/line, word-wrap ตามนี้
• Cash payment change: amount - sum(items) แสดง realtime, ป้องกัน over-tender typo
• Card payment flow: tap → wait → print slip → 'approved?' confirm → receipt (timeout 30s = cancel)
• Tip screen: 15/18/20/25% preset + custom, หลัง payment success, ก่อน receipt (US/UK common)
• Discount: line-level (item 10% off) vs cart-level (cart 100฿ off) vs BOGO — apply ตามลำดับ
• Audit log: every cart add/remove/price change เก็บ + user id + ts (7 years retention ในบางประเทศ)
🚫 Anti-patterns
• เก็บ credit card PAN ใน localStorage — PCI DSS violation, fine $5k-100k/mo, ใช้ Stripe Element/Omise tokenize
• Float money = Number — 0.1 + 0.2 = 0.30000000000000004 bug, ใช้ integer cents (satang) หรือ Dinero.js
• Print receipt แล้วลบ cart ทันที — printer fail = no receipt, await print ack แล้วค่อย clear
• Re-use same order id ใน refund — generate new id + 'originalRef: xyz' link, เก็บ lineage ตลอด
• Use <form> submit reload page เพื่อ confirm payment — SPA + optimistic UI + rollback on error
🔀 Alternatives
• Square (closed, paid)
• Stripe Terminal (cloud-first)
• Vend (heavy, multi-store)
5.6 📦 Inventory (SKU + Stock Alert) Business Apps Intermediate ⏱ 3 hours

SKU + quantity + reorder point + low-stock alert + multi-location. Pos-grocery + noodle-shop ใช้พื้นฐานนี้ในการจัดการ stock

🎯 When to use: Retail store, warehouse mgmt, restaurant ingredient tracking, e-commerce fulfillment, pos-grocery ingredient + product
✅ Pros
• Reorder point = avg_daily_sales × lead_time_days + safety_stock = auto trigger PO ก่อน stockout
• SKU encoding: 'CAT-COL-SIZE' (category-color-size) = sort + filter ง่าย, barcode prefix map 1-1
• Low-stock alert ผ่าน Web Push/email = proactive ไม่ใช่ reactive (หลังของหมด)
• Multi-location (warehouse/store) = transfer stock in/out, inter-location balance
• Stocktake: physical count vs system = variance report (shrinkage/theft tracking)
• Batch/expiry tracking = FEFO (first-expiry-first-out) ใน food/pharma (ลดของเสีย)
⚠️ Cons
• Race condition: 2 cashier deduct same item = negative stock, ต้อง atomic UPDATE ... WHERE qty >= n
• Multi-location sync: transfer A→B ต้อง transaction 2-step พร้อม rollback ถ้า fail ฝั่งหนึ่ง
• FIFO/LIFO/Average cost: เลือก 1 วิธีต่อ org เปลี่ยนยาก, COGS calculation ต้อง consistent
• Negative stock prevention: ตรวจก่อน deduct แต่ถ้า offline 2 cashier = bypass, reconcile ตอน sync
• Barcode collision: 2 product share barcode (supplier mistake) = scanner ambiguity, manual override
• Scale: 100k SKU = full table scan ช้า, ใช้ index (sku, location, expiry) + materialized view
⚡ Gotchas
• Atomic decrement: SQL `UPDATE stock SET qty = qty - 1 WHERE sku=? AND qty > 0` — affected rows = 0 = out of stock
• Reorder point formula: (avg_30d × lead_time_days) + (1.65 × stddev_daily) = 95% service level
• Stock adjustment reason code: 'recount', 'damage', 'theft', 'expired' = audit trail (ตาม ภ.พ.บัญชี)
• Currency cost = average moving: (old_qty × old_cost + new_qty × new_cost) / total_qty (perpetual avg)
• Negative prevention in JS: check qty >= n ใน transaction ไม่พอ, server-side check authoritative
• iOS: barcode via camera ใช้ BarcodeDetector API + ZXing-js fallback (iOS 17+ native support)
• Expiry: FEFO sort expiry ASC, pick min expiry, alert 30/7/1 day before expired
• Offline queue: cart.add() เก็บใน IndexedDB เป็น 'pending txn' → sync เมื่อ online → resolve conflict
🚫 Anti-patterns
• เก็บ stock ใน localStorage แล้ว multi-cashier = state divergence, ใช้ server (Postgres + RLS) เป็น source of truth
• Sync stock ทันทีที่ add to cart (reserve) — UX ดี แต่ timeout/cancel ต้อง release (cron job)
• Reorder alert แบบ 'qty < 10' fixed — seasonal item (ice cream winter) = overstock, ใช้ moving average
• Track cost เป็น float — 0.1+0.2 bug, ใช้ BigInt satang (TH) หรือ integer cents (USD)
• Show 'In stock' label ตอน qty=0 — explicit 'Out of stock' + hide 'Add to cart' button
🔀 Alternatives
• Sortly (paid SaaS)
• inFlow (desktop only)
• Zoho Inventory (full ERP)
5.7 📝 Form Builder Business Apps Intermediate ⏱ 3 hours

User-defined schema (fields array) → render form + validate + save JSON. Schema-as-data = no-code สำหรับ non-tech user

🎯 When to use: Survey, configurable admin form, dynamic intake (medbook pre-screening), quiz/exam builder, lead-gen form, knowledge-garden schema
✅ Pros
• Schema as JSON = version control friendly (git diff), export/import, clone form
• Validation rules ใน schema (required, min, max, regex) = 1 source of truth, render + validate ใช้กฎเดียวกัน
• Field types: text/email/number/date/select/radio/checkbox/file/signature = 9 types ครอบ 90% use case
• Drag-drop field reordering = native HTML5 DnD หรือ react-beautiful-dnd (~50KB)
• Conditional logic: 'show field B if A === "yes"' = schema rule, render evaluation in 5 lines
• JSON export → Google Forms / Typeform alternative = self-hosted, no per-response fee
⚠️ Cons
• Conditional logic ซับซ้อน: A && (B || C) → !D = parser เอง 50+ LOC, ใช้ json-rules-engine (~80KB)
• Field type จำกัด: ไม่มี rich text/multi-step/calculated field ใน 1-day build, ต้อง extension point
• Validation 2 ชั้น: client (UX ทันที) + server (authoritative) — ห้ามพึ่ง client เดียว
• File upload: schema declared แต่ storage (S3/Supabase Storage) ต้อง wiring เพิ่ม แยก
• Multi-page form = state mgmt ยากขึ้น (state เก็บไหน ระหว่าง page), context API หรือ store
• i18n: field label/error message = 2-3 ภาษา = schema ใหญ่ขึ้น 2-3x, ใช้ i18n key + lookup table
⚡ Gotchas
• Unique field name: enforce ใน builder UI ก่อน save (duplicate name = render conflict)
• Field id = stable key (uuid) ไม่ใช่ label — เปลี่ยน label ในภายหลัง ไม่ break submission data
• Validation timing: onBlur (per field) + onSubmit (all) — onChange รำคาญ user
• Required + initial empty: ใช้ empty string check ไม่ใช่ null check (form input default)
• Date input: <input type='date'> return 'YYYY-MM-DD' (no TZ) ≠ Date object, เก็บ string หรือ parse เป็น UTC
• Signature canvas: Pointer Events ต้อง touch+mouse, pressure ไม่ support (iOS Safari quirk)
• File upload preview: URL.createObjectURL(file) — revoke เมื่อ unmount มิงั้น memory leak
• Submission quota: form 100k response = DB 1GB+ (ถ้าเก็บ JSON blob), ใช้ normalized table แทน
🚫 Anti-patterns
• Render form ด้วย eval('if ('+condition+') ...') — XSS vector, ใช้ json-rules-engine หรือ custom AST
• Save form schema เป็น JavaScript object literal ใน code — version control nightmare, JSON ใน .json file
• Validate ฝั่ง client อย่างเดียว — DevTools bypass ได้, server validate เสมอ (Zod, Joi, Yup)
• เก็บ submission เป็น JSON blob ใน 1 column — query/aggregate ทำไม่ได้, ใช้ normalized table (field_value ต่อ row)
• ใช้ innerHTML render user-supplied field label — XSS, ใช้ textContent หรือ DOMPurify sanitize
🔀 Alternatives
• Form.io (heavy enterprise)
• SurveyJS (paid)
• react-hook-form (React-only)
5.8 📊 Dashboard Charts (ECharts) Business Apps Beginner ⏱ 30 min

ECharts interactive (line/bar/pie/scatter/heatmap) + responsive + a11y palette. Ai-data-analyzer ใช้พื้นฐานนี้

🎯 When to use: Admin dashboard, KPI visualization, sales report, data exploration, ai-data-analyzer + flowboard analytics
✅ Pros
• ECharts ~1MB gzipped = 30+ chart types (line/bar/pie/scatter/heatmap/sankey/treemap) ใน bundle เดียว
• Responsive default: resize ไม่ต้อง setOption ใหม่, container query ใช้ได้
• Color-blind safe palette (ColorBrewer) = a11y + สวย, contrast ratio 4.5:1 ผ่าน WCAG AA
• Data zoom + brush select = interactive exploration (zoom range, select subset, drill-down)
• Tooltips rich: HTML content, formatter function, dynamic per series (currency/percent/ratio)
• Theme JSON: switch dark/light 1 line (echarts.init(el, 'dark')), brand color override
⚠️ Cons
• ECharts 1MB gzipped หนักกว่า Chart.js (~150KB) — ใช้ light build หรือ import เฉพาะ type ที่ใช้
• Accessibility: ECharts = canvas-based, screen reader ไม่อ่าน (ต้อง aria-label + data table alternative)
• Performance 1k+ series points: downsample ก่อน render (LTTB algorithm) หรือ dataZoom default = 1st 100
• Mobile touch: tooltip ตามนิ้ว แต่ pinch-zoom ต้อง enable: {x: true, y: false} เพื่อ UX ดี
• 3D charts: ใช้ WebGL แต่ performance drop บน mobile 5-10x เทียบ 2D
• Export PNG/SVG: client-side ใช้ echarts.getDataURL() แต่ resolution = screen, PDF = ink scale
⚡ Gotchas
• Resize observer: ECharts ไม่ auto-detect sidebar toggle, wrap container ใน ResizeObserver
• Color palette: ใช้ colorblind-safe อย่าง Wong/Okabe-Ito (8 colors) แทน rainbow default
• Tooltips: trigger='axis' สำหรับ multi-series, 'item' สำหรับ single point, เลือกตาม use case
• Animation: animationDuration 800ms default, ปิด 0 เมื่อ redraw บ่อย (real-time dashboard)
• Stacked bar: stack: 'total' ทุก series ในกลุ่มเดียวกัน, ข้าม series = grouped bar แทน
• Negative value: y-axis 'dataMin' ไม่ใช่ 0, ใช้ yAxis.min = 'dataMin' (auto-fit)
• Time axis: xAxis.type='time' + ISO string, ห้าม Date object (timezone bug)
• iOS Safari: canvas chart ไม่ support backdrop-filter blur — ใช้ solid background tooltip
🚫 Anti-patterns
• Render chart ใหม่ทั้ง option ทุก data update — merge เฉพาะ series.data ใช้ setOption({series:[{data:newData}]}, {notMerge:false})
• ใช้ 3D pie chart — 2D pie + label better, 3D distort perception (most data viz research)
• Pie chart > 5 slices — ใช้ bar chart แทน, pie ใช้ได้ 2-3 categories เท่านั้น
• Skip axis label (อ่านยาก) เพื่อ 'สวย' — label every nth + tooltip detail, a11y > aesthetic
• Load ECharts ทั้ง bundle — ใช้ tree-shake import (import * as echarts from 'echarts/core'; import {BarChart} from 'echarts/charts')
🔀 Alternatives
• Chart.js (lighter, simpler)
• D3.js (low-level, flexible)
• Plotly.js (3D, scientific)
• Recharts (React-only)
5.9 🧾 Invoice (PDF) Business Apps Intermediate ⏱ 2 hours

jsPDF client-side (zero install) + Thai font embed (Sarabun) + line items + tax + auto-numbering. Pos-grocery ใช้ receipt pattern เดียวกัน

🎯 When to use: Freelancer billing, SMB invoice, retail receipt, e-ticket, contract, pos-grocery receipt, certificate of completion
✅ Pros
• jsPDF client-side = no server cost, GDPR-friendly (data ไม่ออกจาก browser), offline-capable
• Thai/VI/ZH/JA/KR font embed base64 = ใบเสร็จ/ใบแจ้งหนี้ภาษา local render ถูกต้อง
• PDF is final-form: recipient เปิดใน Adobe/Preview/browser ได้, ไม่ต้อง install
• Auto-numbering INV-2024-001 → INV-2024-002 = watermark + sequential + year prefix
• Tax breakdown: subtotal + VAT line (7% TH, 19% DE) + total = audit-ready, ไม่ใช่แค่ราคารวม
• QR code payment (PromptPay TH): jsPDF + qrcode.js = scan จ่ายได้ทันที (1-2 second transfer)
⚠️ Cons
• Email deliverability: invoice PDF เข้า spam 50%+ ถ้า SPF/DKIM/DMARC ไม่ตั้ง — ใช้ SendGrid/Resend
• CJK font base64 = 1-3MB inline → PDF ใหญ่ขึ้น, แต่ละ font subset ต้อง license (Sarabun OFL ok)
• jsPDF no CSS engine: ต้องคำนวณ position เอง (x, y, fontSize), ไม่มี flexbox/grid
• Recurring monthly invoice: client-side generate 12 PDF ต่อเดือน = UX ลำบาก, ใช้ Stripe/Omise subscription
• Late fee / interest calc: business rule แต่ละที่ต่างกัน, ต้อง config layer
• Digital signature: PDF เซ็นชื่อ legally = X.509 cert + pkcs12 (server-side sign), ไม่ใช่ jsPDF
⚡ Gotchas
• Thai font broken: jsPDF default Helvetica ไม่รองรับ unicode ภาษาไทย, embed Sarabun/Noto Sans TH base64
• CORS: external image (logo) ต้อง fetch → toDataURL ก่อน addImage มิงั้น canvas taint
• jsPDF unit: 'mm' (default 'pt') — A4 = 210×297mm, ใช้ mm อ่านง่ายกว่า pixel
• Page break: invoice 20+ items = overflow 1 page, เช็ค y > 280mm แล้ว doc.addPage()
• Font subset: โหลด Sarabun เต็ม 3MB เพื่อ 50 คำ = สิ้นเปลือง, ใช้ pyftsubset ตอน build
• QR code size: 25×25mm = scan ได้ทันที, 20mm = scan fail บน iPhone บางรุ่น
• Tax rounding: 99.50 × 1.07 = 106.465 → display 106.47 vs 106.46 = เลือก rounding mode 1 ที่เดียว
• iOS 17 Mail preview: PDF เปิด inline ถ้า < 5MB, > 5MB = download prompt (size cap)
🚫 Anti-patterns
• Upload HTML ไป server, render ด้วย Puppeteer, ส่ง PDF link — slower, server cost, no offline
• Embed base64 ใน HTML แล้ว browser print to PDF — ไม่มี line items table, format ไม่ consistent
• Generate invoice แล้ว save as Blob URL ตลอดไป — URL.revokeObjectURL หลัง download เพื่อ free memory
• Use Times New Roman default = ไม่ render ภาษาไทย, embed Sarabun ก่อน text() เสมอ
• Re-generate PDF ทุกครั้งที่ view = cache (IndexedDB blob) ครั้งแรก, serve cache ครั้งต่อไป
🔀 Alternatives
• pdfmake (declarative, larger)
• pdf-lib (edit existing)
• react-pdf (React-only)
• Puppeteer (server, full HTML)
5.10 📈 Analytics Business Apps Intermediate ⏱ 2 hours

Capture events + funnels + retention + cohort + anonymize IP + batched send. Ai-data-analyzer ใช้ self-hosted analytics เป็น layer หนึ่ง

🎯 When to use: Product analytics, marketing site conversion funnel, A/B test results, feature adoption tracking, SaaS retention curve
✅ Pros
• Funnel analysis: signup → activate → purchase = conversion % แต่ละ step, drop-off insight
• Retention cohort: week-0 100% → week-4 35% = visualize churn ใน heatmap (ผู้ใช้ return แค่ไหน)
• Event batching (10 events ต่อ 1 request) = 10x network efficiency vs fire-each-event
• Anonymize IP: strip last octet (192.168.1.xxx) = GDPR Art. 5 'data minimization' compliant
• Self-hosted (PostHog/Plausible) = ไม่ต้องพึ่ง GA4 cookie banner, EU data residency
• Funnel + cohort + path = 3 query pattern ครอบ 80% product decision making
⚠️ Cons
• GDPR cookie banner: GA4/Facebook Pixel ใช้ cookie = EU ต้อง consent (opt-in), โอกาส 30% user decline
• Data warehouse cost: 100M events/month = $200-500/mo (BigQuery/Snowflake), ใช้ ClickHouse self-host ถูกกว่า 5x
• Real-time (sub-second) = Kafka + ClickHouse + materialized view, ไม่ใช่ Postgres + cron
• Privacy: fingerprinting (canvas + audio + fonts) ผิด GDPR — ใช้ server-set first-party cookie เท่านั้น
• Funnel re-order: user ข้ามขั้น (signup → purchase โดยไม่ผ่าน onboarding) = funnel undercount
• Identity stitching: anonymous → user after login = event merge heuristic (IP+UA+session) ไม่แม่น 100%
⚡ Gotchas
• Send on beforeunload: fetch with keepalive:true ไม่ cancel เมื่อ tab close (event ไม่หาย)
• Batch size: 10-20 events ต่อ 1 POST = balance latency vs request count (10 = 100KB, 1 = 10KB)
• Anonymize IP client-side: ip.split('.').slice(0,3).join('.') + '.0' (IPv4 only, IPv6 ต้อง /48 mask)
• Session ID: crypto.randomUUID() เก็บ sessionStorage (ลบเมื่อ tab close) = 1 session = 1 funnel entry
• User ID หลัง login: set(userId) merge anonymous + identified events (PostHog distinct_id pattern)
• Sampling: 100% event ตอน dev, 10% production (เก็บ sample rate flag ใน event meta)
• Time zone: event.ts = Date.now() UTC ms, แสดง user TZ ใน dashboard (ไม่เก็บ local)
• Schema migration: เพิ่ม property ใหม่ต้อง default null ใน query เก่า (no breaking change)
🚫 Anti-patterns
• Track every click — noisy, ใช้ auto-track เฉพาะ data-attribute (data-track='cta-click') explicit
• Block analytics script ด้วย cookie gate ก่อน consent — script load delay = first event miss 100%
• ใช้ Google Analytics แล้ว assume 'anonymous IP' = GDPR safe — GA4 still need consent, EU DPA ฟ้องได้
• Fire event ผ่าน setInterval flush ทุก 1s — burst 100 events = 100 req, batch 10 = 10 req
• เก็บ PII (email, name) ใน event property — anonymize เฉพาะ aggregate metric, ไม่ raw event
🔀 Alternatives
• PostHog (self-host OSS)
• Plausible (privacy-first paid)
• GA4 (free, cookie banner)
• Mixpanel (mobile-focused paid)
6.1 🎵 Audio Editor (Waveform) Media Apps Intermediate ⏱ 4 hours

Visual waveform + region trim + fade in/out. WaveSurfer.js v7 ships regions + MP3 decode + 50KB footprint.

🎯 When to use: Podcast editor, music trimmer, karaoke timing, voice memo cleanup, SFX browser ใน CodeHub (karaoke-studio ใช้แพทเทิร์นเดียวกัน)
✅ Pros
• AudioContext.decodeAudioData() ให้ Float32 PCM peaks ทันที ไม่ต้อง parse WAV header เอง
• Region select = trim/fade โดยไม่ต้อง re-encode เพราะใช้ <audio> element seek
• WaveSurfer v7 มี Regions/Timeline/Minimap plugin ใน bundle เดียว ~50KB gzipped
• Web Audio API playback = sample-accurate scrub, สามารถใส่ gain node ต่อ region ได้
• ทำงานบน mobile ได้ (iOS Safari รองรับ decodeAudioData ตั้งแต่ iOS 13)
• Export MP3 ผ่าน lamejs (~30KB) ไม่ต้องส่งไป server
⚠️ Cons
• decodeAudioData() ใช้ memory = file_size × 4 (Float32) → ไฟล์ 50MB ใช้ RAM 200MB
• WaveSurfer render ใช้ canvas ขนาด 1:1 กับ pixels → 4K width = slow on mobile
• ไม่มี built-in effect chain (compressor/EQ) — ต้องเขียน AudioWorklet เอง
• Cannot open .aiff/.flac บน Safari (MP3/WAV/AAC/OGG เท่านั้น)
• Visual seek ไม่ sample-accurate ถ้าไฟล์ VBR MP3 (seek table ผิด)
• iOS background tab = audio pause ทันที (autoplay policy)
⚡ Gotchas
• iOS Safari: user gesture (tap) ต้องเกิดก่อน audio.play() ไม่งั้น block + console error
• decodeAudioData() returns Promise — ห่อ try/catch เพราะ CORS file จะ throw โดยไม่บอกรายละเอียด
• CORS: cross-origin audio ต้องมี Access-Control-Allow-Origin หรือ response เป็น opaque (แล้ว decodeAudioData จะล้มเหลว)
• Web Audio context = single instance per page สร้างซ้อนกันไม่ได้ (resume() หลัง user gesture)
• MP3 VBR seek ไม่แม่น → ใช้ peaks pre-computed แทนการ random-access
• Canvas resize during playback = waveform ต้อง redraw (debounce 100ms)
• Region drag บน touch device = pointer events ต้องใช้ PointerEvent ไม่ใช่ touchstart
• getByteFrequencyData ต้องเรียกหลัง analyser.getFloatTimeDomainData ทุก frame
🚫 Anti-patterns
• Parse WAV header เอง (RIFF chunk) เพื่ออ่าน samples — ใช้ decodeAudioData() แท้ๆ ทำได้ใน 1 บรรทัด
• Render waveform ทุก animation frame — peaks ไม่เปลี่ยน decode ครั้งเดียว วาดครั้งเดียว
• ใช้ setInterval(ontimeupdate) — ใช้ AudioContext.currentTime เป็น master clock แทน
• เก็บ original audio ใน <audio> + AudioBuffer ซ้ำกัน — ใช้ AudioBufferSourceNode.playbackRate แทน
• Export โดย upload file → server → process — ใช้ lamejs (MP3) หรือ WebM Opus encode client-side
🔀 Alternatives
• Tone.js (effect chain, larger 200KB)
• Howler.js (playback only, no waveform)
• Peak.js (BBC, frame-accurate but ~150KB)
6.2 ▶️ Custom Video Player Media Apps Intermediate ⏱ 3 hours

HTML5 <video> + custom controls + playlist + speed + WebVTT subs + Picture-in-Picture. ~30KB zero-dep.

🎯 When to use: Learning platform, social feed video, portfolio reel, internal training, embedded player สำหรับ karaoke-studio / sj88-video-editor-pro previews
✅ Pros
• Native <video> = zero decode cost, GPU compositor, ใช้ codec ของ OS (H.264/H.265/AV1/VP9)
• WebVTT via <track> = captions ทำงานทันที, screen readers อ่านได้ (a11y boost)
• Picture-in-Picture API (Chrome/Safari) = ลอย video บน desktop ขณะ multitask
• PlaybackRate 0.0625–16 = useful สำหรับ tutorial (slow-mo) และ skip-through
• MediaSession API ติดปุ่ม lock-screen controls (iOS/Android) = native feel
• No build, no framework, ship <video>+<script> ได้เลย
⚠️ Cons
• DRM/encrypted streams ต้อง MSE + EME + HTTPS + license request (CORS nightmare)
• iOS Safari: playsinline attribute ต้องใช้ มิงั้น fullscreen อัตโนมัติ
• Autoplay policy: Chrome block autoplay จนกว่า user gesture + muted หรือ media engagement
• Quality switching ต้อง MSE + manifest (HLS/DASH) ไม่ใช่ <video src> ธรรมดา
• Subtitle styling จำกัด (ไม่รองรับ ::cue pseudo บน legacy Edge)
• Seek precision ขึ้นกับ keyframe interval — VBR content seek ±2-5s
⚡ Gotchas
• iOS playsinline: <video playsinline webkit-playsinline> ไม่งั้น iPhone fullscreen ทันทีเมื่อ play
• preload='metadata' = โหลดแค่ header (fps/duration) ลด bandwidth 90% เทียบ preload='auto'
• currentTime = 0.001 trick เพื่อ force thumbnail แสดง (Chrome bug บาง version)
• onloadedmetadata ก่อน oncanplay — ใช้ loadedmetadata เพื่อ get duration
• WebVTT cues ใช้ timestamp HH:MM:SS.mmm — ไม่มี frame number (VTT ไม่ใช่ frame-based)
• iOS volume control ผ่าน JS ไม่ได้ (security) — ใช้ MediaSession metadata แทน
• Track element default ไม่แสดง — ต้อง track.default = true (default attribute)
• Black frame ตอน seek = post-processing keyframe ใหม่, ใช้ requestVideoFrameCallback (Chrome) detect
🚫 Anti-patterns
• สร้าง custom decoder/encoder — browser มี HW-accelerated decoder แล้ว ใช้ <video> ดีกว่า
• ใช้ <iframe YouTube> เพื่อ 'custom' UI — track events ไม่ได้, no offline, branding
• Force flash fallback — Flash เลิกใช้ 2017 แล้ว
• Re-render controls ทุก video state change — ใช้ CSS class toggle แทน (reflow 5x น้อยกว่า)
• Bind onwheel บน progress bar เพื่อ scrub — conflict กับ page zoom, ใช้ pointer events
🔀 Alternatives
• Video.js (full player ~200KB, plugins)
• Plyr (lightweight ~30KB, opinionated)
• HLS.js (live streaming, MSE-based)
6.3 🎥 Video Editor (Timeline) Media Apps Advanced ⏱ 2 weeks

Multi-track timeline + clip trim/ripple + transitions + WebCodecs decode + ffmpeg.wasm export. Reference: sj88-video-editor-pro.

🎯 When to use: Short-form social video, course/tutorial editor, podcast clipper, meme-maker, sj88-video-editor-pro (CapCut-style) ใช้พื้นฐานนี้
✅ Pros
• WebCodecs API (Chrome 94+, Edge) = hardware decode/encode VideoFrame โดยตรง, 5-10x เร็วกว่า <video>+canvas
• ffmpeg.wasm 30MB single-file = MP4/H.264 export client-side ไม่ต้อง server
• Canvas 2D + WebGL shader = transition/effect chain render ที่ 60fps ได้
• IndexedDB blob storage = project file เก็บในเบราว์เซอร์, export MP4 ตอน save
• Multi-track = audio หลายชั้น + video overlays (text/sticker) ใน timeline เดียว
• Keyframe interpolation (linear/ease/bezier) ใน JS = animation ลื่น ไม่ต้อง After Effects
⚠️ Cons
• WebCodecs = Chrome/Edge only (Safari 16.4 partial, Firefox behind flag) — fallback <video>+canvas ช้ากว่า 5x
• ffmpeg.wasm 30MB initial load = first export รอ 5-10s ขณะ WASM compile
• 4K60 timeline = 8GB RAM (VideoFrame pool ไม่คืน memory ทันที)
• Audio sync drift: media time ≠ wall clock เมื่อ render นาน > 30s (frame counter mismatch)
• No GPU shader caching: effect chain 8 filters = recompile shader ทุก frame ถ้าไม่ memoize
• Project state = JSON ไม่พอ — binary blobs (raw frames, audio) เก็บแยก IndexedDB
⚡ Gotchas
• WebCodecs VideoFrame.close() = ต้องเรียนด้วยตัวเอง มิงั้น GPU memory leak 1GB/min
• ffmpeg.wasm: SharedArrayBuffer ต้อง COOP/COEP headers (cross-origin isolated)
• Frame timing: requestVideoFrameCallback() ให้ mediaTime จริง (ไม่ใช่ performance.now)
• Audio drift: AudioContext.currentTime - video.currentTime ตรวจทุก 5s แล้ว resync
• Canvas captureStream(30) = 30fps cap — render 60fps เสียเปล่า, ใช้ VideoEncoder ตรงๆ ดีกว่า
• IndexedDB quota ≈ 60% disk → video project เกิน 2GB จะ quota error
• Seeking = dispose VideoFrame ทั้งหมดก่อน, decode ใหม่ (ทำลาย seek perf)
• WebGL texture upload = bottleneck ถ้าไม่ใช้ texImage2D จาก VideoFrame (vs HTMLVideoElement)
🚫 Anti-patterns
• ใช้ MediaRecorder + canvas.captureStream() เพื่อ export — WebCodecs VideoEncoder เร็วกว่า 10x, ได้ keyframe control
• Render ทุก frame ทั้ง timeline — pre-compute static segments, render dynamic เฉพาะส่วนเปลี่ยน
• เก็บ project ใน localStorage 5MB limit — ใช้ IndexedDB สำหรับ binary, JSON สำหรับ metadata
• Build timeline DOM = div ต่อ frame — ใช้ canvas เดียว (1 clip = 1 region) ลด DOM ลง 1000x
• Sync audio ผ่าน <audio>.currentTime = video.currentTime — drift ทันที, ใช้ AudioContext destination + GainNode
🔀 Alternatives
• Remotion (React, server-render only)
• MLT/Melted (C++ pipeline, native only)
• CapCut Web (closed source)
6.4 🖼️ Image Editor (Crop/Filter) Media Apps Intermediate ⏱ 4 hours

Canvas 2D + WebGL filters (brightness/contrast/blur) + sticker overlay + text. Reference: fitcheck, cutout-studio, palette.

🎯 When to use: Profile pic cropper, sticker maker, meme layer, product photo (fitcheck, cutout-studio, palette ใช้พื้นฐานนี้), social post composer
✅ Pros
• ctx.filter='brightness(1.2)' = CSS-level effect 0-line, GPU-accelerated
• OffscreenCanvas + Worker = filter 4K image โดยไม่ block main thread (responsive UI)
• WebGL fragment shader = 50+ custom filters (sepia/glitch/vignette) ที่ 60fps
• createImageBitmap() = decode 12MP JPEG ใน ~80ms (vs <img> 200ms)
• toBlob('image/webp', 0.9) = 30-50% smaller output กว่า JPEG เทียบ visual quality
• Sticker/text layer = transform matrix (translate/rotate/scale) ผ่าน canvas.setTransform
⚠️ Cons
• Canvas 2D filter quality ต่ำกว่า Photoshop (no 16-bit, no CMYK, limited blend modes)
• WebGL shader ต้อง recompile ทุกครั้ง filter เปลี่ยน — memoize program ด้วย string key
• EXIF orientation: <img> ไม่หมุนอัตโนมัติ, ต้อง parse EXIF หรือ createImageBitmap({imageOrientation:'from-image'})
• 4K image (3840×2160) × Float32 RGBA = 130MB GPU memory — tile-based render แทน
• Browser max canvas = 16384×16384 บน Chrome / 4096×4096 iOS — resize ก่อน drawImage
• iOS Safari: OffscreenCanvas + Worker ตั้งแต่ 16.4, bug บน 14-15
⚡ Gotchas
• iOS rotation bug: iPhone photo ถ่าย portrait แต่ EXIF landscape → drawImage ออกมาตะแคงข้าง
• Workaround: createImageBitmap(blob,{imageOrientation:'from-image'}) แล้ว draw แบบ bitmap
• ctx.filter ต้อง set ก่อน drawImage ไม่งั้นไม่ apply
• WebGL texImage2D from HTMLImageElement = upload CPU-side pixel (slow) → ใช้ texImage2D(target,level,format,bitmap) แทน
• iOS Safari: canvas.toBlob() = 'image/png' เท่านั้น (ไม่รองรับ webp ในบาง version)
• Filter pipeline: ต้อง save/restore context เพื่อไม่ให้ filter leak ไป layer ถัดไป
• Crop aspect ratio: devicePixelRatio 2x retina → canvas ใหญ่กว่าที่ตาเห็น 2 เท่า (handle ก่อน export)
• Undo/redo = เก็บ ImageData snapshot → 50MB per layer × 20 steps = OOM, ใช้ Command pattern (delta) แทน
🚫 Anti-patterns
• Stack CSS filter + ctx.filter = double apply (CSS คือ compositing pass, ไม่ใช่ effect layer)
• ใช้ base64 string แทน Blob URL = RAM x4, GC pressure สูง, ใช้ URL.createObjectURL(file)
• เก็บ canvas state ใน JSON serialization (toDataURL ใน array) — base64 33% bigger, ใช้ OffscreenCanvas snapshot
• Read EXIF manual เพื่อ orientation — createImageBitmap options ทำให้แล้ว 1 บรรทัด
• Re-render full canvas ทุก slider change — debounce 16ms (1 frame) หรือ in-place filter chain
🔀 Alternatives
• Fabric.js (object-oriented, ~300KB)
• Konva.js (DOM-based, ~150KB)
• P5.js (creative coding, ~700KB)
6.5 📄 PDF Generation Media Apps Intermediate ⏱ 1 day

jsPDF client-side (zero install) vs WeasyPrint/Puppeteer server-side (HTML-to-PDF). Reference: pos-grocery receipt, nesspaper-style.

🎯 When to use: Invoice/receipt (pos-grocery), report, certificate, contract, ticket, e-book, nesspaper-style newspaper layout
✅ Pros
• jsPDF ฝั่ง client = no server cost, GDPR-friendly (data ไม่ออกจาก browser), offline-capable
• Puppeteer/WeasyPrint server = full HTML/CSS (flexbox, grid) → PDF ตรง design 100%
• jsPDF font embedding base64 = CJK (TH/VI/ZH/JA/KR) render ถูกต้อง (ฟอนต์ Sarabun, Noto Sans TH)
• pdf-lib client-side = edit existing PDF (add page, fill form, merge) โดยไม่ round-trip server
• Puppeteer headless Chrome = same engine as user's browser = pixel-perfect print
• jsPDF + html2canvas = rasterize HTML div → image → embed (escape hatch เมื่อ layout ซับซ้อน)
⚠️ Cons
• jsPDF: ไม่มี CSS engine — ต้องคำนวณ position เอง (x,y,fontSize), ไม่มี flexbox/grid
• Puppeteer headless Chrome = 200-300MB RAM per process, เปิดใหม่ 1-2s per PDF (cold)
• CJK font base64 = 1-3MB inline → PDF ใหญ่ขึ้น, แต่ละ font subset ต้อง license (Noto OFL ok)
• Puppeteer render = blocking event loop ใน Node (--no-sandbox แล้ว security risk)
• html2canvas + jsPDF = rasterize = text ไม่ selectable, ไฟล์ใหญ่, ไม่ accessible
• jsPDF autoTable plugin ไม่รองรับ colspan/rowspan ซับซ้อน, header repeat across pages เอง
⚡ Gotchas
• CORS: external image ใน jsPDF ต้อง fetch → base64 (jsPDF.addImage base64 only)
• Thai text broken = ใช้ฟอนต์ที่ไม่มีสระลอย (Sarabun, Noto Sans Thai) แล้ว embed เป็น .ttf base64
• Puppeteer sandbox: Docker ต้อง --no-sandbox flag หรือ seccomp profile เพื่อหลีกเลี่ยง crash
• jsPDF: addFont() ต้องเรียกก่อน doc.text() มิงั้น default Helvetica ใช้แล้ว text ผิด encoding
• Font subsetting: jsPDF โหลดฟอนต์เต็ม 3MB เพื่อคำ 5 คำ — ใช้ pyftsubset ฝั่ง build pipeline
• A4 = 595×842 pt (not px!) — convert px@96dpi ด้วย 595/96 = 6.2x scale factor
• Puppeteer page.pdf() ไม่ render @media print stylesheet เสมอ — ใช้ emulateMedia('print')
• WeasyPrint: ไม่รองรับ JS execution (Python+CSS only) — สำหรับ static layout เท่านั้น
🚫 Anti-patterns
• ใช้ html2canvas + jsPDF แทน server-side — raster PDF ใหญ่ 10-20x, ไม่ selectable, accessibility แย่
• Load ฟอนต์จาก Google Fonts ใน jsPDF — CORS block ตอน runtime, embed base64 ตอน build แทน
• Spawn Puppeteer ต่อ request = 300MB process / 2s spinup — ใช้ pool (puppeteer-cluster) หรือ serverless browser
• Generate PDF เป็น base64 string เก็บ DB — base64 33% larger, ใช้ binary blob (BYTEA, S3)
• Assume jsPDF รู้จัก UTF-8 = false — ใช้ addFont() with 'utf-8' encoding parameter เสมอ
🔀 Alternatives
• pdfmake (declarative, ~500KB)
• pdf-lib (edit existing, ~400KB)
• react-pdf (React, server-side only)
6.6 📝 Lyric Video Maker Media Apps Intermediate ⏱ 3 hours

Karaoke-style timing editor: LRC import + click-to-set timing + animated highlight + canvas overlay + WebM export. Reference: karaoke-studio (6+ theme variants).

🎯 When to use: Music content creator, social lyric clip (TikTok/Reels), karaoke timing rehearsal, lyric translation video, karaoke-studio base
✅ Pros
• LRC format = universal (10+ tools export), แค่ [mm:ss.xx]text per line
• Click-to-set timing = กด spacebar ตอนฟัง + click line = sub-second timing precision
• Canvas overlay = render highlight color/bounce/glow ต่อ character (ไม่ใช่ทั้งบรรทัด)
• requestAnimationFrame + audio.currentTime = 60fps highlight scroll แม่นยำ
• WebM export via MediaRecorder + canvas.captureStream = no server, ~5MB/min 720p
• karaoke-studio 11 themes ใช้ pattern เดียวกัน (theme = JSON of CSS+canvas configs)
⚠️ Cons
• LRC ไม่มี syllable timing (ทั้งบรรทัดเปลี่ยนพร้อมกัน) — karaoke effect แท้ๆ ต้อง advanced LRC (Enhanced LRC / LRCv2)
• WebM export = Safari/iOS เปิดไม่ได้ (VP9) — ต้อง MP4 export แยก (ffmpeg.wasm)
• requestAnimationFrame หยุดเมื่อ tab background → timing drift (ใช้ AudioContext clock แทน)
• Subtitle 'bounce' effect ต้อง pre-compute keyframes (ไม่ใช่ random per frame)
• Vertical video (9:16 TikTok) export = re-author layout, WebM ใช้ 720x1280 ไม่ใช่ 1280x720
• Font loading race: โหลดฟอนต์ไม่เสร็จตอน render = fall back to default (text width เปลี่ยน)
⚡ Gotchas
• LRC tag order: [ti:title][ar:artist][al:album][by:creator] ต้องมาก่อน [mm:ss.xx] แรก
• Enhanced LRC: [mm:ss.xx]<mm:ss.xx>word<mm:ss.xx>word สำหรับ per-word highlight
• iOS Safari: ไม่ render ฟอนต์ใน <canvas> ถ้า @font-face ยังโหลดไม่เสร็จ (document.fonts.ready await ก่อน)
• AudioContext clock (currentTime) ไม่ drift แม้ background tab — ใช้เป็น timing master
• WebM MediaRecorder bitrate 2500kbps = 5min = 95MB, ใช้ VP9 + Opus 1500kbps ได้คุณภาพใกล้เคียง
• LRC timestamp regex: \[(\d+):(\d+\.?\d*)\] capture min and sec, validate mm<60 ss<60
• Sync offset: LRC อาจ offset +200ms เทียบ audio — ให้ user ปรับ global offset slider
• Export: render ผ่าน captureStream 30fps = encoding ใช้ CPU 100% บน laptop, เตือน user
🚫 Anti-patterns
• setInterval 16ms เพื่อ update highlight — drift 5s ใน 10 นาที, ใช้ rAF + audio clock
• ใช้ <video> + <track> WebVTT ทำ lyric video — WebVTT ไม่มี per-character style (CSS ::cue จำกัด)
• Pre-render ทุก frame เป็น <img> sequence — memory 1GB/30s, ใช้ canvas real-time render
• Convert LRC → JSON แล้วเก็บใน localStorage เป็น array — LRC string 50KB เปลี่ยน < 5KB JSON
• ใช้ CSS animation keyframes แทน rAF ใน WebM export — CSS anim ไม่ sync กับ audio clock
🔀 Alternatives
• Aegisub (native, desktop only)
• Subtitle Edit (Windows only)
• Hand-rolled WebVTT (simpler but no per-word)
6.7 🎤 Karaoke Studio (Full) Media Apps Advanced ⏱ 1 week

End-to-end: FFT beat detection (1024-pt) + 11 visual themes + per-word timing + MP4 export via ffmpeg.wasm. Reference: karaoke-studio.

🎯 When to use: Music creator publish karaoke video, social content (9:16 lyric clip), YouTube karaoke channel, karaoke-studio คือ reference implementation ของ pattern นี้
✅ Pros
• AnalyserNode FFT size 1024 = 43 beats/sec @ 44100Hz, balance precision vs CPU
• RMS threshold 0.55 + adaptive peak detection = beat sync ตามจังหวะเพลง 95%+
• 11 themes = JSON config (font/colors/animation curve) swap = render pipeline เดียว
• ffmpeg.wasm MP4 export = H.264/AAC browser-side, ไฟล์เปิดได้ทุก platform (ต่างจาก WebM)
• Per-word timing เก็บใน Enhanced LRC = karaoke effect จริง (highlight เคลื่อนที่ตามคำร้อง)
• Export 1080p60 = ~1.5GB RAM, mobile อาจ crash — auto downscale 720p30 fallback
⚠️ Cons
• ffmpeg.wasm = 30MB download, first export รอ 5-10s ขณะ WASM compile + COOP/COEP headers
• FFT 1024 = bass ไม่แม่น (low freq resolution 21Hz) — ใช้ 2048 สำหรับ beat-heavy song
• Beat detection ผิดที่ genre ไม่ใช่ 4/4 (jazz, classical, rubato) — เพิ่ม onset detection fallback
• MP4 export 3 นาที = 60-90s render time บน M1, 5-8 นาทีบน low-end laptop
• Per-word timing ต้อง manual ใส่ (ไม่มี auto-align client-side แม่น) — Whisper API ช่วยได้
• Audio mixing = Web Audio destination → MediaRecorder = mute issue ถ้า tab background
⚡ Gotchas
• SharedArrayBuffer: COOP: same-origin + COEP: require-corp header จำเป็นสำหรับ ffmpeg.wasm multi-thread
• FFT window: AnalyserNode.fftSize=1024, getByteFrequencyData ให้ 0-255 (map 0-1 RMS)
• RMS formula: sqrt(sum(sample^2)/N) — window 1024 sample overlap 50% = smoother beat
• Beat peak: rms[i] > max*0.55 AND rms[i] > localAvg*1.3 AND (i-prevBeat) > 0.2s (debounce)
• ffmpeg.wasm: -c:v libx264 -preset ultrafast -crf 23 -c:a aac -b:a 128k for browser MP4
• Canvas captureStream + MediaRecorder VP9 vs VP8 vs H.264 — เลือก H.264 ตอน export จริง (ffmpeg remux)
• AudioContext: createMediaStreamDestination() ต่อไป MediaRecorder + ffmpeg.wasm mux เข้าด้วยกัน
• Theme JSON: animation curve ease-out (cubic) = เนียนกว่า linear 5x, keyframe density 30/sec
🚫 Anti-patterns
• Beat detection ด้วย zero-crossing — ใช้ energy/RMS + spectral flux ดีกว่า 10x
• MP3 export ผ่าน lamejs — iOS/Safari เปิดไฟล์ได้ แต่ social media (TikTok/IG) ต้อง MP4/H.264
• Manual frame-by-frame highlight เก็บ 30fps × 180s = 5400 entries ใน JSON — ใช้ onset times + ease interpolation
• Export WebM แล้ว expect upload to YouTube — YouTube รับ WebM แต่ compress แย่กว่า MP4
• ใช้ setInterval 16ms draw highlight — background tab หยุด, ใช้ AudioContext clock + rAF
🔀 Alternatives
• Aegisub (native desktop)
• Karaoke Mug Editor (Windows)
• Vrew (AI auto-sub, closed source)
6.8 💬 Subtitle Editor (VTT/SRT) Media Apps Beginner ⏱ 2 hours

VTT/SRT editor + waveform sync + auto-time-stretch + bilingual. WebVTT for web, SRT for video file. Reference: sj88-video-editor-pro caption panel.

🎯 When to use: Course/tutorial (sj88-video-editor-pro), accessibility (a11y WCAG 2.1), multilingual video, social clip captions (TikTok/IG auto-import SRT)
✅ Pros
• WebVTT = W3C standard, native <track> support, ::cue pseudo-class for styling
• SRT = 99% player support (VLC, mpv, OBS), human-readable 4-byte timestamp
• Waveform thumbnail strip = visual cue place (เหมือน audio editor แต่ cue-based)
• Bilingual stack: <track srclang='en' label='English'> + <track srclang='th' label='ไทย' default>
• Import SRT → VTT converter ใช้ regex 1-pass, เก็บ cue ID auto-generate
• Export ใช้ Blob + download attribute = ไม่ผ่าน server, GDPR-safe
⚠️ Cons
• VTT cue settings (line:80%, position:50%) browser-specific render — test ทุก target
• SRT 1-based index, VTT optional cue ID = conversion ง่ายแต่ round-trip มี gotcha
• Long lines > 42 chars wrap ไม่สวย — ใช้ VTT line:13, position:50% center ดีกว่า
• Frame-accurate timing ไม่มีใน VTT/SRT (HH:MM:SS.mmm only) — video editor ใช้ FCPXML แทน
• Right-to-left (Arabic/Hebrew) ต้อง mark <c.langdir='rtl'> + reverse word order (FFmpeg ช่วย)
• Tag escape: VTT ห้ามมี '<' '>' '&' ภายใน cue text ต้อง &lt; &gt; &amp;
⚡ Gotchas
• VTT ต้องขึ้นต้นด้วย 'WEBVTT' + บรรทัดว่าง (header parser strict)
• Timestamp format VTT: HH:MM:SS.mmm (dot ไม่ใช่ comma), SRT: HH:MM:SS,mmm (comma)
• Cue settings: align:start|center|end, line:0-100%, position:0-100%, size:0-100%
• VTT ไม่มี cue ID mandatory แต่ถ้าใส่ ต้อง unique ต่อ file (ใช้เป็น anchor ตอน update)
• Default track: <track ... default> = browser เลือกอัตโนมัติ, ถ้ามีหลาย default = เอาอันแรก
• srclang ต้องเป็น BCP 47 ('en', 'th', 'pt-BR') ไม่ใช่ display name
• Cue ซ้อนทับกัน: VTT render ทั้งคู่ (player ไม่ enforce non-overlap), SRT ก็เหมือนกัน
• Subtitle timing: 1.5-7s per cue (a11y guideline), 42 char/line (display) — editor ต้อง warn
🚫 Anti-patterns
• Hardcode subtitle ใน video timeline text layer — export MP4 embed, search/edit ไม่ได้
• Convert VTT → SRT ด้วย .replace(',', '.') เฉยๆ — SRT ใช้ comma, ไม่ใช่ dot, ผิด format
• ใช้ setTimeout แสดง subtitle แทน <track> — drift 5s ใน 10 นาที, <track> sync กับ media time
• Store subtitle ใน textarea เดียว — structured data (cues array) edit/undo ได้ดีกว่า 10x
• Ignore newline ใน VTT (\n) — display เป็น space ถ้าไม่ใส่ '\n' explicit
🔀 Alternatives
• Subtitle Edit (Windows native)
• Aegisub (advanced ASS format)
• Amara (web collab, paid)
6.9 😂 Meme Generator Media Apps Beginner ⏱ 1 hour

Template picker + top/bottom Impact text + stroke + center-wrap + share. <50KB, no server. Reference: sj88cute-frame-extractor style.

🎯 When to use: Social media share, internal comms, education example (captioning), viral marketing, drag-drop meme studio
✅ Pros
• Impact font + black stroke = 95% readability บนทุก background (classic meme convention)
• Canvas text wrapping (measureText + manual break) = center per-line ไม่ใช่ left-aligned
• Text uppercasing 'hello' → 'HELLO' (meme style) = single CSS text-transform
• Web Share API navigator.share() = mobile native share sheet (FB/IG/Twitter 1 tap)
• toBlob('image/png') = ไฟล์ lossless, social media upload quality เต็ม
• Template library: 20 popular meme template เก็บเป็น base64 = offline ready
⚠️ Cons
• Impact font ไม่มีบน Linux/Android (fallback Arial Black) — embed @font-face ถ้าจริงจัง
• Canvas text baseline = alphabetic (descender 'g' ตัด) — ใช้ 'middle' baseline แล้ว offset y
• Long text wrap by word = 1 line จะเล็กกว่า 5 lines (font size = canvas.height/numLines)
• Copyright: ใช้ template ที่ไม่ใช่ IP (Drake meme ok, Disney character = infringement)
• EXIF strip: meme ที่ upload จาก iPhone มี GPS — canvas re-render strip metadata แต่ watermark risk
• Dark mode preview: meme ใน dark feed จะจางลง (Impact+black stroke ยังอ่านได้)
⚡ Gotchas
• ctx.strokeText ก่อน ctx.fillText (ไม่งั้น stroke ทับ fill = readable แต่ ugly)
• strokeWidth = fontSize × 0.08 (4px stroke สำหรับ 50px font) — vary ตาม view port
• Multi-line wrap: วน split words, measureText accumulate, เมื่อ width > maxWidth → \n
• Font load delay: document.fonts.ready await ก่อน render ไม่งั้น fallback font = wrong layout
• Image source: <input type='file' accept='image/*'> + URL.createObjectURL ห้ามลืม revoke
• Twitter share: navigator.canShare เช็คก่อน (iOS Safari 14+ only), fallback download + manual share
• Top text y position = fontSize × 0.85 (ไม่ใช่ 0) เพราะ visual baseline = ขอบล่างของ capital
• Bottom text y = canvas.height - 10 (10px padding ล่าง) — centered per line by font baseline
🚫 Anti-patterns
• ใช้ CSS text-shadow แทน canvas stroke — export แล้วหาย (CSS = render only, not baked)
• Hardcode font 'Impact' บน Linux = fallback ugly — embed base64 TTF หรือ document.fonts.load check
• Top/bottom text = 1 string เสมอ — render line-by-line wrap, ไม่ใช่ multiline string
• Image source = base64 in URL → memory leak — Blob URL + URL.revokeObjectURL หลัง done
• No watermark/source attribution = โดน claim copyright จาก template owner (Drake Hot Court)
🔀 Alternatives
• imgflip (web, ad-supported)
• Canva (full editor, $)
• Kapwing (video meme, paid)
6.10 🖥️ Screen Recorder Media Apps Intermediate ⏱ 2 hours

getDisplayMedia + MediaRecorder (WebM VP9/Opus) + mic mixing via Web Audio. Reference: sj88-video-editor-pro recorder, sj88cute-frame-extractor capture flow.

🎯 When to use: Tutorial/demo recording, bug report capture, internal training, social clip (sj88-video-editor-pro, sj88cute-frame-extractor ใช้ pattern เดียวกัน), product walkthrough
✅ Pros
• navigator.mediaDevices.getDisplayMedia() = 1 call ได้ screen + system audio (Chrome/Edge)
• MediaRecorder + VP9 codec = 1 hour recording ≈ 1.5GB, เล่น Chrome/Firefox ได้ทันที
• Audio mixing: AudioContext.createMediaStreamDestination() mix mic + system → record 1 track
• Web Speech API caption realtime overlay (browser built-in, no API key)
• Picture-in-Picture recording = tab-only capture (Safari ได้, Chrome ตั้งแต่ 104)
• Pause/Resume = MediaRecorder.pause()/resume() ไม่สร้าง file ใหม่ (single Blob)
⚠️ Cons
• Safari getDisplayMedia = tab only (ไม่ได้ full screen) ก่อน 16.4, ตอนนี้ full screen ได้
• System audio (Chrome): ต้อง check 'Share tab audio' ตอนเลือก source — ไม่มี API enforce
• Firefox ไม่ support system audio เลย (ต้อง mic only)
• iOS Safari: ไม่รองรับ getDisplayMedia (ไม่มี screen capture extension)
• WebM file ใหญ่ 720p30 1hr ≈ 2.5GB VP9, MP4 export ต้อง ffmpeg.wasm (30MB)
• Memory: 1 hour recording = 2-3GB RAM (VideoEncoder + audio buffer pool)
⚡ Gotchas
• getDisplayMedia = Promise ต้อง user gesture (button click) — ไม่งั้น NotAllowedError
• videoBitsPerSecond: 2_500_000 (2.5Mbps) เป็น sweet spot — 5Mbps ไฟล์ใหญ่เกิน, 1Mbps เห็น artifact
• audioBitsPerSecond: 128_000 Opus = podcast quality, 64_000 speech-only = OK
• mimeType 'video/webm;codecs=vp9,opus' ตรวจ MediaRecorder.isTypeSupported ก่อนใช้ (fallback vp8)
• Mic mixing: getUserMedia({audio:true}) → createMediaStreamSource → destination stream
• Two stream: screen video + mixed audio → MediaRecorder รับได้แค่ 1 stream ต่อไป ต้อง addTrack merge
• Pause/Resume ระหว่าง recording: dataavailable event ต้อง save chunk แยก, concat ตอน stop
• Permission re-request: navigator.permissions.query({name:'camera'}) cache ไม่ได้, user ต้อง allow ใหม่ทุก navigation
🚫 Anti-patterns
• Record เป็น data array สะสมใน memory — 1 hour = 2GB array, ใช้ stream + chunks เขียน disk incremental
• getUserMedia({audio:true}) เพื่อ system audio — Chrome getDisplayMedia({audio:true}) เท่านั้นที่ได้ tab audio
• WebM upload ตรงเข้า YouTube — YouTube รับ แต่ transcode 2x, MP4 เข้า transcode ครั้งเดียว
• Bind record start บน page load — NotAllowedError ทันที, ต้อง user click handler
• ไม่ handle 'ended' event ของ track — user click 'Stop sharing' ใน Chrome UI = MediaRecorder stop อัตโนมัติ, blob empty
🔀 Alternatives
• OBS Studio (native, scene+overlay)
• Loom (cloud, paid)
• Vimeo Record (web, paid)
7.1 🧠 OpenAI API (Chat/Completion/Streaming) AI Skills Intermediate ⏱ 3 hours

GPT-4o / GPT-4o-mini via fetch + SSE stream. The default brain for any CodeHub AI app.

🎯 When to use: chatbot/memory agent (memo-ai), text summarizer, generator, classifier, JSON-extractor. ใช้เมื่อ task ต้องการ SOTA reasoning หรือ multi-step planning ที่ rule-based ทำไม่ได้
✅ Pros
• gpt-4o-mini at $0.15/M input is ~15x cheaper than gpt-4o and handles 95% of use cases (summarize, classify, draft, translate)
• Streaming via SSE cuts TTFB from ~8s to ~0.4s — perceived speed is what makes chat UIs feel alive
• Function calling (sub 7.7) turns chat into action — AI can hit YOUR DB, send email, book a slot
• JSON-mode (`response_format:{type:'json_object'}`) eliminates 90% of parsing errors vs regex on prose
• 128k context window on gpt-4o means you can stuff 5+ chapters of docs in one prompt for analysis
• OpenAI SDK is a thin fetch wrapper — no vendor lock-in if you wrap behind your own `ai.complete()` interface
⚠️ Cons
• Cost compounds: a 10-turn chat with 4k context each is ~40k tokens = $0.006 on mini, $0.10 on 4o (16x more)
• Network round-trip 200-800ms per call (excluding generation time) — chatty agents will feel sluggish
• Rate limits hit hard at scale: Tier 1 is 500 RPM / 30k TPM — a runaway loop can lock you out for 60s
• Non-deterministic: same prompt can give different outputs (seed helps but doesn't guarantee identical)
• API can be down 0.1-1% of the time; you NEED a fallback (cached response, static reply, or queued retry)
• Reasoning models (o1/o3) are 10-100x slower and more expensive — only use for hard math/code, not chat
⚡ Gotchas
• API key in browser = instant leak via DevTools or extension — always proxy through YOUR server, never expose `sk-` to client
• Streaming with `stream:true` returns newline-delimited `data: {...}` chunks — split on `\n\n` not `\n` (each SSE event is double-newline terminated)
• `max_tokens` is a HARD cap including the stop reason — set to 100 for classifiers, 800-2000 for chat, 4000+ for long-form
• System prompt counts toward `max_tokens` — for 128k models you have room, but on older 4k/8k models a 2k system prompt leaves nothing for output
• GPT-4o has knowledge cutoff Oct 2023 — for recent events you MUST pass context (RAG sub 7.8) or it'll confidently hallucinate
• Temperature 0 ≠ deterministic — it's 'most likely' sampling; for true determinism use seed + `top_p=0` AND cache results
• Tool-call JSON in response can have wrong types (string vs number) — always validate with Zod/Pydantic before passing to your function
• Embeddings (sub 7.4) and chat completions are SEPARATE endpoints with SEPARATE rate limit buckets — bumping one doesn't help the other
🚫 Anti-patterns
• Calling OpenAI from the browser with the API key in `localStorage` or hardcoded — leaks in 24h to scrapers
• Building a 'do-everything' mega-prompt that asks for JSON + translation + summary + sentiment — break into 2-3 calls, each is cheaper and more accurate
• Using gpt-4o when gpt-4o-mini suffices (3-line prompt, simple classification) — wastes 15x cost for no quality gain
• No retry/backoff on 429/500/503 — a 60s `setTimeout` retry on 429 with exponential backoff is mandatory for production
• Storing full chat history in every new request without summarization — at 50 turns you'll blow past context and pay 10x more than needed
🔀 Alternatives
• Anthropic Claude (longer context, better code)
• Google Gemini (free tier, vision)
• Local Ollama (no API cost, slower)
7.2 🗣️ Web Speech API (TTS/STT) AI Skills Beginner ⏱ 2 hours

Browser-native TTS (`speechSynthesis`) + STT (`SpeechRecognition`). Zero API cost, gesture-required, varies by browser.

🎯 When to use: voice UI for a11y, prototype Thai/English voice features, offline narration (knowledge-garden reading mode), no-budget MVPs. ใช้เมื่อต้องการ TTS/STT ฟรีและเร็วโดยไม่ต้องเรียก OpenAI
✅ Pros
• Zero API cost — `speechSynthesis.speak()` is a one-liner with no auth, no quota, no latency to a remote server
• Offline-capable on Chrome with downloaded voices — works on airplanes, behind firewalls, with no internet
• Privacy-preserving: STT audio never leaves the device — ideal for medical/legal note-taking (svj-thai-voice-factory pattern)
• A11y win: screen-reader users expect TTS, and self-voicing controls (volume/rate) need almost no code
• Cross-platform: works on every modern browser + iOS Safari (with `webkitSpeechRecognition` prefix)
• No server load: 1000 concurrent users TTS'ing = 0 backend cost, the browser does the work
⚠️ Cons
• Voice quality is robotic vs cloud TTS (ElevenLabs, OpenAI TTS) — user testing shows 60% prefer cloud for long-form listening
• STT accuracy on Thai is ~70-80% WER (Word Error Rate) vs cloud at 95%+ — many 'voice input' UIs feel broken in production
• Voice selection is non-deterministic: same code on Chrome Windows picks Microsoft Zira, on Mac picks Samantha — UX will drift
• STT requires user gesture in Chromium (click-to-start) — can't auto-listen on page load (privacy feature, but limits hands-free flows)
• No server-side access: Web Speech is browser-only, so a Node.js background job that needs STT must use a different API
• Firefox lacks STT entirely (only TTS) — graceful degradation is REQUIRED, not optional
⚡ Gotchas
• Safari uses `webkitSpeechRecognition` and `webkitSpeechRecognitionError` — always check `window.SpeechRecognition || window.webkitSpeechRecognition` and wrap in try/catch
• First call to `speechSynthesis.speak()` on Chrome is silent for ~100ms while voices load — pre-load by calling `getVoices()` on `voiceschanged` event
• Thai STT in Chrome requires `lang='th-TH'` AND a Thai voice pack installed in OS — silent failure if missing, no error thrown
• Long text (>150 chars) gets cut off on iOS Safari — chunk into 2-3 utterances with `boundary` event between them
• `recognition.continuous = true` on Chrome will auto-stop after ~60s of silence — re-start in `onend` for a 'always listening' UX
• iOS Safari pauses TTS when phone locks or app backgrounds — add `onerror` handler and resume on `visibilitychange`
• Microphone permission revoked = `recognition.onerror({error:'not-allowed'})` — show a clear 'enable mic' CTA, not a generic 'failed'
• STT `interimResults = true` gives you live partials (great for 'user is saying X' UI) but final result may differ — always show '...', then commit
🚫 Anti-patterns
• Assuming the same voice across browsers/sessions — test on Chrome Win, Chrome Mac, Safari iOS, Firefox separately, every time
• Using Web Speech for production Thai dictation (medical, legal) — switch to OpenAI Whisper or Google Cloud Speech for 95%+ accuracy
• TTS'ing full articles without pause/chunking — long text stalls iOS; split at sentence boundaries with `split(/[.!?]\s+/)`
• No fallback when STT unsupported (Firefox, old Edge) — check `if (!('webkitSpeechRecognition' in window)) showFallbackInput()`
• Calling `recognition.start()` in `useEffect` without user gesture — Chrome blocks silently; always trigger from click
🔀 Alternatives
• OpenAI Whisper (server STT, 95%+ accuracy)
• ElevenLabs (premium TTS, voice cloning)
• Google Cloud Speech (production STT)
7.3 👁️ Vision API (gpt-4o Vision / Gemini) AI Skills Intermediate ⏱ 3 hours

Image understanding via multimodal LLMs. gpt-4o sees 1024+ px images, returns structured descriptions, OCR, or classification.

🎯 When to use: bg-remover prompts, cutout-studio caption, document scanner (receipt → JSON), product photo tagging, accessibility (alt-text at scale). ใช้เมื่อต้อง OCR/general-vision ที่ rule-based (template match) ทำไม่ได้
✅ Pros
• Zero training: gpt-4o-vision handles OCR, object detection, scene description, color/style analysis out of the box
• One endpoint, many tasks: same `image_url` input works for 'describe', 'extract JSON', 'classify into X/Y/Z', 'count objects'
• Handles 1024x1024 to 4096x4096 images natively (auto-tiles larger) — no preprocessing pipeline needed for typical photos
• Cheap for batch: $0.00255/image (gpt-4o-mini, low-res) → $0.0115/image (gpt-4o, high-res) — process 10k receipts for ~$115
• Multilingual OCR: Thai, Japanese, Arabic, handwritten — single call, no per-language model swap
• Returns rich JSON when you prompt right: `[{label:'product',confidence:0.9,box:[x,y,w,h]}]` instead of just text
⚠️ Cons
• High-res images explode tokens: 4096x4096 ≈ 13k tokens = $0.033/image on gpt-4o (vs $0.001 on mini) — always resize first
• Latency: 2-8s per image (vs 200ms for classification) — bad UX for live camera feeds without aggressive caching
• Hallucinates text/numbers on low-quality images — won't say 'I can't read this', will confidently give wrong digits
• No bounding box output (unlike YOLO/Detectron) — to localize objects you need a second call or post-processing
• Privacy/data residency: image leaves your server → OpenAI's GPU — won't pass HIPAA/medical or EU GDPR-Sovereign requirements
• Rate limits are TOUGH on vision: 30k tokens/min means ~2 large images/sec, not the 500 RPM chat limit
⚡ Gotchas
• Always resize to <1024px longest side before sending — cuts cost 10x with 0% quality loss for most tasks; use `canvas.toBlob({quality:0.8})`
• Send image as `data:image/jpeg;base64,...` URL when <100KB, or upload to your CDN and send URL — base64 has 33% size overhead
• `detail:'low'` is 85 tokens fixed (great for 'is there a dog?' yes/no), `detail:'high'` auto-tiles — pick explicitly, don't default to auto
• GPT-4o Vision is a CHAT endpoint — same rate limit bucket as text — vision traffic will starve your text traffic if you don't separate keys
• Thai handwritten text recognition is ~80% accurate vs 99% for printed — for forms consider a dedicated Thai OCR model (Baidu/Google) first
• Always include image in SECOND message, not first — first message is system context; put text instruction in system, image in user
• Gemini 1.5 Pro processes 1M context — can analyze 100s of images in one prompt for video understanding, but $$ adds up
• Pre-process: remove EXIF (privacy), strip alpha channel (PNG→JPEG), grayscale if color doesn't matter — 50% size reduction
🚫 Anti-patterns
• Sending full 4K camera frames at 30fps to vision API — bankrupts you in minutes; sample 1 fps, resize to 512px, use `detail:'low'`
• Trusting vision output without spot-check — 1-5% hallucination rate on numbers/IDs means manual verify step is mandatory for financial/medical
• Using gpt-4o-vision for tasks a $0 free model handles (Tesseract.js for printed English text, MediaPipe for face detection)
• Storing full base64 image in chat history context — blows past 128k after 30 images; reference URL or summary instead
• Forgetting to set `max_tokens` — vision responses can ramble; cap at 500 tokens for JSON extraction, 2000 for descriptions
🔀 Alternatives
• Google Document AI (specialized OCR)
• AWS Textract (forms/tables)
• YOLOv8 (self-hosted detection)
• CLIP (zero-shot classification)
7.4 🧮 Embeddings + Vector Search AI Skills Intermediate ⏱ 5 hours

Text → 1536-dim float vectors (text-embedding-3-small). Cosine similarity finds semantically similar docs, not just keyword matches.

🎯 When to use: semantic search ('how to fix wifi' finds 'router reset steps'), recommendation, dedup, RAG retrieval (sub 7.8), clustering. ใช้เมื่อ keyword search ไม่เจอเพราะ user พูดคนละคำกับ doc
✅ Pros
• Semantic: 'car' matches 'vehicle', 'auto', 'รถยนต์' — no synonym lists to maintain, the model learned it
• text-embedding-3-small at $0.02/M tokens = $0.02 per 1k chunks of 500 tokens — cheaper than one RAG query's LLM call
• Multilingual out of box: 100+ languages including Thai (CLIP-style training) — embed Thai + English in same vector space
• 1536-dim dense vector captures nuance a TF-IDF sparse vector misses (sentiment, intent, abstract concepts)
• Pre-compute once at index time, search at query time in O(log n) with HNSW — 1M vectors search in <10ms
• Combine with metadata filters: `WHERE category='receipts' AND embedding <-> query_emb < 0.3` for hybrid search
⚠️ Cons
• Storage: 1M docs × 1536 floats × 4 bytes = 6 GB just for vectors — pgvector is fine to 10M, then Pinecone/Qdrant
• No exact matching: 'error code E1234' won't rank a doc containing 'E1234' higher than a doc with synonyms (need hybrid)
• Re-embedding is expensive: changing models means re-embed ALL docs (hours + $) — pin your model version forever
• Cosine ≠ relevance: two unrelated docs can have high cosine if they share style/tone; add cross-encoder re-rank (sub 7.8)
• Cold-start latency: first embed call takes 200-500ms (model warm-up), subsequent calls are 50-100ms
• No 'why this matched' explanation: a 0.92 cosine score is a black box — users need to trust it or you need to add citations
⚡ Gotchas
• ALWAYS normalize vectors before cosine — OpenAI embeddings are already normalized but if you mix models, normalize to unit length
• Batch embedding: `embeddings.create({input:[t1,t2,...t100]})` is 5-10x cheaper per token than 100 individual calls (also avoids rate limit)
• Chunk text to ~500 tokens (not 1 sentence, not 5 pages) — too small loses context, too large dilutes signal; overlap 50 tokens
• Store the original text alongside the vector — you NEED it for LLM context (RAG), and for debugging 'why did this match?'
• text-embedding-3-large (3072 dim) is 6.5x more expensive but only 2-5% better on most tasks — stick with `3-small` unless benchmarks prove otherwise
• Matryoshka embeddings: `3-small` supports custom dimensions (512, 256) — smaller dims = 4x faster search with minimal quality loss
• For Thai: tokenize with `new Intl.Segmenter('th', {granularity:'word'})` before chunking — word boundary matters for embedding quality
• Index choice: pgvector IVFFlat for <100k, HNSW for 100k-10M, Pinecone/Qdrant for 10M+ or when ops overhead is too much
🚫 Anti-patterns
• Embedding the entire document as one vector — chunks of 200-800 tokens are 30-50% more accurate for retrieval (Naver/Stanford benchmarks)
• Using cosine on un-normalized vectors — gives wrong rankings for OpenAI; they happen to be normalized, don't assume for other models
• No re-embedding strategy when model deprecates — OpenAI deprecates embedding models in 6 months; always store `model_version` in your metadata
• Storing vectors only (no original text) — you can't debug, can't show citations, can't re-embed without re-fetching source
• Cosine similarity alone for production search — combine BM25 (keyword) + vector (semantic) via Reciprocal Rank Fusion for 20-40% better recall
🔀 Alternatives
• TF-IDF (sub 7.5, free, no semantics)
• BM25 (classic IR, keyword)
• Sentence-Transformers (self-hosted)
• Cohere Embed v3 (multilingual)
7.5 🔍 TF-IDF Search (Bilingual TH+EN, no API) AI Skills Intermediate ⏱ 5 hours

Tokenize with regex (Latin word OR Thai unicode range), compute TF-IDF vectors, cosine-rank. Zero cost, instant, fully offline.

🎯 When to use: static site search (codehub-search powers /api/search), bilingual corpus < 50k docs, no API budget, want explainable matches. ใช้เมื่อต้องการ search ที่ทำงาน offline, deterministic, และ zero cost
✅ Pros
• Zero cost: pure JS regex + math, runs in browser or Node, no API key, no rate limit, no internet after index build
• Deterministic: same input always gives same result — cacheable, debuggable, explainable (show matched terms)
• Bilingual out of box: regex `[a-zA-Z0-9]+|[\฀-\๿]+` tokenizes English words AND Thai runs as separate tokens
• Real-time: search 10k docs in <50ms in pure JS — fits in a single Cloudflare Worker free tier call
• Explainable: you can show 'matched: [รถยนต์, ราคา]' to the user — vector search gives a black-box score
• Works on cold data instantly: no pre-warming, no model load, no GPU — just a JSON index of IDF weights
⚠️ Cons
• No semantic understanding: query 'cheap' won't match doc with 'affordable' — must maintain synonym lists manually
• Thai tokenization is naive: 'เล่นกับเพื่อน' becomes ['เล่น', 'กับ', 'เพื่อน'] instead of proper segmentation — misses compound words
• Stop words pollute: 'the', 'a', 'ของ', 'ใน', 'และ' appear in 80% of docs, kill IDF signal — must filter explicitly
• Scaling: O(n) per query unless you build an inverted index — 100k docs takes 500ms+ which feels slow in UI
• Boost tuning is fragile: the magic numbers `+0.3 exact, +0.05 thai` are corpus-specific — re-tune when content changes
• No context: matching 'apple' in 'apple pie recipe' same as 'Apple Inc stock' — needs LLM re-rank or document structure
⚡ Gotchas
• Thai unicode range `\฀-\๿` covers only Thai — if your docs have Lao/Khmer/Japanese, add those ranges or tokenize per-lang
• Lowercase BEFORE counting: 'Apple' and 'apple' are different tokens without `.toLowerCase()` — IDF inflates and rankings go wrong
• Filter stop words: maintain a list of ~200 EN + 100 TH common words ('the', 'a', 'ของ', 'ใน', 'คือ') — subtract from tokens before TF
• IDF formula: `Math.log((N+1)/(df+1)) + 1` (smoothed, never zero) — classic `log(N/df)` gives -Infinity for terms in every doc
• Build index at INDEX TIME not query time: pre-compute TF-IDF vectors for all docs, store JSON; query is just `cosine(query_vec, doc_vec)`
• Cosine vs dot-product: if vectors are L2-normalized (mag=1), they're equivalent — normalize once at index time for 3x faster search
• Hybrid boost: in codehub-search we add `+0.3` to score if exact phrase appears in doc, `+0.05` for any Thai token match — empirically 40% better NDCG
• Update strategy: append-only index works fine to 100k docs; for more, re-build index in background worker every N docs added
🚫 Anti-patterns
• Using plain `text.includes(query)` for search — case-sensitive, word-boundary-blind, ranks 'cat food' lower than 'category food'
• Computing IDF on the fly per query — build the IDF dict ONCE at index time, ship it as JSON (KBs for 10k docs), query just does TF×IDF
• Ignoring token frequency in long docs — a 10k-word doc naturally matches more terms; divide by `sqrt(word_count)` (length normalization)
• Same boost numbers copied from Stack Overflow — TF-IDF tuning is corpus-specific; re-tune with a held-out query set of 50+ real queries
• No fallback when 0 results — always show 'no results for X, did you mean Y?' with edit-distance or prefix match on tokens
🔀 Alternatives
• Embeddings (sub 7.4, semantic)
• BM25 (Okapi, classic IR)
• Lunr.js (browser search lib)
• FlexSearch (faster, larger)
7.6 📡 Streaming Responses (SSE / ReadableStream) AI Skills Intermediate ⏱ 4 hours

Server-Sent Events for token-by-token AI output. TTFB drops from 8s to 0.4s. Cancellable via AbortController.

🎯 When to use: chat UIs (memo-ai), AI assistants (mavis-assistant), long-form generation where perceived speed matters, autocomplete-as-you-type. ใช้เมื่อ user รอ output > 2s และ type-while-generate จะให้ UX ดีกว่า
✅ Pros
• Perceived speed: 0.4s TTFB vs 8s — users see the first word 20x faster, even though total time is the same
• Cancellable: AbortController + `signal: ctrl.signal` lets user stop generation mid-stream, saves 90% of remaining tokens/cost
• Memory efficient: server doesn't buffer full response, client doesn't need full response in memory — streams 100k tokens fine
• Backpressure built-in: `await reader.read()` blocks if client is slow, server naturally throttles — no overload
• Auto-reconnect: EventSource has built-in retry on disconnect; `fetch` + ReadableStream you handle it manually (more control)
• Works through proxies/CGNAT: SSE is HTTP/1.1 long-poll, not WebSocket — survives corporate firewalls that block WS
⚠️ Cons
• Server complexity: you need streaming-capable infra (Node `Response(stream)`, FastAPI `StreamingResponse`, no nginx buffering)
• Can't set status code after first byte: if auth fails mid-stream, you've already sent 200 OK — must validate before streaming starts
• Buffering kills it: nginx `proxy_buffering on` (default) waits for full response before forwarding — set `off` for `/api/stream/*`
• Connection limits: browsers max 6 concurrent HTTP/1.1 connections per origin — one stream holds one, exhausting budget for other fetches
• No built-in ack: client doesn't know if server crashed mid-stream — must send explicit `event: end` and handle reconnection
• Time-to-first-byte varies: 0.2-1s is normal, but cold model (first request) can be 5s — set `Transfer-Encoding: chunked` early
⚡ Gotchas
• EventSource is GET-only — for POST bodies, use `fetch + body.getReader()` instead (memo-ai does this for chat POST)
• SSE format: each event is `data: <json>\n\n` (DOUBLE newline) — `data:` without trailing blank line is silently dropped by EventSource
• Flush explicitly: `controller.enqueue(...)` is buffered; some servers need `flushHeaders()` or `resp.flush()` to push to network immediately
• AbortController: pass `signal` to fetch, then `ctrl.abort()` on user click — but ALSO clean up server-side (close the upstream API stream)
• Don't parse JSON per line for OpenAI: chunks are `data: {json}\n\n`, NOT line-delimited JSON — split on `\n\n` first, then strip `data: `
• OpenAI stream chunks: `delta.content` is empty in some chunks (tool calls, role tokens) — use `chunk.choices[0]?.delta?.content || ''`
• Server-Sent Events need `Content-Type: text/event-stream` AND `Cache-Control: no-cache, no-transform` — missing these and proxies cache
• CORS: SSE responses need `Access-Control-Allow-Origin` like any other response — but preflight is for POST, not GET-with-EventSource
🚫 Anti-patterns
• Using WebSocket for one-way AI streaming — SSE is 5x simpler, no server state, works through proxies, browser reconnection is free
• Buffering full response server-side before streaming — kills the whole point; stream from OpenAI directly through to client
• No client-side abort on unmount — React component unmounts but fetch keeps running, leaks memory and burns API quota
• Wrapping stream in JSON.stringify per chunk — `JSON.parse` per chunk wastes CPU; send raw text or use `application/x-ndjson`
• No heartbeat: long streams (5+ minutes) get killed by intermediate proxies' 60-120s idle timeout — send `: heartbeat\n\n` every 15s
🔀 Alternatives
• WebSocket (bi-directional, more complex)
• Long polling (legacy fallback)
• Server-Sent Events vs WebSockets (sub 7.6 SSE simpler)
7.7 🛠️ Function Calling (OpenAI / Anthropic tools API) AI Skills Advanced ⏱ 1 day

AI returns a JSON tool call → your code executes → result fed back. Turns chat into action (book, query, send, delete).

🎯 When to use: agent AI that needs to DO things (mavis-assistant booking, email send, DB query, file edit), structured data extraction with validation, multi-step workflows. ใช้เมื่อ AI ต้อง interact กับ external system ไม่ใช่แค่ตอบ
✅ Pros
• AI acts on YOUR data: weather, calendar, internal DB, Stripe — anything you can wrap in a function becomes an AI tool
• Structured I/O: tool args are validated JSON-schema, not free text — no regex parsing, no 'did the AI say 5 or five?'
• Multi-tool in one call: define 10 tools, model picks which to call (or none) — single round-trip for complex intents
• Parallel tool calls: gpt-4o can call 3 tools at once (e.g. weather+calendar+email) — model sees results, decides next step
• Anthropic and OpenAI have nearly identical tool APIs — same `tools: [...]` schema works on both with minor tweaks
• Reduces hallucination: AI can't 'guess' the weather, it MUST call `get_weather('Bangkok')` — grounded in real data
⚠️ Cons
• Schema design is hard: bad descriptions cause wrong tool selection — 'use this to fetch weather' vs vague 'weather tool' is 30% accuracy diff
• Multi-turn orchestration: model may call wrong tool, your code must validate, send back error, let model retry (adds 1-3 round-trips)
• Cost: each tool call is a full chat completion round-trip — a 5-tool workflow = 5x cost vs single prompt; cache tool results when possible
• Latency: tool execution adds 200-2000ms before the LLM 'thinks' about result — for time-critical flows, this dominates
• Security: AI calling `delete_user(id)` is dangerous — ALWAYS whitelist tools, require confirmation for destructive actions
• Model can refuse to call tools or call them when not needed — system prompt with 'use X only when user explicitly asks' helps
⚡ Gotchas
• ALWAYS validate tool args server-side with Zod/Pydantic — model can send `age: 'twenty'` when schema says `number`; trust nothing
• Tool descriptions are CRITICAL: 'Get current weather for a city. Returns {temp_c, conditions, humidity}. Use when user asks about weather.' — be specific
• Max tools in one call: ~15-20 before model gets confused; group similar tools or use sub-routing (one tool that dispatches to 10 internal ones)
• Anthropic requires `input_schema` (renamed from `parameters`), no `type: 'function'` wrapper, and `tool_use` blocks (not `function_call`) — small diffs
• For multi-step: after tool executes, send back `{role:'tool', tool_call_id:'abc', content:result}` — model needs the id to associate result with call
• Long tool outputs: if `get_logs()` returns 100k tokens, truncate to top 500 lines OR summarize with another LLM call before sending back
• Idempotency: model may call same tool twice on retry — pass an `idempotency_key` derived from the user message + tool name to dedupe server-side
• Streaming + tools: OpenAI stream returns tool calls in `delta.tool_calls` (not content) — buffer these, they arrive in pieces
🚫 Anti-patterns
• Exposing 'dangerous' tools (delete_*, send_email_*, charge_card_*) without confirmation — at minimum require explicit user approval for each call
• Using tools for things the LLM already knows (translate, summarize) — adds latency, no benefit; use direct prompt
• Tools with huge schemas (50+ fields) — model will miss required fields; break into 2-3 tools with simpler scopes
• No timeout on tool execution — a slow DB query blocks the chat for 30s; enforce 5s timeout, return error, let model decide next step
• Logging full tool args to console in production — can leak PII (emails, addresses) to log aggregators; redact sensitive fields
🔀 Alternatives
• LangChain (framework, more deps)
• AutoGen (multi-agent)
• Raw prompt engineering (sub 7.9)
• Anthropic MCP (newer standard)
7.8 📚 RAG (Retrieval Augmented Generation) AI Skills Advanced ⏱ 3 days

Embed query → search top-k similar chunks → stuff in LLM prompt. Grounds AI in YOUR data, eliminates hallucination, cites sources.

🎯 When to use: chat on your docs (knowledge-garden Q&A, quick-note search), customer support bot on knowledge base, internal company wiki assistant, code search. ใช้เมื่อ AI ต้องตอบจากข้อมูลเฉพาะที่ LLM ไม่รู้
✅ Pros
• Eliminates hallucination: AI quotes YOUR docs verbatim, not inventing facts from training data
• Always cited: show user 'source: doc-id, page 3' — builds trust, lets them verify, required for medical/legal
• Cheap to update: add a doc → re-embed → done, no fine-tuning ($1000s) or retraining (weeks)
• Works on private data: docs never leave your server (until final LLM call) — fine-tune was the only alternative before
• Multi-source: combine PDFs, Slack, Notion, DB rows in one vector index — model doesn't care where chunks came from
• Quick-Note pattern: chunk on save, embed, index — next user query hits all 3 sources in <200ms total
⚠️ Cons
• Quality = GIGO: bad chunks (too big, too small, wrong boundaries) = bad retrieval = bad answers — chunking is 60% of the work
• Context window pressure: 5 chunks × 1000 tokens = 5000 tokens just for context, leaving less for answer + conversation history
• Embedding cost adds up: 1M chunks × 500 tokens × $0.02/M = $10 one-time + storage forever — re-embedding is expensive if you change models
• Cross-encoder re-rank is +30% quality but +200ms latency + separate model — usually skipped in MVP, regretted in production
• Thai/Asian language RAG is harder: chunk boundaries (sentences) don't exist in Thai the same way, embeddings are 10-20% worse than EN
• Stale data: if you forget to re-embed updated docs, users get old answers — need an ingestion pipeline with version tracking
⚡ Gotchas
• Top-k sweet spot is 3-5, NOT 20: more chunks dilute the prompt with noise, model gets confused; 3-5 with re-rank beats 20 without
• Chunking strategy matters: Markdown headers (semantic) > fixed 500 chars (dumb) > single sentence (too small); aim for 200-800 token chunks
• Overlap: 50-100 token overlap between adjacent chunks preserves context at boundaries; without it, 'the cat sat on the ___' gets cut mid-sentence
• Hybrid search wins: vector (semantic) + BM25 (keyword) fused via Reciprocal Rank Fusion is 20-40% better than vector-only on tech docs
• Cross-encoder re-rank: after vector search, run a smaller model (`cross-encoder/ms-marco-MiniLM`) to score query+chunk pairs, re-sort — +30% NDCG@10
• Metadata filters BEFORE vector search: `WHERE category='api' AND created_at > '2024-01-01'` reduces search space 10-100x, faster + better
• Prompt template: 'Use ONLY the context below. If answer not in context, say "I don\'t know". Cite source IDs.' — critical to prevent hallucination
• Chunk metadata: store `{doc_id, section, page, lang, timestamp}` with each vector — needed for citations, filtering, debugging
🚫 Anti-patterns
• Embedding the WHOLE document as one vector — precision dies, you get the right doc but wrong section; always chunk
• No chunk overlap — context gets cut at boundaries, answers miss critical info at start/end of sections
• Re-ranking 50 chunks with cross-encoder — too slow, too expensive; re-rank top-20 from vector, return top-5
• Stuffing 10+ chunks into prompt — model gets confused, attention dilutes, answers degrade; 3-5 is the sweet spot
• No evaluation set: shipping RAG without measuring recall@10 on 50+ held-out queries = flying blind; build eval set before launch
🔀 Alternatives
• Fine-tuning ($$$ for static knowledge)
• Long context (1M tokens works, but $)
• GraphRAG (Microsoft, for relational queries)
7.9 📝 Prompt Engineering (System, Few-shot, CoT) AI Skills Beginner ⏱ 1 day

The art of writing prompts that consistently get high-quality outputs. 30-50% quality lift on same model, free, no code.

🎯 When to use: every AI integration in memo-ai, before reaching for a bigger model, when outputs are inconsistent or wrong. ใช้เนื้อ 90% ของเวลาในการ tune AI feature ก่อนจะไปถึง RAG/fine-tune
✅ Pros
• Free 30-50% quality lift on same model — better prompts on gpt-4o-mini often beat worse prompts on gpt-4o, 15x cheaper
• Iteration speed: change a prompt, hit save, see new result in 5 seconds — no deploy, no retrain, no waiting
• Portable: same prompt works on gpt-4o, Claude, Gemini, Llama (with minor tweaks) — no vendor lock-in
• Few-shot > zero-shot: 3 examples typically beats 100 words of instruction; model pattern-matches examples
• Chain-of-thought: 'think step by step' on reasoning tasks (math, logic) gives 20-40% accuracy boost at no cost
• System prompt = behavior DNA: tone, format, refusal rules, persona — set once, applies to all 1000s of calls
⚠️ Cons
• Brittle: a prompt that works 99% may fail 1% in a way you can't reproduce — need eval set, not vibes-based testing
• Token cost: long system prompt is paid EVERY call — 2k system prompt × 10k calls = 20M tokens ($3 on mini, $50 on 4o)
• Model drift: gpt-4o behavior changes with model versions, fine-tuning data updates — your prompt that worked in March may break in June
• Multi-language: Thai prompts often 20-30% less accurate than English on same model — translate or use EN for critical instructions
• Diminishing returns: 1st hour of prompt tuning = 30% gain, 5th hour = 2% gain — know when to stop and switch to RAG or fine-tune
• Hard to version: prompts end up in 5 places (code, env, DB, admin UI, analytics) — diff/rollback is manual
⚡ Gotchas
• Be SPECIFIC: 'Reply in 2-3 sentences, in Thai, with bullet points, no preamble' beats 'be concise' every time
• Few-shot examples > instructions: 3 well-chosen examples typically outperform paragraphs of rules — show, don't tell
• Chain-of-thought for reasoning: append 'Think step by step, then give your final answer.' to math/logic prompts — 20-40% accuracy gain
• Negative instructions confuse models: 'Do not use markdown' is weaker than 'Reply in plain text, no markdown' — say what TO do
• System prompt vs user prompt: persona/format/limits go in system, the actual task in user — models weight system higher
• JSON mode: `response_format: {type: 'json_object'}` + a 'Return JSON with fields {x:number, y:string}' — eliminates 90% of parse errors
• Role priming: 'You are an expert Thai translator with 20 years experience' measurably improves output vs neutral persona
• Temperature by task: 0.0-0.3 for code/JSON/factual, 0.7-1.0 for creative writing, 0.5 default — most apps leave at 1.0 by mistake
🚫 Anti-patterns
• Mega-prompt with 500 words of instructions — model attention dilutes, gets worse than 3 examples + 1 sentence
• Mixing Thai and English instructions in same prompt — model often mixes languages in output; pick one for instructions, allow response in target
• No eval set: changing prompt based on 'feels better' without measuring 50+ held-out examples = random walk
• Hard-coding model name in 50 places — wrap in `const MODEL = 'gpt-4o-mini'`; upgrade is 1-line change
• Forgetting to test on EDGE cases: prompt works for happy path, fails on empty input, on 10k char input, on emoji, on Thai — test boundaries
🔀 Alternatives
• DSPy (programmatic prompt optimization)
• Guidance (Microsoft, templated)
• LangChain prompts (overkill for most)
7.10 🛡️ Safety Filters (Moderation + Jailbreak Detection) AI Skills Intermediate ⏱ 3 hours

OpenAI Moderation API (free <1M tokens/mo) + pattern-based jailbreak detection. Required for any public AI app.

🎯 When to use: any public-facing AI app (memo-ai, mavis-assistant), user-generated content flows, child/family products, anything with comments/chats. ใช้เมื่อ user input เข้าถึง AI โดยตรง
✅ Pros
• OpenAI Moderation API is FREE for inputs <1M tokens/mo — zero cost for most small/medium apps, no excuse to skip
• Sub-100ms latency on moderation call — can run inline before main LLM call, user won't notice
• Multi-category: hate, violence, self-harm, sexual, harassment — 13 categories with confidence scores, not just binary
• Jailbreak pattern detection: regex on 'ignore previous instructions', 'DAN mode', 'pretend you have no rules' catches 80% of attempts
• Compliance: EU AI Act (Aug 2025), COPPA, app store review — moderation is a hard requirement, not a nice-to-have
• Two layers: moderate INPUT (block user abuse) and OUTPUT (catch model regression on safety) — defense in depth
⚠️ Cons
• False positives: 'I want to kill this bug' flagged as violence — needs human review queue for borderline cases
• Doesn't catch everything: novel slurs, coded language, multi-language attacks slip through — combine with human report flow
• OpenAI Moderation is English-optimized: Thai profanity/slurs detection is 60-70% recall vs 95%+ for English
• Cost adds up: 2 calls (moderate input + moderate output) per chat = 2x tokens + 100-200ms latency on every message
• Adversarial attacks evolve: new jailbreak techniques (Base64, ROT13, multi-turn grooming) appear weekly — pattern lists age out
• Privacy: moderation API sees the text — for medical/financial PII you may need a self-hosted classifier (Llama Guard)
⚡ Gotchas
• Moderation is a SEPARATE endpoint: `openai.moderations.create({input:text})` — distinct from chat, distinct rate limit bucket
• Check BOTH input and output: input for user abuse, output for model regression (a fine-tune gone wrong can start producing bad content)
• Thresholds: 0.5+ flagged = block, 0.3-0.5 = human review, <0.3 = allow — tune per audience (kids' app needs 0.2, dev tool can be 0.7)
• Free tier: OpenAI Moderation free to 1M tokens/mo, then $0.005/1k — even at scale, costs <1% of main LLM bill
• Jailbreak patterns to block: 'ignore previous', 'pretend you', 'DAN', 'developer mode', 'no restrictions', role-play as unrestricted AI
• Self-host for sensitive: Llama Guard (Meta, 7B) runs on a single GPU for $0.001/call — better for medical, financial, EU data residency
• Log everything: store (with PII redacted) what was flagged and action taken — needed for audits, abuse pattern analysis, model fine-tuning data
• Don't ONLY rely on moderation — user reports + rate limits + post-hoc review are also needed; no single layer is enough
🚫 Anti-patterns
• Skipping moderation on 'internal' apps — they go public via URL share, screenshot, or promotion; plan for public from day 1
• Only moderating input, not output — fine-tunes, prompt injections, and model regressions can make output unsafe even if input is clean
• Hard-blocking on any flag (0.01 confidence) — will block legitimate users on edge cases; use threshold (0.5) + human review queue
• Logging full moderated text to analytics — may store PII, medical, or illegal content in plain text logs; redact or hash sensitive fields
• No user feedback loop: user flagged unfairly? Build 'this was a mistake' button → re-evaluate thresholds, update pattern list
🔀 Alternatives
• Llama Guard (self-hosted)
• Perspective API (Google, free tier)
• Azure Content Safety (enterprise)
• Cloudflare AI Gateway (built-in)
8.1 🚀 ship.py (1-Click Deploy Pipeline) Infrastructure Intermediate ⏱ 2 hours

One-command deploy. edit → curl-verify → paramiko SFTP → API upload → cache refresh = sandbox-to-prod in 30s for any single-file app.

🎯 When to use: Every static app on codehub.sj88ai.com (karaoke-studio, stronghold-3d, noodle-shop, demo-coin-flip); 1 app < 5MB; < 10 deploys/day; solo dev pushing from local sandbox
✅ Pros
• 1 command = edit → curl-verify (local MD5) → paramiko SFTP → /api/upload (server MD5) → /api/cache/refresh; ~30s end-to-end from sandbox to https://codehub.sj88ai.com/APP/
• MD5 hash check before/after SFTP catches corrupted transfer (SFTP doesn't natively verify integrity) — corrupted file = silent 500 on prod
• Auto-creates /api/cache/refresh in same deploy = no separate curl + no stale HTML/JS served to users after deploy
• Idempotent: re-running ship.py overwrites old version, no stale files; --keep-versions=N gives rollback history on disk
• Bash-friendly exit codes (0=ok, 1=verify fail, 2=SFTP fail, 3=API fail) — slots into pre-commit hooks and cron jobs
• Local file = source of truth; no need to git push before ship (for hotfixes); ship.py reads file directly off disk
⚠️ Cons
• Single-VPS = no horizontal scale; if VPS dies, no deploy possible; consider GitHub Actions (8.5) for multi-region
• SFTP is unencrypted at the application layer beyond SSH tunnel — enable sftp -C for compression, but real solution is rsync/scp for sensitive data
• No atomic rollback — ship.py overwrites, doesn't version on disk; manual restore needs /mnt/16tb/codehub/backup/<date>/
• Paramiko is ~2x slower than ssh CLI for files > 5MB; SFTP upload of 10MB+ = noticeable delay
• Credential lives in $DEPLOY_VPS_PASS env = one secret leak in shell history = full VPS root access; rotate quarterly
• Race condition if 2 ships hit same slug at same time (last-writer-wins, no lock); use flock in cron wrapper for sequential deploys
⚡ Gotchas
• Always set $DEPLOY_VPS_PASS in ~/.bashrc or use a secrets manager (1Password CLI, sops) — NEVER inline `password='hunter2'` in script
• Verify file MD5 BEFORE paramiko (sanity check source) AND after SFTP put() (transfer integrity); log both hashes for audit
• If ship.py fails at API upload step, the file IS on VPS but NOT in catalog — manual cleanup needed (curl /api/admin/delete?slug=X)
• Cache refresh hits nginx (via /api/cache/refresh), NOT the file itself — file changes alone don't propagate to users without cache bust
• Title field must match folder slug exactly (`--title 'My App'` for slug 'my-app') or /APP/ will 404; spaces in slug = URL-encoding hell
• Use --dry-run flag first time to see exactly which steps will run; avoids accidentally overwriting prod on typo
• ship.py requires $API_ADMIN_TOKEN (generate via POST /api/admin/login) — token expiry 24h, re-auth at start of script if 401
• If VPS disk > 90% full, SFTP put() succeeds but nginx serves stale (check `df -h | grep '9[0-9]%'` pre-deploy to fail fast)
🚫 Anti-patterns
• DON'T pipe password inline: `echo $DEPLOY_VPS_PASS | ssh root@host cmd` — leaks via `ps aux` process list visible to all users on host
• DON'T skip the curl-verify step (local MD5) — corrupted files will 500 silently on prod, debug takes 30 min to trace
• DON'T ship without --title matching slug — old URL stays live, breaks SEO, and /API/list returns confusing duplicate
• DON'T ship 50MB+ files via ship.py — paramiko SFTP struggles; use `rsync -avz --progress app.html root@host:/srv/apps/` directly
• DON'T run ship.py from CI/CD without --confirm flag — accidental prod pushes from broken main branch are real incidents
🔀 Alternatives
• 8.4 Paramiko (raw SSH)
• 8.5 GitHub Actions (CI/CD)
• 8.9 Docker (containerized deploy)
8.2 🌐 nginx Configuration (vhost + proxy + cache + CORS) Infrastructure Intermediate ⏱ 1 day

Virtual host + reverse proxy + gzip + rate limit + try_files SPA routing. Serves every codehub.sj88ai.com app + proxies /api to FastAPI on 127.0.0.1:8765.

🎯 When to use: Every CodeHub static + dynamic site; reverse-proxy to uvicorn:8000 or codehub-api:8765; SSL termination; HTTP/2; SPA routing for client-side apps
✅ Pros
• 1 config file = 1 site; reload with `nginx -s reload` = zero-downtime config change (workers keep serving, new config loads on next request)
• try_files $uri $uri/ /index.html = SPA routing — any 404 falls back to index.html, lets React/Vue/Babylon apps use HTML5 pushState
• location priority predictable: exact (`= /api`) > prefix-with-=`^~` > regex (`~ \.php$`) > plain prefix = no surprise matching
• proxy_pass + X-Forwarded-Proto/X-Forwarded-For = real client IP/host seen by FastAPI; otherwise logs all show 127.0.0.1
• gzip + brotli on text assets = 60-80% bandwidth saved on HTML/JS/CSS; `gzip_types` whitelist prevents wasted CPU on already-compressed jpg/png
• limit_req zone=api burst=20 nodelay = basic DDoS protection at edge; nginx handles 10k concurrent limits with ~16 bytes/IP memory
⚠️ Cons
• Config syntax strict — missing semicolon = entire site fails to reload (no partial apply); `nginx -t` before reload is mandatory
• No dynamic vhosts — adding 100 subdomains needs 100 server blocks OR wildcard cert (latter = security hole, any subdomain resolves)
• Default worker_processes = 1 — on 4-core VPS, set `worker_processes auto;` (auto = N CPU cores) to handle concurrent requests
• log_format default misses $request_time and $upstream_response_time — define your own; otherwise can't debug slow /api responses
• rate-limit zone memory = 16 bytes per IP × limit zones — high cardinality IPs (botnets) can OOM nginx on 1GB VPS
• `if` is evil: `if ($var = X)` creates a new context, breaks other directives (use `map` instead for variable assignment)
⚡ Gotchas
• `location /api/` vs `location /api` (trailing slash) = completely different match — `/api` matches /api-v2/, `/api/` doesn't
• alias vs root: `location /static { root /var/www; }` appends full path (`/var/www/static/foo.png`); `alias /var/www/static;` strips prefix (`/static/foo.png` → `/var/www/static/foo.png`)
• Cache-Control: no-cache ≠ no-store — no-cache = must revalidate before serving cached; no-store = never store at all (different semantics)
• Worker connections default 768 — on busy sites, set `worker_rlimit_nofile 65535;` BEFORE `events { worker_connections 4096; }`
• If you `proxy_pass http://127.0.0.1:8000;` (with trailing /) the path is STRIPPED — `location /api/ { proxy_pass http://127.0.0.1:8000/; }` removes /api prefix
• try_files BEFORE fastcgi_pass = required, otherwise 404s go to FastAPI and choke it; order: try_files → fastcgi_pass → fastcgi_params
• After editing config, run `nginx -t` BEFORE `nginx -s reload` — typos take the WHOLE site down (last good config still in workers, new fails)
• Add HSTS header AFTER SSL is confirmed working: `add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;` — pre-SSL HSTS = self-inflicted outage
🚫 Anti-patterns
• DON'T put site config in /etc/nginx/sites-enabled/ AND include it in nginx.conf twice (duplicate `listen 80` = `nginx: [emerg] duplicate listen options`)
• DON'T use `if` for redirects — use `return 301 https://$host$request_uri;` (3x faster, no context creation, easier to read)
• DON'T set `server_tokens off;` AND `more_clear_headers Server;` (use one, both = wasted work; server_tokens is enough)
• DON'T `proxy_pass http://upstream;` without `proxy_set_header Host $host;` — backend sees wrong host, virtual-hosted apps (multi-tenant) break
• DON'T gzip image files (jpg/png/webp/gif) — already compressed = waste CPU; only text/html/css/js/svg/json/xml
🔀 Alternatives
• 8.10 CDN (edge cache)
• Apache httpd (.htaccess per-dir)
• Caddy (auto-HTTPS, simpler config)
8.3 🔒 Let's Encrypt SSL (certbot + auto-renew) Infrastructure Beginner ⏱ 30 min

Free DV certs via certbot. Auto-renew via cron. 90-day expiry. Currently secures codehub.sj88ai.com + subdomains.

🎯 When to use: Every production HTTPS site; wildcard certs for *.codehub.sj88ai.com; staging: certbot --staging to avoid rate limits; new domain going live
✅ Pros
• Free DV certs (no paperwork, no payment, no email verification) for unlimited domains per cert (up to 100 SANs per cert)
• Auto-renew via `0 3 * * * certbot renew --quiet` = zero ops; certbot handles ACME challenge + nginx reload via --post-hook
• Wildcard via DNS-01 challenge = ONE cert for ALL subdomains (*.codehub.sj88ai.com); add a new subdomain = zero cert work
• 90-day expiry = lower window of compromise if private key leaks (vs 1-year paid certs); shorter shelf life = less time to crack
• Pre-built nginx plugin = `certbot --nginx -d x.com` edits nginx config + reloads in one step; no manual CSR generation
• Rate limit is high (50 certs/week per domain, 5 duplicate) — won't bite legitimate use; staging server for dev
⚠️ Cons
• Rate-limited: 50 certs/week per registered domain, 5 duplicate; if you regenerate aggressively, you hit the wall (use --staging for dev)
• HTTP-01 challenge requires port 80 open + accessible from internet; if firewall blocks, use DNS-01 instead (needs DNS provider API)
• Wildcard certs only via DNS-01 = need API access to DNS provider (Cloudflare, Route53, DigitalOcean, Hetzner DNS, etc.)
• No EV/OV certs = no green bar in browser; some enterprise users / financial apps may distrust DV-only (rare in 2024)
• Cert lives in /etc/letsencrypt/live/ — symlinks to /archive/ — confused newbies edit archive (don't, edits lost on next renew)
• Auto-renew cron needs to be testable — add `--dry-run` flag to test, or you'll be paged at 3am by expired cert (oh the irony)
⚡ Gotchas
• After `certbot --nginx`, it adds 2 listen lines (80→301 redirect, 443 ssl) — verify with `nginx -t` BEFORE reload; typo = 30min outage
• Cron PATH is minimal — use FULL paths: `0 3 * * * /usr/bin/certbot renew --quiet --post-hook '/usr/sbin/nginx -s reload'` not `certbot renew`
• If you revoke and re-issue, you may hit rate limit (5 duplicate/week) — use `certbot renew --force-renewal` only in real emergency
• DNS-01 challenge TTL: lower TXT-record TTL to 60s BEFORE certbot runs, restore after (otherwise 5min TTL = cert fails propagation)
• Port 80 MUST answer 200 for HTTP-01 — if you `return 301 https://` BEFORE the challenge, cert fails; certbot --nginx handles order correctly
• Let's Encrypt CA chain has 2 certs (R3 intermediate + ISRG Root X1) — older Android < 7.1.1 needs cross-signed ISRG Root X2; mostly fixed in 2024
• Certbot's --nginx plugin only works if it recognizes the vhost — custom config may need manual `include /etc/letsencrypt/options-ssl-nginx.conf;` line
• Test renewal: `certbot renew --dry-run` runs FULL ACME flow against staging server but doesn't store cert — run this BEFORE relying on cron
🚫 Anti-patterns
• DON'T commit /etc/letsencrypt/ to git — private key leak = impersonate site FOREVER (no cert rotation will help if attacker has the key + DNS)
• DON'T run certbot interactively in CI — use `--non-interactive --agree-tos -m ops@sj88ai.com --no-eff-email` flags; otherwise CI hangs forever
• DON'T set cron to renew daily (1 success/day wastes rate limit slot); weekly (Mon 03:00) is plenty — 90 day cert has 60-day buffer
• DON'T ignore renew failures — `certbot renew` exit code is reliable; alert on non-zero in monitoring (8.7) to catch issues before expiry
• DON'T mix certbot certs with manually-installed certs in same nginx server block (different chain files = TLS handshake confusion, browsers see as invalid)
🔀 Alternatives
• paid DV (DigiCert, Sectigo) for EV/OV
• Cloudflare (auto-SSL + DDoS)
• Caddy (auto-HTTPS built-in)
8.4 🔌 Paramiko (Python SSH + SFTP) Infrastructure Intermediate ⏱ 2 hours

Pure-Python SSH2 + SFTP client. Powers ship.py (8.1) deploy automation from sandbox to VPS without ssh CLI in PATH.

🎯 When to use: Python deploy scripts (ship.py 8.1); ad-hoc remote commands from code; multi-host orchestration; reading remote logs/files via SFTP; running in Lambda/Cloud Functions/sandbox envs without ssh CLI
✅ Pros
• Pure Python = no ssh CLI binary in container; works in Lambda, Cloud Functions, sandbox envs where /usr/bin/ssh missing
• SFTP client built-in (sftp.put, sftp.get) = one library for shell exec + file transfer + remote stat; no separate pysftp dep
• AutoAddPolicy = no `known_hosts` prompt in fresh dev envs; still warns in prod (use RejectPolicy + ship known_hosts to deploy hosts)
• Channel-level exec gives you stdout/stderr/exit-code = real shell semantics in Python; run `nginx -s reload` and check exit code
• Pluggable auth: password ($DEPLOY_VPS_PASS), key file, ssh-agent, GSSAPI = same API for all SSH needs; no separate paramiko RSA / ECDSA libs
• Async support via `asyncssh` (drop-in compatible) if you need 100+ concurrent connections; paramiko itself is sync, GIL-friendly
⚠️ Cons
• Slower than ssh CLI for large files (paramiko uses pure-Python SFTP vs OpenSSH's optimized C); 50MB+ = noticeable delay (use scp/rsync instead)
• No native ProxyCommand support (jump hosts / bastion) — needs `paramiko.ProxyCommand` workaround with manual channel setup; ssh -J is simpler
• AutoAddPolicy = MITM vulnerability in untrusted networks — always use `RejectPolicy` in prod with known_hosts pre-shipped to deployer
• Connection pool not built-in — you must wrap in your own context manager; long-lived process leaks FDs if you forget client.close()
• Python 3.12+ deprecated some threading APIs paramiko used; need to keep paramiko >= 3.4 for cryptography library compatibility
• No built-in retry/backoff — if VPS is rebooting, your script crashes with ConnectionError; wrap in try/except + exponential backoff yourself
⚡ Gotchas
• NEVER hardcode password — `password=os.environ['DEPLOY_VPS_PASS']` reads from env at runtime; `password = 'hunter2'` ends up in git history forever
• Use `transport.default_window_size = 2147483647` for files > 100MB (default 2MB = slow); also `transport.default_packet_size = 32768`
• SFTP `put()` callback gives bytes-transferred — show progress bar with `tqdm` wrapper: `sftp.put(local, remote, callback=lambda x,y: progress.update(y-x))`
• Channel `recv()` BLOCKS — use `channel.recv_ready()` to check before reading (avoid timeout); also set `timeout=` in exec_command for slow commands
• After `exec_command`, MUST call `stdout.channel.recv_exit_status()` or you'll race the close — exit code 0 = success, non-zero = failed
• On key auth, key file must be mode 600 (chmod 600 ~/.ssh/id_rsa) or sshd rejects; paramiko doesn't auto-chmod; CI secrets often have wrong perms
• Keep-alive: set `transport.set_keepalive(30)` or NAT/firewall drops connection after 60s of idle (Cloudflare Tunnel, AWS NAT Gateway drop)
• Closing client: `client.close()` is enough — don't manually close channel/sftp first (race condition; close client = close all transports)
🚫 Anti-patterns
• DON'T use `set_missing_host_key_policy(AutoAddPolicy())` in PROD — MITM risk; first deploy can pin, then switch to RejectPolicy
• DON'T store password in script as string — `password = 'mypassword'` ends up in git history; use env var or secrets manager (1Password, AWS SM)
• DON'T reuse SSHClient across threads — each thread needs its own (paramiko is not thread-safe per client); use threading.local() or per-thread client
• DON'T exec raw user input via channel — shell injection (`; rm -rf /` in command = disaster); use paramiko's invoke_shell with explicit command array
• DON'T skip `transport.close()` on error paths — connection leaks fill your VPS file descriptors (`ulimit -n` = 1024 default; 200 deploys = OOM)
🔀 Alternatives
• ssh CLI (faster, simpler)
• asyncssh (async)
• Fabric (higher-level wrapper)
• 8.5 GitHub Actions (managed CI)
8.5 ⚙️ GitHub Actions (CI/CD + matrix + secrets) Infrastructure Intermediate ⏱ 1 day

CI/CD — test on push + auto deploy. Free for public repos. matrix strategy, cache, secrets, reusable workflows. For OS/multi-dev CodeHub projects.

🎯 When to use: OS projects (stronghold-3d, realm-of-heroes repo); multi-dev team; need PR checks before merge; need matrix (Node 18/20/22); scheduled jobs (nightly build); release automation
✅ Pros
• Free for public repos = unlimited CI minutes; 2000 min/month for private repos (plenty for 5-10 commits/day per repo)
• Matrix builds = test Node 18/20/22 in parallel with 1 line: `strategy: { matrix: { node: [18, 20, 22] } }` — catches version-specific bugs
• Cache npm/pip via `actions/cache@v4` = 5x faster installs after first run; key on lockfile hash so cache invalidates on dep changes
• Self-hosted runners = unlimited minutes + access to internal network (e.g., ship.py to VPS that has firewall); tag with labels for routing
• Reusable workflows = share deploy logic across 10 repos without copy-paste; `uses: org/codehub/.github/workflows/deploy.yml@v1`
• Secrets via GitHub UI = `${{ secrets.DEPLOY_VPS_PASS }}` injected at runtime, NEVER in PR logs (PR from fork = no secrets, by design)
⚠️ Cons
• Debug = read YAML logs (no live shell, no breakpoints) — 30 min to fix a typo in a 200-line workflow; SSH debug action ($/mo) is the workaround
• GitHub-hosted runners = no persistent state (each job = fresh Ubuntu VM, 7GB RAM); cache is your only continuity; can't install once-and-keep
• Free tier = 2000 min/month private; heavy repos (build iOS, run E2E) burn through it fast ($0.008/min over); use self-hosted for unlimited
• No native SSH to private VPS without storing credentials in secrets (audit-trail cost; rotate quarterly; never log ${{ secrets.X }} in run: blocks)
• Pull request from FORK = NO secrets access (security = expected, but blocks external contribs from running deploy steps); use PR-target branches instead
• Workflows are YAML + Jinja — version control them, but tests-for-tests (meta-CI) is hard; yaml-lint via pre-commit is your only safety net
⚡ Gotchas
• Cache key MUST include lockfile hash: `key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}` — without hash, cache never invalidates on dep change
• Use `actions/checkout@v4` with `fetch-depth: 0` for git-based tooling (semantic-release, changelog generators need full history)
• Step-level `if: github.event_name == 'push' && github.ref == 'refs/heads/main'` for env-specific deploys; different stages = different triggers
• Job timeout default = 360 min (6h!); set `timeout-minutes: 15` to fail fast on hangs; per-step `timeout-minutes: 5` for individual commands
• Self-hosted runners: tag them `runs-on: [self-hosted, linux, x64, gcp-fra1]` to avoid stealing for wrong job; register once, reuse forever
• Concurrency group: `concurrency: { group: ${{ github.workflow }}-${{ github.ref }}, cancel-in-progress: true }` = no double-deploys on rapid pushes (5 pushes in 1min = 1 deploy)
• Use `env:` block, NOT inline `${{ secrets.X }}` in `run:` — Jinja substitution in shell = escaping nightmare (quote, $, backtick); env block = clean
• If you `uses: third-party/action@v3`, PIN to SHA not tag: `uses: actions/checkout@<40-char-sha>` (tags can be re-published maliciously — 2021 nx compromise)
🚫 Anti-patterns
• DON'T run `npm install` without `npm ci` in CI — `ci` is lockfile-pinned (reproducible), `install` is permissive (modifies lockfile, surprise breakage)
• DON'T put `${{ secrets.X }}` directly in a `run:` step — echo'd in plain-text logs visible to anyone with repo read; use `env:` block (`env: KEY: ${{ secrets.X }}`)
• DON'T use `actions/upload-artifact@v3` — DEPRECATED and broken in Node 20 (v3.4+); use `@v4` always; v3 = silent failure on GitHub Enterprise
• DON'T `uses: actions/checkout@main` — pin to `@v4` or SHA (main branch can be hijacked via PR); v3 is also deprecated (uses Node 16 = EOL)
• DON'T ignore step failures — `continue-on-error: true` masks real problems; use only for non-critical steps like Slack notifications (failed deploy = still notify)
🔀 Alternatives
• 8.1 ship.py (local)
• GitLab CI (self-host)
• Jenkins (legacy, full control)
• CircleCI (faster matrix)
8.6 ⚙️ systemd (Service Manager + Auto-Restart) Infrastructure Intermediate ⏱ 2 hours

Long-running services — uvicorn/codehub-api with Restart=always, EnvironmentFile, journalctl logging, Type=notify for readiness signal.

🎯 When to use: Every backend service (uvicorn, gunicorn, node, redis-server, codehub-api FastAPI); background workers; long-lived daemons; production-only service management
✅ Pros
• Restart=always = service recovers from crashes in < 5s (tested with `kill -9 $(pidof uvicorn)` → systemd restarts in 2.3s); SLA-grade
• Type=notify + sd_notify() = systemd knows when service is ACTUALLY ready (waits for HTTP 200 from /api/health before routing traffic) — no race conditions
• EnvironmentFile=/etc/codehub/api.env = secrets NOT in unit file, NOT in shell history; perms 600 = secrets visible only to root
• WorkingDirectory + ReadOnlyPaths/ReadWritePaths = service can't accidentally `rm -rf /` (whole FS marked RO except /var/lib/codehub)
• journalctl -u codehub-api -f = structured logs with rotation (no logrotate needed); systemd handles log file size + retention
• `systemctl status codehub-api` = PID, memory, restart count, last 10 log lines in ONE command; ops debugging = 5s not 5min
⚠️ Cons
• systemd-specific (Linux only) — Mac dev / Windows WSL requires Docker alternative; Alpine uses OpenRC (different syntax)
• Unit file syntax = learn a new format (INI-like with sections [Unit]/[Service]/[Install]); errors are cryptic (`status=203/EXEC` = no such file)
• Restart=always WITHOUT StartLimitBurst = crash loop floods logs (OOM killer hits 100x in 1 min = 100 log lines); set `StartLimitIntervalSec=60, StartLimitBurst=3`
• Environment vars from .env are NOT auto-loaded (unlike Docker) — need `EnvironmentFile=/etc/codehub/api.env` or explicit `Environment=KEY=VAL` in unit
• Service can't see user's SSH agent keys (default `PrivateTmp=yes` hides /tmp + agent socket); use `LoadCredential=` for keys instead
• Hard to debug in dev: `systemctl --user` doesn't work for services in /etc/systemd/system/ — must be in ~/.config/systemd/user/ AND XDG_RUNTIME_DIR set
⚡ Gotchas
• After editing unit file: `systemctl daemon-reload` THEN `systemctl restart codehub-api` — forgot reload = old config still in effect (silent no-op)
• `systemctl enable` only creates symlinks — DOESN'T start. Use `systemctl enable --now` for both (saves 1 command, common typo trap)
• Logs: `journalctl -u codehub-api --since '1 hour ago' -p err` = filter by time + severity; `-p err` shows only err/crit/alert/emerg levels
• User=root is dangerous — prefer `User=codehub`, `Group=codehub` with `chown -R codehub:codehub /srv/app`; unit file runs as that user
• Hardening: `NoNewPrivileges=true`, `ProtectSystem=strict`, `PrivateTmp=true`, `ProtectHome=true` = service can't tamper with host even as codehub user
• Graceful stop: `KillSignal=SIGTERM` and Python handles it (uvicorn exits cleanly, finishes in-flight requests); SIGKILL = data loss + 502s for 1s
• Resource limits: `MemoryMax=512M`, `CPUQuota=50%` = OOM-killer doesn't take down whole VPS; service throttles itself, gets a clean shutdown
• Type=simple (default) starts immediately; Type=notify waits for sd_notify(READY=1); wrong type = service marked 'running' before actually ready → load balancer sends traffic to dead instance
🚫 Anti-patterns
• DON'T use `Type=oneshot` for long-lived service — systemd thinks it ended and won't restart (oneshot is for `backup.sh` that runs + exits cleanly)
• DON'T put `ExecStart=/usr/bin/python3 app.py` without `Restart=always` — one crash = service down forever until manual `systemctl start`
• DON'T put env vars in unit file (`Environment=DB_PASS=hunter2`) — file readable by anyone with sudo / ls -la; use EnvironmentFile with 600 perms
• DON'T use `User=root` unless absolutely necessary — privilege escalation blast radius (one RCE in app = root on VPS); prefer codehub user + setcap for specific ports
• DON'T skip `systemctl daemon-reload` after editing unit — silent: old config still active, new directives ignored; 5min debug time wasted on this
🔀 Alternatives
• 8.9 Docker (portable)
• supervisord (legacy, Python)
• PM2 (Node-only)
• tmux/screen (dev only)
8.7 📊 Monitoring (Uptime + /api/health + Alerts) Infrastructure Intermediate ⏱ 4 hours

Uptime monitoring via /api/health endpoint, log scraping via journald→Loki, alerts to Telegram. Catches issues before users do.

🎯 When to use: Production services (codehub-api); uptime SLA needs (>99% = need monitoring); capacity planning (CPU/mem trends); on-call rotation; multi-VPS fleet
✅ Pros
• /api/health endpoint = 1 endpoint to tell you service is up (200 + JSON); no agent on host, no sidecar; UptimeRobot/Pingdom can hit it for free
• Pull-based: UptimeRobot/Pingdom hits /api/health every 60s; alert if 3 consecutive misses (180s = real outage, not network blip)
• Log scraping via journald → Loki → Grafana = query logs by app, time, regex; free tier works for small VPS (50GB/month logs)
• Prometheus + node_exporter = CPU, memory, disk, network per VPS (free, self-hostable); Grafana dashboards in 1 hour
• Synthetic checks = curl /api/projects every 5min, alert if 0 results returned (catches silent corruption: API returns 200 but empty data)
• Alerts to Telegram/Slack via webhook = 0-cost paging (no PagerDuty subscription, $0/month); ack via emoji reaction, thread for runbook
⚠️ Cons
• Pull-based monitoring misses 90s windows if monitor itself is down (UptimeRobot has 99.99% SLA but their outage = your false alarm); use 2+ providers
• /api/health returns 200 doesn't mean service is functional — add DB ping, cache hit check, recent-write test (silent 200 with broken DB is real)
• Self-hosted Prometheus + Grafana = another thing to monitor (dogfooding paradox); for solo dev, prefer SaaS (Grafana Cloud free = 10k metrics)
• Log volume grows fast — 100MB/day/host is normal; 30-day retention = 3GB = $$; sample 10% of info logs, keep 100% of error logs
• Alert fatigue is real — 10 alerts/day = nobody reads them; tune to 1 alert/day = page-worthy (combine related signals into 1 alert)
• Metrics without context = useless (Prometheus: `up=1` means what? need labels: `up{instance="vps-fra1", job="codehub-api"} = 1`); label everything
⚡ Gotchas
• /api/health should return 200 even if DEGRADED (load balancer takes it out of rotation only if 503); 200/JSON with `degraded: true` is signal, not outage
• Include version + uptime + project count in /api/health — debug 'is this the new deploy?' in 1 call (no SSH to read /srv/codehub-api/VERSION)
• Don't put heavy logic in /api/health — every monitor hits it; if health check takes 2s, that's bad (cascades to 60s+ under load)
• Log time format: ISO 8601 + UTC (`logger.info(json.dumps({ts: datetime.utcnow().isoformat() + 'Z', level, msg}))`) — no local time confusion in 30s of log
• Monitor the monitor: run a cron that hits your monitoring URL — if monitor is down, alert via DIFFERENT channel (SMS via Twilio = $0.0079/msg)
• SLO ≠ SLI ≠ SLA: SLI = measurement (99.9% uptime), SLO = target (99.9 over 30 days), SLA = contract (refund if breached); SLO is internal target, SLA is customer-facing
• Disk fills up silently — alert on `df -h | grep '9[0-9]%'` via cron + Telegram (no Prometheus needed); full disk = 503s + data loss
• Use health check with random nonce to defeat cache: `/api/health?t=$RANDOM`; some nginx configs cache /api/* responses (8.2 `expires 1h`)
🚫 Anti-patterns
• DON'T alert on every 5xx — alert on rate > 1%/min (otherwise log noise = alert noise = oncall ignores all = misses real outage); aggregate first
• DON'T have /api/health that hits the DB (down DB = health check fails = load balancer pulls it = no recovery = stuck in degraded forever); test cache/queue only
• DON'T use email-only alerts (delayed 5-15min, spammed, no ack) — webhook to chat = real-time + ack-able (Telegram/Slack free)
• DON'T monitor without runbook (alert fires, no one knows what to do = wake up, read code, fix = 2h instead of 5min); write runbook in alert message
• DON'T trust uptime monitors that share your DNS — if DNS dies, monitor says 'your site is down' but service is fine; use IP-based ping as ground truth
🔀 Alternatives
• 8.6 systemd (built-in journald)
• UptimeRobot (free SaaS)
• Datadog ($$$)
• Prometheus+Grafana (self-host)
8.8 💾 Backup (3-2-1 rule, 16TB XFS, rsync) Infrastructure Beginner ⏱ 1 hour

rsync + 16TB XFS at /mnt/16tb/codehub + 3-2-1 rule (3 copies, 2 media types, 1 offsite). Nightly cron, monthly restore-test.

🎯 When to use: Production data (SQLite DB, user uploads, configs); recovery SLA < 1 day; data > 1GB; regulatory compliance (GDPR data residency); pre-launch baseline
✅ Pros
• 3-2-1 rule = industry standard: 3 copies (primary + 2 backups), 2 media types (XFS disk + B2 cloud), 1 offsite (B2 encrypted); survives 2 simultaneous failures
• rsync -a = only DELTAS transferred after first sync (16TB → 16TB after = 1GB transfer for daily changes); 2-hour nightly run vs 16-hour full copy
• XFS at /mnt/16tb/codehub = handles 8M files in single directory (ext4 limits to ~1M = 1.7M projects case-study); xfs_freeze for atomic snapshots
• Cron daily at 02:00 SGT = low contention; can layer with --link-dest for cheap incremental snapshots (hard-link tree = near-zero space)
• Offsite via rsync.net or Backblaze B2 = $5/TB/month; encrypted at rest (gpg -c) = safe to use public cloud storage (B2 leak = encrypted blob)
• Verification: `rsync --checksum` on monthly schedule catches silent corruption (bit rot = 1 in 1M chance per file per year, real for 8M files)
⚠️ Cons
• Storage cost: 16TB XFS = $300-500 one-time; B2 offsite = $5/TB/month = $80/month for 16TB; 2nd physical disk = $300; total $1.2k year 1
• Backup is NOT a backup until you've RESTORED from it (test monthly or you'll find out during outage = worst time to discover corruption)
• rsync window = 2-6 hours for 16TB at 1Gbps; nightly cron must finish before next cycle (06:00 deadline) or jobs pile up
• XFS CAN'T shrink filesystem (only grow) — if you over-allocate, wasted space forever (mkfs.xfs 10TB, only 4TB used = 6TB gone)
• No built-in encryption at filesystem level (LUKS underneath OR eCryptfs on top = complexity); B2 upload must encrypt separately (gpg)
• Backup of /mnt/16tb itself isn't a backup (it's the SAME disk) — need 2nd physical disk OR offsite (B2) for true redundancy
⚡ Gotchas
• Cron must use `rsync -aHAX` (a=archive, H=hard-links, A=ACLs, X=xattrs) — default `rsync -a` misses ACLs + xattrs = corrupted permission on restore
• Use `--delete` ONLY on the primary → mirror, NOT on mirror → source (delete on mirror = delete on source next sync = data loss); one-way sync only
• Pre-flight: `df -h /mnt/16tb` before rsync — disk full mid-sync = corrupted backup (worse than no backup; gives false confidence); abort if <10% free
• Permission check: backup target must be writable by `backup` user (set `uid`, `gid` in /etc/fstab or chown); ssh key in ~backup/.ssh/ with 600 perms
• Offsite: encrypt BEFORE upload (`tar czf - /data | gpg -c --batch --passphrase-file /root/.b2_key > backup.tar.gz.gpg`); B2 leak = encrypted blob = useless to attacker
• Restore test: pick a RANDOM file monthly, restore to /tmp, `diff` against source (catches bit rot, ACL drift, partial corruption); 10min test saves 10h outage
• Database dumps: `pg_dump dbname | gzip > backup.sql.gz` BEFORE file rsync (DB files locked during run, rsync = inconsistent); cron order: dump → rsync
• XFS quota: if you set project quota on /mnt/16tb/codehub, backup user needs `noquota` mount option OR backup fails halfway with 'disk quota exceeded'
🚫 Anti-patterns
• DON'T backup to same disk only (one disk failure = both gone) — need physical separation (2nd disk) + offsite (B2) for 3-2-1
• DON'T skip restore testing — found-out-during-fire is the worst time to discover backup is corrupted; test monthly minimum
• DON'T backup /var/log (recovers itself, just rotation needed; saves 50% of backup size for chatty apps); backup only /etc, /home, /srv, /opt, DB dumps
• DON'T use `cp -r` for backups (no delta, no progress, no resume, no --delete, no ACLs) — terrible for 100GB+; rsync or restic or borg only
• DON'T store backup credentials in cron (root crontab is world-readable on some systems) — use /etc/cron.d/backups with mode 600, owned by root:root
🔀 Alternatives
• restic (deduplication, modern)
• borgbackup (encryption built-in)
• ZFS snapshots (different FS)
• AWS Backup (managed)
8.9 🐳 Docker (Dockerfile + Compose + Multi-Env) Infrastructure Intermediate ⏱ 1 day

Dockerfile + docker-compose for reproducible multi-service env. NOTE: NOT currently used at CodeHub — all apps are static, served directly by nginx. Listed for future use.

🎯 When to use: Multi-service architecture (web + worker + db + cache in 1 stack); need reproducible env across dev/CI/prod; on-prem deploy; microservice split; 12-factor app compliance
✅ Pros
• Reproducible env: Dockerfile pins OS + Node/Python version = 'works on my machine' solved forever (sha256 base image = exact bit-for-bit)
• Multi-stage builds: `FROM node:20 AS build` then `FROM nginx:alpine` = final image 50MB not 1.5GB (intermediate build layer discarded)
• docker-compose = 1 YAML file to spin up web+db+cache (no separate Postgres install on dev box; `docker compose up` = full stack in 30s)
• Layer cache: only changed layers rebuild (3-line package.json change = don't reinstall 500 deps; CI rebuild = 8s not 5min)
• Portability: same image runs on dev laptop, CI, prod VPS, k8s, ECS, Cloud Run = no deploy-specific bugs (no 'prod-only' mystery errors)
• Ecosystem: official images for almost every service (postgres, redis, nginx, node, python, rust, go) = no manual install + security patching
⚠️ Cons
• Image size: even multi-stage Node image = 100-200MB; alpine = 50MB but musl libc breaks some native modules (sharp, bcrypt, canvas); debian-slim = 80MB compromise
• Daemon overhead: Docker Desktop on Mac = 2GB RAM just for the VM (linuxkit); can be slow on old laptops; OrbStack = lighter alternative
• Security: privileged containers = root on host; always set `user:` and `read_only: true` in compose; rootless docker for paranoid mode (loses some features)
• Orchestration beyond 1 host = need k8s or swarm (massive learning curve vs docker-compose); k3s = lightweight option for edge/multi-host
• Storage: anonymous volumes fill `/var/lib/docker`; set `VOLUME` named OR `docker system prune -a` weekly to reclaim (10GB+ easily)
• BuildKit cache: without it, rebuilds are slow; with it, syntax quirks (`# syntax=docker/dockerfile:1.4`); Buildx = more features, more YAML
⚡ Gotchas
• `.dockerignore` is CRITICAL — without it, `COPY . .` copies `node_modules/`, `.git/`, `dist/`, `.env`, `coverage/` = 1GB+ image + secrets leak
• Don't run as root in container: `RUN adduser -D app && USER app` (set in Dockerfile, NOT compose); root in container = easy escape to host via misconfig
• CMD vs ENTRYPOINT: CMD = default arg to ENTRYPOINT; ENTRYPOINT = main executable; use exec form `CMD ["node", "server.js"]` not shell form `CMD node server.js` (no PID 1 = no signal handling)
• Build args vs env vars: `ARG` only at build time, `ENV` at runtime; secret in `ARG` = visible in `docker history` (recoverable!); use BuildKit secrets mount instead
• Compose healthcheck: `healthcheck: { test: ["CMD", "curl", "-f", "/health"], interval: 30s, timeout: 5s, retries: 3 }` = depends_on condition: service_healthy actually works
• Layer ordering: `COPY package*.json ./ && RUN npm ci` BEFORE `COPY . .` (otherwise any code change reinstalls all 500 deps = 5min build instead of 8s)
• Bind mounts on Mac: `.:/app` is slow (osxfs syncs both ways); use `:cached` (Mac writes to fs, slow) or `:delegated` (Mac is host of truth) for hot-reload dev
• Image tag: `node:20` = latest 20.x (rebuild changes base!); pin to `node:20.11.0` for reproducibility; for true immutability use digest `node@sha256:abc...`
🚫 Anti-patterns
• DON'T `FROM ubuntu:latest` — use specific version (`ubuntu:24.04`); latest = breaking changes on rebuild (glibc, openssl, base libs change without notice)
• DON'T `RUN apt-get update && apt-get install -y pkg` without `&& rm -rf /var/lib/apt/lists/*` (bloats image 100MB+; cached apt lists = bigger layer)
• DON'T store secrets in image (ENV DB_PASS=... visible in `docker inspect` + image layers); use Docker secrets, env at runtime, or BuildKit `--mount=type=secret`
• DON'T use `:latest` tag in production (rebuilds break; pin to digest `sha256:abc...` for true immutability); `:latest` = 'whatever is latest when you pull'
• DON'T run multiple processes in one container (web + worker) — `supervisord` is anti-pattern; use docker-compose with 1 process per container + orchestration
🔀 Alternatives
• 8.6 systemd (single-host)
• Podman (rootless daemonless)
• LXC (lighter isolation)
• k8s (multi-host orchestration)
8.10 ☁️ CDN (jsDelivr / unpkg / Cloudflare for static assets) Infrastructure Beginner ⏱ 30 min

Free CDNs for static JS libraries + GitHub assets. jsDelivr + unpkg for npm packages (karaoke-studio uses ffmpeg.wasm via unpkg). Cloudflare for full-site proxy.

🎯 When to use: Static JS libraries (CDN-hosted, no npm install needed); big files users download (ffmpeg.wasm = 30MB); public open-source assets (GitHub release → jsDelivr); cross-origin asset needs
✅ Pros
• jsDelivr serves any GitHub release: `https://cdn.jsdelivr.net/gh/user/repo@version/file` = free, fast, global edge network (200+ POPs)
• unpkg auto-resolves npm versions: `https://unpkg.com/[email protected]` = no manual upload, no version management; perfect for prototype/demo
• Cache hit ratio 95%+ on popular libs = 50ms TTFB from edge vs 500ms from origin (ffmpeg.wasm users in karaoke-studio load in 1.5s not 8s)
• Saves VPS bandwidth: 1M ffmpeg.wasm downloads/mo = 30TB off your VPS bill (VPS at 1Gbps = saturates at 1000 concurrent downloads)
• HTTP/2 multiplexing, Brotli, HTTP/3 all on by default = no config needed; edge servers tuned for static asset delivery (kernel TCP buffer, TCP fast open)
• Versioned URLs: pin `@0.12.10` = reproducible builds, no breaking changes on dependency update; roll back = change 1 string in code
⚠️ Cons
• Cache invalidation is ASYNC — `?purge=true` not supported on jsDelivr/unpkg; new version = new URL OR wait 24h for natural cache expiry
• jsDelivr has rate limits (unlimited per IP for small files, but heavy concurrent use gets throttled = 429); for >100 req/s, self-host or use Cloudflare
• unpkg had a 2-day outage in Dec 2021 — your app went down too (single point of failure; same risk for jsDelivr); fallback to local copy is good practice
• No custom headers (CORS, CSP) on jsDelivr/unpkg — relies on their default (open CORS = good for browser use, but no CSP nonce = can be blocked by strict CSP)
• Files in `/dist/` of npm packages may differ from main field — ESM/CJS resolution can be wrong; check browser console for 404s on sub-imports (e.g. `@ffmpeg/util`)
• Privacy: CDN sees all client IPs (their logs = your data leak vector for EU users under GDPR; Cloudflare has EU-only POPs, jsDelivr/unpkg = mixed)
⚡ Gotchas
• Importmap + unpkg: `<script type="importmap">{"imports": {"react": "https://esm.sh/[email protected]"}}</script>` for clean ESM; lets `import React from 'react'` work in browser without bundler
• Cache busting: `?v=1.2.3` in URL = forced refresh on deploy; CDN purges on URL change anyway, but old browsers (Safari) cache 7d = version param wins
• Subresource Integrity: `<script src="https://..." integrity="sha384-..." crossorigin="anonymous">` = CDN compromise can't inject malicious JS; browser blocks if hash mismatches
• For ffmpeg.wasm: core needs COOP/COEP headers (`Cross-Origin-Opener-Policy: same-origin`, `Cross-Origin-Embedder-Policy: require-corp`) or SharedArrayBuffer fails (karaoke-studio gotcha)
• jsDelivr strips LICENSE files from npm — for some packages, must self-host with license attached (GPL/AGPL compliance); check `unpackedSize` for license files
• unpkg.com caches forever for some browsers (Safari 14 bug) — version bump your imports to force refresh: `?v=2024-01-15` after a CDN change
• Cloudflare free tier = no custom cache rules; upgrade to Pro ($20/mo) for cache TTL control, page rules, workers; for hobby = free is fine
• Pin version, NOT @latest: `@ffmpeg/[email protected]` not `@ffmpeg/ffmpeg` (auto-bumps = breaking changes; `@0.12.10` → `@0.13.0` could remove your API)
🚫 Anti-patterns
• DON'T load from multiple CDNs for same lib (jsDelivr + unpkg = 2x requests on miss, 2x security audit, 2x SRI to maintain); pick one, stick with it
• DON'T use `@latest` or no version in URL — CDN edge caching means your users get random versions over time (some get 0.12.10, some get 0.13.0 = inconsistent bugs)
• DON'T trust CDN for critical-path WITHOUT SRI + fallback — CDN down = app down; self-host fallback in /assets/vendor/ (2x disk, 100% uptime guarantee)
• DON'T use `cdnjs.cloudflare.com` AND `cdn.jsdelivr.net` for same asset (wastes cache slots, increases TTI, 2x DNS lookup); pick one CDN per project
• DON'T bypass CORS by using `<script src>` for JSONP-style data — use proper Fetch with CORS headers; JSONP = XSS vector, deprecated, hard to debug
🔀 Alternatives
• 8.8 Backup (self-host)
• Cloudflare R2 (S3-compatible)
• AWS CloudFront (paid)
• self-host with nginx (8.2)
9.1 🔌 REST API Design Data & APIs Beginner ⏱ 1 day

Resource-oriented URLs + /api/v1/ + OpenAPI 3 + correct 4xx/5xx status codes.

🎯 When to use: ทุก backend ที่ expose data ให้ frontend/SPA/mobile consume / any client-server boundary where the contract needs versioning. Best for: CRUD, public APIs, integrations. EN: any backend that exposes data to SPA / mobile / third-party; needs cacheable, cache-friendly, stateless contracts.
✅ Pros
• Resource-oriented URLs (GET /api/projects/{slug}/versions) = human-readable, log-friendly, no method-name soup like /api/getProjectBySlug
• HTTP verbs map to CRUD (GET=read, POST=create, PUT=replace, PATCH=partial, DELETE=remove) — 30+ years of CDN/proxy/load-balancer tooling understands them
• OpenAPI 3 spec auto-generates client SDKs (TypeScript, Swift, Kotlin) — frontend never hand-writes fetch wrappers
• Cache-friendly by default: GET is the only safe verb; ETag + Last-Modified give 304s and save 90% bandwidth on list endpoints
• Status codes carry semantic load: 401=auth, 403=forbidden, 404=missing, 409=conflict (unique key), 422=validation — frontend can branch on status alone, not body shape
• Stateless server = horizontal scale trivially; no session affinity needed unless you add WebSocket (sub 9.7)
⚠️ Cons
• Over-fetching: GET /api/project returns full version history when list view only needs slug+name+icon — fix with ?fields=summary or /api/projects (light) vs /api/project/{slug} (full)
• Under-fetching: detail page needs 5 endpoints (project, versions, screenshots, files, ai-instructions) — fix with /api/project/{slug}/everything OR GraphQL
• N+1 query risk: returning nested arrays forces client to issue follow-up requests; consider /api/project/{slug}?expand=versions,files
• Versioning pain: /api/v1 vs /api/v2 forces URL change OR header (Accept: application/vnd.codehub.v2+json) — pick ONE strategy and stick to it
• Idempotency burden: POST is not naturally idempotent (double-submit = 2 records); need client-supplied Idempotency-Key header for any money/action endpoint
• No streaming: returning 10k rows blocks the response thread; for huge lists use pagination + cursor OR switch to GraphQL/SSE
⚡ Gotchas
• POST should return 201 Created + Location: /api/project/{new_slug} header — clients need the new ID, not just the body
• PUT replaces the WHOLE resource; PATCH for partial — never PATCH without Content-Type: application/merge-patch+json (or json-patch+json)
• DELETE should be idempotent (second DELETE returns 404, not 500) — clients retry on network failure and expect safety
• Pagination: ?limit + ?offset is easy but breaks when list changes; cursor (?cursor=base64) is robust but harder to implement
• Always return errors as JSON with 'detail' field (FastAPI default) — never HTML error pages from proxy, they break JSON parsers
• Trailing slash matters: /api/projects/ vs /api/projects — FastAPI redirects 307 by default, but the extra round-trip hurts mobile
• CORS preflight fires on every POST with Content-Type: application/json — 5x latency on cold path; pre-cache OPTIONS with Access-Control-Max-Age: 86400
• Sort order MUST be stable and explicit (?sort=created_at&order=desc) — relying on DB insertion order breaks after vacuum/reindex (SQLite) or replica failover (Postgres)
🚫 Anti-patterns
• RPC-in-REST-clothing: POST /api/getUserById with body {id: 5} — defeats caching, breaks OpenAPI tooling, hard to grep. Use GET /api/users/5
• Returning 200 with error body: {"status":"error","code":404,"msg":"not found"} — frontend must check BOTH http status AND body field. Just use 404
• Verbs in URLs: POST /api/createProject, GET /api/getProjectsList — the verb IS the HTTP method. /api/projects with POST is enough
• Mass assignment: PATCH /api/user/5 with body {role:"admin", is_superuser:true} — server MUST whitelist allowed fields, not blindly merge
• Using PUT for partial update: PUT /api/user/5 with body {email:"new@x"} wipes the name field. Use PATCH for partial
🔀 Alternatives
• 9.7 WebSocket (realtime push, not request/response)
• 9.8 SSE (server→client stream)
• GraphQL (when client shapes vary wildly)
9.2 FastAPI Backend Data & APIs Intermediate ⏱ 2 days

Async Python + Pydantic validation + auto /docs Swagger + Depends() DI. The batteries-included default for new Python APIs.

🎯 When to use: ทุก Python backend ใหม่, ML model serving, async I/O heavy (DB + HTTP + WebSocket), team ที่ต้องการ type safety. EN: any new Python API; async I/O bound work; need type-checked contracts.
✅ Pros
• Pydantic v2 validation at route boundary = early error catch with line-precise field paths (faster than hand-rolled if-elif chains)
• Auto OpenAPI 3 + Swagger UI at /docs + ReDoc at /redoc = frontend and QA never ask 'what's the contract?' — they read /docs
• Depends() injection makes auth/db/session one-liner per route — no contextvars, no thread-locals, no Flask g globals
• async def routes + anyio backend = one event loop handles 10k+ concurrent connections (vs Flask which is 1 thread per request by default)
• BackgroundTasks for fire-and-forget (rebuild, send email) without Celery — keeps the deploy surface small
• Pydantic models double as TYPE for the route + SCHEMA for /docs + VALIDATOR for the body — single source of truth
⚠️ Cons
• GIL still blocks CPU work even with async — pure Python number crunching stays single-core; offload to numpy/C/numba/ProcessPoolExecutor
• Pydantic v2 is fast (rust core) but cold start is ~80ms vs Flask ~15ms — matters for serverless (Lambda cold start penalty)
• Async footgun: blocking call (time.sleep, requests.get, sqlite3) inside async def stalls the event loop — use httpx.AsyncClient and aiosqlite
• Dependency injection can hide expensive work: Depends(get_db) that opens a new connection per request adds ~2ms — use connection pool (asyncpg pool)
• FastAPI version churn: 0.100 → 0.110 changed Pydantic v1→v2 migration, lifespan syntax, async test client — pin versions in requirements.txt
• OpenAPI auto-gen is great until you have 200+ endpoints: /docs page loads slowly and groups become a mess — split into routers + tags, use response_model_exclude_unset=True
⚡ Gotchas
• async def vs def: use async ONLY when you actually await something; sync def runs in threadpool (4-40 workers default) and is fine for blocking I/O like psycopg2
• Pydantic BaseModel inside List[Project] = N-times the validation cost — for 10k items use response_model_exclude_unset=True OR return plain dict
• Depends() executes PER REQUEST — putting heavy setup (compile ML model) there = recompile every request. Use module-level singleton or @lru_cache
• CORS preflight: add CORSMiddleware BEFORE any route; missing = browser blocks all POST from other origin
• Lifespan context manager (replaces on_event('startup')) is the v3 way — startup code in yield, shutdown in finally
• Response_model strips extra fields by default — useful for security (don't leak password_hash) but bites if you return dynamic dict
• Form vs Body vs Query: use Form() for multipart upload, Body() for raw JSON, Query() for ?key=val; mixing them in one route fails 422
• StaticFiles mount must come LAST or it shadows /docs: app.mount('/static', StaticFiles(directory='.')) AFTER all API routes
🚫 Anti-patterns
• Returning dict when you have a Pydantic model: return {'slug': p.slug} — drops validation, breaks OpenAPI schema, IDE loses autocomplete. Return ProjectOut()
• Global mutable state for db connection: db = Session() at module top — broken across workers (gunicorn -w 4 = 4 different sessions, requests leak). Use Depends(get_db)
• Putting business logic in route handler: 200-line async def that reads request, queries DB, formats response, sends email. Split into service.py + route just orchestrates
• Using @app.on_event('startup') in new code: deprecated in 0.110. Use lifespan context manager (async with Lifespan(app): ...)
• Returning ORM model directly: return db.query(Project).first() — leaks lazy-loaded fields on serialization, breaks response_model. Use .from_orm() or Pydantic v2 model_validate()
🔀 Alternatives
• Flask (simpler, no async, no auto docs)
• Django REST (heavier, batteries-included)
• Litestar (newer, similar)
9.3 🗄️ SQLAlchemy ORM Data & APIs Intermediate ⏱ 2 days

Python ORM with Alembic migrations, 2.0 typed Mapped[] syntax, async + sync engines.

🎯 When to use: ทุก relational DB project, multi-table queries, need migrations, team wants IDE autocomplete on query results. EN: any relational DB project; multi-table JOINs; need Alembic migrations; want typed query results.
✅ Pros
• Mapped[str] / mapped_column() in 2.0 = full type hints → mypy catches 'user.email' typo on Unmapped instance before runtime
• Unit of Work pattern: session.add() + session.commit() batches N statements into 1 transaction = 10x faster than per-row commits
• relationship() with lazy='selectin' = 1 query for parent + 1 IN-query for all children — fixes the classic N+1 (1 query → 101 queries for list of 100)
• Alembic autogenerate detects model diffs and writes migration: alembic revision --autogen -m 'add index' — no more handwritten DDL
• Engine-agnostic: same code runs on SQLite for tests + Postgres for prod via DATABASE_URL — CodeHub does this in api/db.py:get_db_url()
• Connection pool built-in (QueuePool default) — set pool_size=10, max_overflow=20 for 30-conn budget; pool_pre_ping=True auto-recycles dead conns
⚠️ Cons
• Async support in 2.0 still has rough edges: AsyncSession requires asyncpg + greenlet, lazy loading doesn't work, no sync-from-async bridge
• N+1 trap: query Project then access .versions in a loop fires 1+N queries. ALWAYS use selectinload(Version.project) or joinedload()
• Session lifecycle is footgun: forget session.close() = connection leak → pool exhausted → 500s. Use context manager: with session_scope() as s: ...
• Reflection (autoload_from) looks magical until schema changes break prod — explicit Base models are worth the typing
• Complex GROUP BY / window functions force you into raw SQL anyway — ORM stops helping past 'give me all rows'
• Alembic autogenerate MISSES: column renames (looks like drop+add), table renames, enum changes, server defaults — ALWAYS review the generated migration before shipping
⚡ Gotchas
• session = Session() is NOT thread-safe — share only via Depends(get_db) per request, NEVER as module global
• expire_on_commit=True (default) means accessing .name after commit fires a fresh SELECT — set False for read-heavy loops
• Relationship lazy='select' is the DEFAULT (per-row SELECT); for lists ALWAYS use selectinload() or subqueryload()
• String length: String(64) without length becomes TEXT in Postgres — fast for inserts but no index can be built on TEXT without expression index
• Index columns you WHERE/ORDER BY on, NOT columns you only SELECT — index has write cost, only useful if query uses it
• DateTime(timezone=True) — store UTC always; naive datetimes in Postgres = 'timestamp without time zone' which silently drops TZ
• session.add() does NOT flush — it queues. session.commit() flushes. To get auto-increment ID back, use session.flush() before commit()
• Pydantic v2 + ORM: use from_attributes=True in Config OR model_validate(user, from_attributes=True) — naive dump() misses relationships
🚫 Anti-patterns
• Model as dict: user.name when name is None returns 'None' string. Always check attribute access returns Optional[T]
• Committing in a loop: for u in users: session.add(u); session.commit() — 1000 commits = 1000 round-trips. Build list, commit once
• Mixing ORM and Core: query(Project).filter(Project.id == 1) is fine, but don't mix into(select(Version)) without aliasing — name collision errors are cryptic
• Forgetting rollback in exception handler: except Exception: pass — leaves session in poisoned state, next request may see stale data. Use context manager: with session_scope() as s:
• Using ORM for bulk insert > 10k rows: 10k INSERT statements are 100x slower than COPY (raw SQL). Use session.execute(insert(Project), [dict, dict, ...]) OR copy_expert()
🔀 Alternatives
• Django ORM (heavier, batteries-included)
• Tortoise ORM (async-first, Django-like)
• Peewee (light, sync only)
• raw asyncpg/psycopg (max perf)
9.4 💾 SQLite Data & APIs Beginner ⏱ 1 hour

File-based SQL DB. Zero install, ACID, FTS5, WAL mode, perfect for dev/cache/edge.

🎯 When to use: Single-user, dev/test, local cache, mobile (iOS/Android native), edge (Cloudflare D1 = SQLite), read-heavy embedded. EN: single-writer, dev/test, local cache, mobile native, edge serverless (D1/Turso).
✅ Pros
• Zero install: import sqlite3 (stdlib) and you're running. No docker, no apt-get, no env vars. CodeHub default in api/db.py
• ACID transactions out of the box — full crash safety via WAL (write-ahead log); no separate config
• FTS5 built-in full-text search with BM25 ranking — CodeHub's /api/search uses this for code+ai-instructions search in one .sqlite file
• One file = trivial backup (cp file.sqlite backup.sqlite), trivial migration (send the file), trivial inspection (DB Browser for SQLite)
• In-process = no network round-trip; queries are 5-50x faster than Postgres for small datasets (< 100k rows)
• Edge-deployable: Turso, Cloudflare D1, LiteFS — same SQLite dialect, runs at CDN edge in 5ms
⚠️ Cons
• Single writer at a time: even with WAL, writes serialize. Heavy insert workload (> 1k writes/sec) stalls readers waiting for lock
• No client-server: you can't connect from another machine without writing your own server. For multi-user app, jump to Postgres
• No user accounts / no GRANT system: any process with file read = full DB access. Mitigate with file permissions (chmod 600)
• No parallel write scaling: 1 process holding write lock blocks all other writers; high-write web app must use Postgres
• WAL file grows until checkpoint: on long-running writers, wal file can hit GB before auto-checkpoint. Set wal_autocheckpoint=1000
• Type affinity is loose: '5' (TEXT) and 5 (INTEGER) compare equal; explicit CAST or strict tables needed for type safety
⚡ Gotchas
• Enable WAL for any real workload: PRAGMA journal_mode=WAL — gives 1 writer + N readers concurrently instead of 1 writer OR 1 reader
• Foreign keys OFF by default: PRAGMA foreign_keys=ON must run per connection (or via SQLAlchemy event listener) — easy to miss for months
• busy_timeout: PRAGMA busy_timeout=5000 makes writers wait 5s for lock instead of failing immediately — critical for web apps under contention
• DateTime stored as TEXT (ISO8601) or REAL (julian) — neither has timezone; always store UTC and convert in app code
• Connection per thread (not per process): in threaded server (gunicorn -w 4 --threads 2), each thread needs its own connection, NOT shared
• NUL bytes in strings break: 'foo\x00bar' gets truncated. Sanitize input or use BLOB type for binary
• VACUUM after big DELETE: empty space doesn't auto-reclaim, file stays huge. Run VACUUM (or auto_vacuum=INCREMENTAL + pragma)
• Memory temp store: PRAGMA temp_store=MEMORY for ORDER BY on large results — defaults to disk which 100x slower
🚫 Anti-patterns
• Using sqlite for multi-user web app write path: 100 concurrent users = 1 writer + 99 waiters = 504 Gateway timeouts. Use Postgres
• Storing the .db file in /tmp on Linux: cleaned on reboot = total data loss. Always in a persistent, backed-up location
• Storing the .db file on a network filesystem (NFS/SMB): SQLite's lock protocol breaks on NFS — corruption guaranteed after 1 week. Local SSD only
• Forgetting to close connection: conn = sqlite3.connect('x.db') in a hot loop = 1000 file handles → 'too many open files'. Use context manager: with sqlite3.connect() as conn:
• Using INTEGER PRIMARY KEY as 'always increments from 1': after delete + VACUUM, IDs can be reused. Use AUTOINCREMENT keyword (with the perf cost) if monotonic required
🔀 Alternatives
• 9.5 PostgreSQL (multi-writer, client-server)
• DuckDB (OLAP, single-node columnar)
• JSON file (no schema, no queries)
9.5 🐘 PostgreSQL Data & APIs Advanced ⏱ 3 days

Production-grade RDBMS: MVCC, JSONB, FTS, partitioning, replication, 30+ years of battle-testing.

🎯 When to use: Multi-user production web app, complex JOINs (>3 tables), need JSON + relational hybrid, full-text search beyond SQLite FTS5, > 10k writes/sec, need read replicas. EN: any multi-user production system; complex queries; JSON+relational mix; need read replicas.
✅ Pros
• MVCC = readers NEVER block writers and writers NEVER block readers — high concurrency without table-level locks (vs SQLite's 1-writer limit)
• JSONB = indexed JSON column, query with ->> and @> operators: WHERE meta @> '{"category": "games"}' is index-accelerated
• Native full-text search: tsvector + tsquery + GIN index — better ranking than SQLite FTS5, supports 30+ languages out of box
• LIST/RANGE partitioning for time-series: CREATE TABLE ... PARTITION BY RANGE (created_at) — keep hot data fast, archive cold to cheap storage
• Logical replication: 1 primary → N read replicas async — scale reads to 100+ connections without app changes (PgBouncer pools in front)
• Generated columns, CTEs, window functions, lateral joins, UPSERT (ON CONFLICT DO UPDATE) — every feature you'd write in app code, already in DB
⚠️ Cons
• Ops burden: install, configure, monitor, backup, vacuum, autovacuum tune, WAL archive, replica lag, failover. Use managed (RDS, Supabase, Neon) unless you have a DBA
• Vacuum & bloat: dead tuples from UPDATE/DELETE accumulate; autovacuum must be tuned per table or you hit 'table is full' or 10x storage bloat
• Connection cost: each connection forks a process (~10MB RAM); default max_connections=100 = 1GB just for idle conns. Use PgBouncer in transaction-pool mode
• Slow COUNT(*): no cheap row count without table scan (unlike MySQL). Use pg_class.reltuples (estimate, fast) or maintain counter in separate table
• Migration downtime: ALTER TABLE on big table can lock for minutes (ADD COLUMN with default = full rewrite in < 12). Use pg_repack or pt-online-schema-change
• Replication lag: read replica can be 5-30s behind primary. 'Read your own write' needs session stickiness or read-after-write routing
⚡ Gotchas
• Use SERIAL or GENERATED ALWAYS AS IDENTITY (preferred, SQL-standard) — NOT 'id INTEGER PRIMARY KEY' (no auto-increment, manual sequence needed)
• Timestamptz (with timezone) NOT timestamp — naive timestamp silently drops TZ, then ORDER BY mixes Bangkok and UTC rows
• ILIKE for case-insensitive search: WHERE name ILIKE '%foo%' — but it's SLOW without trigram index. CREATE EXTENSION pg_trgm; CREATE INDEX ON users USING gin (name gin_trgm_ops)
• Connection pool: use asyncpg.create_pool(min_size=2, max_size=10) — NOT one connection per request (10x slower under load)
• VARCHAR(n) without length = TEXT, no perf difference. Use TEXT unless you need a constraint at DB level
• EXPLAIN ANALYZE before optimizing: shows actual row counts vs estimates. WHERE on unindexed column = Seq Scan = full table scan, kills perf
• Default isolation = READ COMMITTED. For 'no phantom reads' use REPEATABLE READ. For 'no serialization anomalies' use SERIALIZABLE (5x more retries)
• Backups: pg_dump for small, pg_basebackup for hot, WAL archiving for PITR. NEVER rely on just one of these — restore drill quarterly
🚫 Anti-patterns
• Storing everything in one JSONB column to 'avoid migrations': loses JOIN performance, can't enforce structure, IDE has no autocomplete. JSONB for VARIABLE parts only
• Using OFFSET for pagination: OFFSET 100000 skips 100k rows = 1s latency. Use cursor: WHERE id > last_id ORDER BY id LIMIT 20 (10ms regardless of depth)
• Forgetting to add index on FK column: queries like WHERE user_id = 5 do Seq Scan because FK doesn't auto-create index. Always CREATE INDEX ON child_table (fk_col)
• Using SELECT * in production: breaks when column added, transfers 10x more data than needed (BLOB column? hope you wanted it). Always explicit column list
• Embedding business logic in stored procedures: hard to test, hard to version-control, no type safety. Use Python + SQLAlchemy, keep DB as persistence only
🔀 Alternatives
• 9.4 SQLite (single-writer, file-based)
• MySQL (similar, less JSONB)
• CockroachDB (Postgres-compatible, distributed)
• Supabase/Neon (managed Postgres)
9.6 Redis Cache & Pub/Sub Data & APIs Intermediate ⏱ 4 hours

In-memory data store: cache, pub/sub, rate limiting, session, queue, leaderboard, geo. Sub-ms latency.

🎯 When to use: Hot data cache, session store, realtime pub/sub, rate limit counters, Celery broker, leaderboards (sorted set), distributed lock, geo radius. EN: anything that needs sub-ms latency + shared state across multiple app servers.
✅ Pros
• Sub-millisecond GET/SET (in-memory, single-threaded but I/O multiplexed) — vs Postgres 5-50ms = 10-100x speedup for hot data
• Atomic INCR = perfect for rate limiting (sub 9.10) and counters: INCR api:user:42:hits → no race condition across concurrent requests
• Sorted sets (ZADD + ZRANGEBYSCORE) = O(log N) leaderboard: 'top 10 users by score this week' in 0.5ms vs Postgres window function 200ms
• Pub/Sub: PUBLISH + SUBSCRIBE = realtime cross-process messaging without Kafka setup; great for WebSocket fanout (sub 9.7 scaling)
• TTL on every key: SET foo bar EX 300 → auto-expire after 5 min. No cron job to clean up; no memory leak from forgotten keys
• Persistence optional: AOF every-sec or RDB snapshot — survive restart. Or pure in-memory = lose on restart but max speed
⚠️ Cons
• Volatile by default: server crash + no persistence = total data loss. Mitigate with AOF + appendfsync=always (slower) or everysec (lose 1s)
• Single-threaded for commands: 1 huge SLOW command (KEYS *) blocks ALL other clients. Use SCAN cursor (non-blocking) for any iteration
• No SQL: data is key-value / hash / list / set / zset. Complex 'SELECT with 4 JOINs' = multiple HGETs in app code = round-trip cost
• Memory-bound: entire dataset must fit in RAM. 1M keys × 1KB = 1GB. Eviction policy (allkeys-lru) helps but cold data vanishes on pressure
• Pub/Sub is fire-and-forget: no replay, no acknowledgment, no queue. Subscribers offline = messages lost. Use Redis Streams (XADD/XREAD) for durability
• No client-side cursors: KEYS pattern blocks, SCAN cursor adds 5x round-trips vs Postgres' single query. For 'find all keys matching X' Redis is wrong tool
⚡ Gotchas
• Use SET with EX (seconds) or PX (ms), not separate EXPIRE: SET k v EX 300 = atomic. Two commands = race condition + key without TTL
• Pipeline (batched round-trip): pipe = redis.pipeline(); pipe.set('a', 1); pipe.get('b'); results = pipe.execute() — 1 round-trip for N commands = 10x throughput
• MSET for multi-key write: MSET k1 v1 k2 v2 = atomic + 1 round-trip. But MSET can't set per-key TTL, use Lua script for that
• Pipelining ≠ transaction: pipeline = 1 round-trip but no atomicity. MULTI/EXEC = atomic + 1 round-trip + commands run sequentially
• Eviction policy matters: default noeviction = OOM error on full. allkeys-lru = drop least-used (good for cache). volatile-lru = only drop keys with TTL (good for mixed)
• Lua scripts run atomically server-side: eval 'return redis.call("INCR", KEYS[1])' 1 mykey = race-free increment + custom logic. Use for any 'read-modify-write' pattern
• Connection pool: redis.Redis() uses 1 connection by default. For 100 concurrent web requests, use redis.asyncio.Redis with connection_pool=ConnectionPool(max_connections=50)
• Don't use FLUSHDB in production: deletes EVERYTHING. Use FLUSHDB on a test DB (DB 15 say) or a separate Redis instance
🚫 Anti-patterns
• Using Redis as primary DB without persistence: server restart = total data loss. Either enable AOF or treat as cache-only with warm-up on restart
• Storing 1MB+ values: 1 huge SET hogs the single thread, blocks all other clients. Keep values < 100KB; use S3/Postgres for large blobs
• Storing session data without TTL: 'session:abc' with no EX = leaks memory forever. Always SETEX or SESSION EXPIRE
• KEYS * in production: blocks Redis for seconds on million-key dataset. Always use SCAN cursor or maintain a separate index set
• Using LIST as a queue without consumer-group: BRPOP returns item to ONE client, no replay if consumer crashes mid-process. Use Streams (XADD/XREADGROUP) for durable queue
🔀 Alternatives
• Memcached (cache only, no pub/sub)
• RabbitMQ/Kafka (durable queue)
• Valkey (Redis fork, BSD)
• Upstash (serverless Redis)
9.7 🔄 WebSocket Data & APIs Advanced ⏱ 1 day

Bidirectional persistent connection for realtime: chat, live cursor, multiplayer, push notifications.

🎯 When to use: Realtime features (chat, multiplayer, live cursor, dashboard updates, push notifications), low-latency bidirectional, server-push needed. EN: anything realtime that needs server→client AND client→server on same connection.
✅ Pros
• Full-duplex over single TCP connection: no HTTP overhead per message (no headers, no auth re-check) = 100x lower per-message latency vs polling
• Server can push anytime: client doesn't have to ask. Perfect for chat, live price, multiplayer position sync, notifications
• Persistent connection = no reconnect/auth cost per message. Open once, send 10k messages for free (vs 10k HTTPS round-trips)
• Frame-based protocol: 1 TCP connection multiplexes many 'channels' (subprotocol pattern) — chat + notifications + presence on one socket
• Browser native API: new WebSocket('wss://...') — no library needed for client. Works through HTTP/2 + HTTP/3 (RFC 8441)
• Binary frames supported: send ArrayBuffer for game state, image delta, protobuf. 60% smaller than JSON text
⚠️ Cons
• Stateful: server must track open connections in memory. Restart server = 10k clients reconnect at once (thundering herd). Use exponential backoff on client
• Scaling pain: connection 1234 lives on server A. Request 1235 goes to server B. Need sticky session (load balancer hash on client IP) OR centralized pub/sub (Redis)
• No HTTP semantics: no status code per message, no headers, no auth middleware. Auth happens once on upgrade (Sec-WebSocket-Protocol or first message)
• Browser limits: 6 connections per origin in HTTP/1.1 (HTTP/2 lifts to 1000+). One tab with 1 socket = 5 sockets left for other things
• Proxy/load-balancer hostile: many CDNs/edges don't support WebSocket (Cloudflare does, AWS ALB needs config, Vercel = no). Plan deploy target before design
• Reconnect logic must be bulletproof: mobile networks drop constantly, browsers throttle background tabs. Need heartbeat ping every 30s + exponential reconnect
⚡ Gotchas
• Heartbeat ping/pong every 30s: without it, NAT/firewall silently drops the idle connection after 60s and client doesn't know. Send 'ping', expect 'pong' within 10s
• Use wss:// (TLS), not ws://, in production: cleartext WebSocket leaks auth tokens in plaintext, banned by all browsers in 2024+
• Auth on upgrade: pass JWT in Sec-WebSocket-Protocol header OR as ?token= query string. Validate BEFORE accept(). Accept-then-validate = unauth'd message allowed
• Sticky session needed for single-server deploy: nginx ip_hash; HAProxy cookie; AWS ALB sticky session. Without it, reconnect lands on different server = lost state
• Cross-server broadcast needs Redis pub/sub: PUBLISH 'room:42' message on server A, server B SUBSCRIBEs and forwards to its local sockets. Don't try direct connection between servers
• Frame size limit: default 1MB max payload. Sending 10MB image = PROTOCOL_ERROR + drop. Chunk large messages client-side, send length-prefix
• Clean close: send close code (1000 = normal, 1001 = going away, 4000-4999 = app-specific) + reason. Clients can branch on code (e.g. '1001 = reconnect with backoff')
• Reconnect storm: 10k clients all disconnect on server restart = 10k reconnects in 1s. Client must use exponential backoff (1s, 2s, 4s, 8s, max 30s) + jitter
🚫 Anti-patterns
• Sending every game tick: 60Hz position updates = 60 messages/sec/client × 1000 clients = 60k msg/sec. Throttle to 10Hz + delta compression
• Storing connection state in module global dict: works for 1 process, breaks for multi-worker / multi-server. Use Redis or DB
• Skipping auth on upgrade: any anonymous client can connect and spam. Validate JWT in connection handler before accept()
• Sending JSON with Date object: client gets string, not Date, may interpret wrong TZ. Use ISO 8601 string OR numeric epoch ms (preferred)
• No backpressure handling: client slow to receive = server send buffer fills up = 1 slow client blocks all others on same event loop. Per-socket bounded queue + drop oldest
🔀 Alternatives
• 9.8 SSE (one-way, simpler)
• Socket.IO (WebSocket + fallback to polling)
• Pusher/Ably (managed, 100k+ conn)
• WebRTC (peer-to-peer, no server)
9.8 📡 Server-Sent Events (SSE) Data & APIs Intermediate ⏱ 2 hours

One-way server→client HTTP stream for live feeds, progress bars, notifications. Simpler than WebSocket.

🎯 When to use: Server-to-client only stream: live news feed, build progress bar, AI token stream, stock price, log tail, notification push. EN: any one-way server→client stream over plain HTTP.
✅ Pros
• Plain HTTP: works through every proxy, CDN, load balancer, no upgrade handshake, no special config. wss:// vs ws:// headaches gone
• EventSource API in browser auto-reconnects on disconnect with Last-Event-ID — server can resume from where client left off (just return from log cursor)
• One-line server: yield 'data: foo\n\n' from a generator = stream. No socket state, no sticky session, scales horizontally with zero config
• Text-only is fine: 99% of use cases (progress, status, text tokens) are text. JSON in payload = same speed as binary for < 1KB
• Built-in event types: 'data:' for message, 'event: name' for typed events, 'id:' for resume, 'retry:' for reconnect interval. Simple but complete
• Serverless-friendly: Lambda response streaming + EventSource = realtime without long-running WebSocket server. Far cheaper
⚠️ Cons
• One-way only: client can't send back over the same connection. Need a separate POST endpoint for client→server. 2 connections instead of 1 for chat-like
• Text-only (UTF-8): binary data needs base64 = 33% bloat. Use WebSocket sub 9.7 if sending binary frames (game state, images)
• Browser limit: 6 concurrent SSE connections per origin in HTTP/1.1. HTTP/2 lifts this. Problem only if > 6 live feeds per page
• No binary framing: every message is full HTTP chunk (header + payload). High-frequency (> 100 msg/sec) = high overhead. WebSocket better for chat firehose
• Reconnect floods: server restart = all clients reconnect. EventSource retry hint helps, but no built-in exponential backoff (need to use polyfill or library)
• Proxy buffering: nginx by default buffers responses up to 1MB. SSE needs proxy_buffering off + chunked transfer-encoding or client sees nothing until 1MB
⚡ Gotchas
• Every event MUST end with \n\n (two newlines): 'data: hello\n\n' — one newline = client never sees it, this is the SSE framing rule
• Disable proxy buffering: nginx 'proxy_buffering off; proxy_cache off;', Cloudflare 'Cache-Control: no-cache' header, otherwise clients see nothing until buffer fills
• Use 'event: name' to type messages: client adds eventSource.addEventListener('progress', e => ...). Default 'message' event for 'data:' lines
• Send heartbeat every 15-30s: 'data: \n\n' (just a colon) keeps connection alive through NAT/firewall (they drop idle after 60s). Without it, clients see 'reconnecting' every minute
• Last-Event-ID header on reconnect: client sends the last 'id:' it received. Server can use it to resume: SELECT * FROM logs WHERE id > last_id. Cheap catch-up
• FastAPI StreamingResponse: don't await sleep — use async for + yield, OR sync generator with time.sleep. Mixing breaks the event loop
• CORS: 'Content-Type: text/event-stream' + 'Cache-Control: no-cache' + 'Connection: keep-alive' headers. EventSource in browser respects CORS like fetch
• Serverless: AWS Lambda response streaming (2023+) supports SSE but max 20-min connection. For longer, use Cloudflare Workers (5-min) or EC2/Container
🚫 Anti-patterns
• Sending JSON without 'data: ' prefix: EventSource client only fires on 'data: ' lines. JSON.stringify(obj) directly = client gets nothing
• Closing connection on error: client wants to reconnect, server should send 500 then close. Client tries reconnect per 'retry:' hint. Closing on first error = no recovery
• Sending huge payload: each message is a new HTTP chunk. 1MB JSON = 1MB per message = slow. Batch: send last 100 events as one chunk if late
• Forgetting to set Content-Type: 'text/event-stream' without it, browser EventSource refuses to connect. application/json = also rejected
• Polling for SSE support: old IE/old Edge don't support EventSource. Use a polyfill (event-source-polyfill) OR just WebSocket sub 9.7
🔀 Alternatives
• 9.7 WebSocket (bidirectional)
• Long polling (old, server holds HTTP open)
• Webhooks (client polls server, opposite direction)
9.9 🔑 OAuth 2.0 & Social Login Data & APIs Advanced ⏱ 3 days

Delegate auth to Google/GitHub/Facebook. PKCE for SPA, Authorization Code for backend, refresh tokens for long sessions.

🎯 When to use: Apps ที่ต้องการ user account โดยไม่เก็บ password; need profile data from Google/GitHub; B2B SSO (Google Workspace, Okta). EN: any app that needs user accounts; don't want to store passwords; need social profile data.
✅ Pros
• No password storage: zero liability for breach, no 'forgot password' flow to build, no email verification to code. Provider handles all of it
• Social profile = instant onboarding: 1 click → name, email, avatar, verified email. Conversion rate 2-5x vs signup form
• OAuth scopes: ask only for what you need (email, profile, calendar). User sees exactly what you can access, more trust
• Refresh token = long-lived session (30 days) without storing user's password. Short access token (1h) limits blast radius if leaked
• Standardized: every library (NextAuth, Authlib, Passport, requests-oauthlib) supports it. Same code works for Google, GitHub, Facebook, Microsoft
• PKCE (RFC 7636) makes authorization code flow safe for public clients (SPA, mobile) without client_secret — code_verifier in cookie, code_challenge in URL
⚠️ Cons
• Multiple flows: Authorization Code, Implicit (deprecated 2020), Client Credentials, Password Grant (deprecated). Pick wrong = security hole. Code + PKCE is the default for 2024+
• State parameter for CSRF: forget to validate state on callback = attacker pre-auths victim's browser. Always generate random state, store in session, validate on return
• Refresh token rotation: if attacker steals refresh token, you can't detect. Solution = rotate on every use (new refresh token issued + old invalidated), keep family-id for replay detection
• Scope creep: ask for 'profile + email + contacts + drive' = user sees scary consent screen = 50% drop-off. Minimum scope, ask incrementally (re-consent later)
• Provider downtime: Google OAuth down 1h/quarter (their SLA). Implement fallback: also support GitHub OR password login, so user has backup
• Logout complexity: revoking token at provider requires separate API call. Just dropping cookie locally = user still 'logged in' if they reopen in new tab
⚡ Gotchas
• Authorization Code with PKCE: client generates code_verifier (random 43-128 char), code_challenge = base64url(SHA256(verifier)). Send challenge in /authorize, verifier in /token exchange
• State parameter: random base64 token, store in HttpOnly cookie, validate on callback. Without it, CSRF attack can pre-auth attacker's account into victim's browser
• Nonce for ID token: required by OIDC, prevents token replay. Random value in /authorize, must match in ID token claim
• Token storage: NEVER localStorage (XSS-readable). Use HttpOnly + Secure + SameSite=Lax cookie. SPA reads from cookie, sends to /api/me, not from JS
• Token expiration: access token 1h, refresh token 30d. Server checks exp claim on every request. Refresh 5 min BEFORE expiry, not after (avoids race)
• Redirect URI whitelist: must match EXACTLY (scheme + host + port + path). 'http://localhost:3000/cb' ≠ 'http://localhost:3000/cb/' = auth fails. Add to provider console FIRST
• Client secret: NEVER in SPA bundle (visible in DevTools). SPA uses PKCE without secret. Backend uses secret. Mobile: secret in keystore, NOT in code
• Logout everywhere: revoke token via /revoke endpoint, clear cookie, optionally call provider's logout URL to log out from Google too. Otherwise 'logout' is a lie
🚫 Anti-patterns
• Implicit flow for SPA: deprecated by OAuth 2.1 draft (2020). Token in URL fragment = logged in referer headers, no PKCE, no refresh. Use Authorization Code + PKCE
• Storing access token in localStorage: XSS = game over. HttpOnly cookie ONLY, OR in-memory + refresh in HttpOnly cookie
• Skipping state parameter: 'works fine in dev' = CSRF vulnerability. Always generate, store, validate
• Asking for 'offline_access' scope without using refresh: 90% of apps never refresh, just re-login on expiry. Use 'prompt=consent' to get refresh only when needed
• Not validating ID token signature: just trusting payload = trivial forgery. Verify with provider's JWKS endpoint, check 'iss', 'aud', 'exp', 'nonce' claims
🔀 Alternatives
• Session cookie + bcrypt password (simpler, no provider)
• NextAuth/Auth0 (managed)
• Clerk/Supabase Auth (managed)
• JWT-only (stateless, harder to revoke)
9.10 🚦 Rate Limiting & Abuse Prevention Data & APIs Intermediate ⏱ 1 hour

Token bucket per IP/user/endpoint. Prevent abuse, brute force, scraper overload, runaway clients.

🎯 When to use: Public APIs, login endpoints (anti-bruteforce), signup (anti-spam), expensive endpoints (search, export), any endpoint where one client can hurt others. EN: any endpoint where abuse can affect cost, fairness, or security.
✅ Pros
• Prevents brute force: 5 attempts/min on /login = 7k attempts/day max vs unlimited = millions. Kills credential stuffing
• Fairness: stops one misbehaving client from starving all others. 100 req/s global limit divided = 10 req/s/user cap
• Cost control: protects DB / external API bill from runaway. Each /api/search = 100ms DB + external API call. Limit to 60/min/user = max 1/sec expensive work
• DDoS mitigation: per-IP limit drops a single attacker from 10k req/s to 60 req/s in 1 line. Doesn't stop L3/L4 DDoS (need CDN), but blocks L7 abuse
• Many algorithms: fixed window (simplest, bursts at boundary), sliding window (smoother), token bucket (allows burst then steady), leaky bucket (steady rate)
• Implementation trivial in FastAPI: slowapi library = decorator, 2 lines. Redis-backed = multi-server consistent
⚠️ Cons
• False positives: shared NAT (school, corporate VPN) = 1000 users behind 1 IP = 1 user spamming kills whole school. Need per-user-after-auth limit, not just per-IP
• Boundary bursts: fixed window allows 2x rate at boundary (5:59:59 → 6 reqs, 6:00:00 → 6 reqs = 12 in 1s). Use sliding window or token bucket to avoid
• State storage: limit counter must be in Redis (multi-server) — not in-memory (per-server). 1 line config, but a new dep
• Differentiating legit from abuser: simple rate limit blocks both. Need 'challenge' (CAPTCHA, JS proof-of-work) for grey zone, not just deny
• 429 response: clients must read Retry-After header and respect it. Naive client hammers at 1 Hz = 1% of limit used for retries. Servers should 'punish' by raising limit
• Cost of enforcement itself: 1 Redis INCR per request = 0.5ms. For 100k req/s that's 50k Redis ops/s. Acceptable; but every micro-optimization costs more than it saves
⚡ Gotchas
• Use 429 status code (Too Many Requests) + Retry-After header (seconds or HTTP date). Client respects it, user sees 'try again in 30s' not generic 500
• Per-IP works only as first line: identify user (JWT/API key) and use per-user limit when authenticated. Otherwise 1 authenticated client = unlimited, 1 IP = limited
• Different limits per endpoint: /login = 5/min/IP (brute force), /api/search = 60/min/user (cost), /api/projects = 300/min/IP (fair use). One global limit = wrong priorities
• Burst allowance: token bucket with burst=10 means client can do 10 quick then 1/s steady. Better UX for legit users (page load = 8 calls in 200ms)
• Redis INCR is atomic + O(1): but key naming matters. r:user:42:60 = per minute. Or r:user:42 (with TTL=60 set on first INCR). Or Lua script for read-modify-write
• Pre-auth vs post-auth: pre-auth (login, signup) = per-IP. Post-auth = per-user. After successful auth, check per-user (catches credential stuffing across IPs)
• SlowAPI (Python) or express-rate-limit (Node) or tollbooth (Go) — pick one. Hand-rolled is always wrong (forgot edge case: clock skew, race condition, fail-open vs fail-closed)
• Fail-open vs fail-closed: if Redis is down, allow all (fail-open, safer for UX) or deny all (fail-closed, safer for security). Pick based on endpoint: /login fail-CLOSED, /api/data fail-OPEN
🚫 Anti-patterns
• In-memory rate limit on multi-server deploy: server A has 60 reqs, server B has 60 reqs, total = 120. Use Redis-backed ALWAYS in production
• Per-second limits only: client can do 5 reqs/sec = 432k/day. Real abuse is sustained over minutes. Use per-minute OR per-hour limit, not per-second
• Returning 503 instead of 429: 503 = 'service unavailable, try again later' (server problem). 429 = 'you specifically, slow down' (client problem). Use 429 for rate limit
• Rate limiting on /health endpoint: legitimate health checks fire every 1s, will get blocked. EXCLUDE /health, /metrics, /docs from rate limiter
• No X-RateLimit-* headers: client has no way to know remaining quota, can't back off proactively. Return X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (epoch seconds)
🔀 Alternatives
• Cloudflare (L7 DDoS, free tier)
• API Gateway (AWS/Cloudflare)
• Envoy proxy (service-mesh level)
• fail2ban (SSH-style, not HTTP)
10.1 🏪 Tycoon Game Domain Recipes Intermediate ⏱ 1 week

Customer state machine (arrive→queue→order→wait→eat→pay) + patience decay + tip multiplier + reputation + upgrade economy.

🎯 When to use: ร้านอาหาร/โรงงาน/ธุรกิจจำลองที่ player ต้อง serve entities แบบ real-time; idle/incremental ที่มี production loop. Use เมื่อ core fun = wait → upgrade → wait less.
✅ Pros
• Customer state machine ทำให้ทุก customer มี 'life' ของตัวเอง — player เห็น urgency จาก patience bar ที่ลดลง
• Tip multiplier (1.0-2.0×) ตอบแทนความเร็ว + คุณภาพ — reward skillful play แทนการ grind อย่างเดียว
• Reputation 0-5★ เป็น meta-loop ที่กัน inflation — ราคาแพงขึ้นต้องคู่กับคุณภาพ ไม่งั้น customer หนี
• Upgrade tree (queue size, cook speed, tip bonus) ให้ player รู้สึก progress ทุก 2-3 นาที
• วัด KPI ได้ง่าย (served/hour, avg tip, complaint rate) — เหมาะกับ data-driven balancing
⚠️ Cons
• Balance tuning ใช้เวลา 2-3 สัปดาห์ — patience decay rate, spawn rate, upgrade cost ต้องวนปรับจนกว่า 'feels right'
• Late-game prestige ต้องคิดล่วงหน้า — เมื่อ player มีเงิน 1M+ ต้องมี sink (cosmetics/upgrades) ไม่งั้นเกมจบ
• Customer spawn ต้อง rate-limit ไม่งั้น frame rate ตก — ใช้ object pool แทน new/delete
• AI customer ทำนายไม่ได้ — random complaint ทำให้ player รู้สึก unfair ถ้าไม่มี visual cue (anger icon)
• Save/load ต้อง serialize pending customers + queue position + patience — ง่ายที่จะ bug ตรงนี้
⚡ Gotchas
• Patience decay rate ต้อง ≤ 1.5%/วินาที — เร็วกว่านี้ player กดเครียด, ช้ากว่านี้ไม่มี urgency
• Tip = base × (patience_remaining/100) × reputation_modifier — clamp [1.0, 2.0] ไม่งั้น whale player ได้ infinite money
• Spawn rate ต้อง Poisson distribution (avg 8s) ไม่ใช่ fixed interval — fixed = 'feels robotic'
• Reputation decay 0.05/นาทีเมื่อไม่ serve — ถ้าไม่ decay, late game ไม่มี tension
• Use object pool สำหรับ customer entities (50 ตัว max) — new/delete ทุก 8s = GC stutter
• Customer ต้องมี patience bar visible + color-coded (green→yellow→red) — text-only ไม่สื่อ
• Upgrade cost = base × 1.6^level (geometric) — linear cost ทำให้ late upgrade feel grindy
• Save state ต้องเก็บ queueIndex + patience snapshot ต่อ customer, ไม่ใช่แค่ money
🚫 Anti-patterns
• ทำ fixed upgrade path — player เบื่อเร็ว ต้องมี 2-3 branching specialization (speed vs quality vs capacity)
• ลืม in-game economy sink — เงินไม่มีที่ใช้ late game = grind ไม่จบ
• Spawn customer เร็วเกินจน overlap กัน visually — queue เละ, player สับสน
• ไม่มี fail state — tycoon ที่ไม่มี bankruptcy/chapter reset = ไม่มี risk = ไม่สนุก
• Patience bar แบบ linear — ต้องเป็น piecewise (0-30% เร็ว, 30-70% ช้า, 70-100% เร็วมาก) เพื่อ create urgency ช่วงท้าย
🔀 Alternatives
• 10.7 Restaurant Sim (food-specific)
• 10.4 Tower Defense (wave + currency, no patience)
• 10.2 City Builder (no per-entity state)
10.2 🏰 City Builder Domain Recipes Advanced ⏱ 2 weeks

Grid 14×10 + buildings + level + visual scale 1.15× + adjacency bonus + resource chain (wood→plank→house) + zoning.

🎯 When to use: เกมวางผังเมือง/ฐาน/อาณาจักรที่ player วาง tile บน grid แล้วตึก produce resource. Use เมื่อ fun = spatial puzzle + production chain optimization.
✅ Pros
• Grid (14×10) ทำให้ spatial puzzle ชัด — player เห็น adjacency ได้ทันที, optimize placement = core skill
• Visual scale 1.15× per level ทำให้ upgrade ที่ level 1→5 ใหญ่ขึ้น 2.0× — visual feedback ที่ 'ฟรี'
• Resource chain (wood→plank→house) สร้าง depth — player ต้อง plan 3-4 step ล่วงหน้า
• Zoning (residential/commercial/industrial) กัน bug ได้ทันที — แต่ละ zone มี adjacency rule ตายตัว
• Stronghold-3D proof: Babylon handles 80 buildings + 200 villagers ที่ 60fps บน mobile
⚠️ Cons
• Empty space = wasted potential — player รู้สึก guilty ถ้าเหลือ cell (ต้องมี 'filler' เช่น park/decoration)
• Adjacency bonus ต้องสมดุล — ใหญ่ไป = ต้องวางแน่น, เล็กไป = ไม่มีประโยชน์ ต้อง tune
• Building mesh swap ตอน upgrade ต้อง dispose mesh เก่า — ลืม = memory leak หลัง 30+ upgrades
• Zoning conflict เกิดง่าย (โรงงานติดบ้าน = -happiness) — ต้อง visual indicator (red glow) ทันที
• Save/load grid state ใหญ่ — 14×10=140 cells × 5 properties = 700 numbers ต่อ save
⚡ Gotchas
• Adjacency check = 4-neighbor (up/down/left/right) ไม่ใช่ 8 — 8-neighbor = bug เรื่อง diagonal confusion
• Upgrade cost = baseCost × (1.6^level) — geometric, ไม่ใช่ linear (linear = grind ไม่จบ)
• Production tick = every 5s, ไม่ใช่ every frame — frame-rate production = lag spike ตอน save
• Babylon mesh dispose: scene.removeMesh(mesh) + mesh.dispose() ไม่งั้น GPU memory เพิ่มเรื่อยๆ
• Zoning bonus: residential + park = +20% happiness, residential + industrial = -30% — encode ใน 2D table
• Resource chain: ต้อง visualize arrow (wood→plank) — ไม่งั้น player งงว่าทำไม plank ขาด
• Grid 14×10 = 140 cells; max 80 buildings = 60 empty ต้องคิด decoration (tree, fountain) ให้ player
• Camera Y lock ที่ 30° — 90° = top-down น่าเบื่อ, 0° = first-person ไม่เห็น grid
🚫 Anti-patterns
• ทำ free placement (no grid) แล้วหวังว่า player จะ align — เกิด overlap, bug, frustration
• Building ใหญ่ผิดสเกล — โรงงาน 3×3 ใกล้บ้าน 1×1 ดูแปลก ใช้ unified cell size 1.0
• ไม่มี undo/redo — city builder ที่ผิดพลาดแล้วลบไม่ได้ = rage quit ทันที
• Production loop เป็น instant — ต้องมี delay (5-30s) ไม่งั้น resource ไม่มี value
• ไม่มี win/lose condition — sandbox OK แต่ต้องมี milestone (population 100, gold 10k) ให้ player ตั้งเป้า
🔀 Alternatives
• 10.4 Tower Defense (path-based, no zoning)
• 10.1 Tycoon (no spatial grid)
• Mars-colony (3D voxel, harder)
10.3 ⚔️ RPG Pattern Domain Recipes Advanced ⏱ 1 week

NPC needs (hunger/energy/social/fun) + relationship 0-100 stages + dialogue tree + quest chain + idle behavior tree.

🎯 When to use: RPG/life-sim/dating-sim ที่ player มีปฏิสัมพันธ์กับ NPC ที่ 'มีชีวิต'. Use เมื่อ core = character ไม่ใช่ combat.
✅ Pros
• NPC need-decision (energy<20→sleep) ทำให้ NPC 'รู้สึกมีชีวิต' โดยไม่ต้อง animate อะไรเลย
• Relationship 0-100 with stages (stranger/friend/bestie/love) ให้ progress ที่วัดได้ + dialogue เปลี่ยนตาม stage
• Quest chain (intro→mid→epilogue) ทำให้ NPC มี arc — Sims-lite มี 12 NPCs แต่ละตัว 3 arcs
• Dialogue tree แบบ branch ให้ player agency — replay value จากการเลือกคำตอบต่างกัน
• Idle behavior tree (wander→work→eat→sleep) ทำให้ village ดู populated แม้ player ไม่อยู่
⚠️ Cons
• Authoring time สูง — 12 NPCs × 3 arcs × 5 dialogues = 180 scripts ใช้เวลา 3-4 weeks
• NPC AI loop tick ทุก 1s × 12 NPCs = 12 decisions/sec — scale เป็น 100 NPCs = frame drop
• Relationship decay เมื่อไม่คุย — player รู้สึกผูกพันน้อย ถ้าไม่ visualize (heart meter) จะลืม
• Dialogue tree branching ต้อง test ทุก permutation — bug ง่าย (typo, dead-end, soft-lock)
• Save/load NPC state ซับซ้อน — needs (4 floats) + relationship (12 ints) + quest flags (20 bools) ต่อ NPC
⚡ Gotchas
• Need decay rate: hunger +2/min, energy -1.5/min, social -0.8/min, fun -0.5/min — calibrate ให้ cycle 4-6 ชั่วโมงเกม
• Decision priority = critical > high > low — energy<20 = critical, hunger>80 = high, social<30 = low
• Relationship stage thresholds: 0=stranger, 25=friend, 50=close, 75=bestie, 100=love — encode ใน 1 helper function
• Dialogue: option visibility ต้อง check quest flags ไม่งั้น player เห็น option ที่ soft-lock
• Quest chain: ใช้ bitmask flags (1<<0=intro_done, 1<<1=mid_done) แทน array of bools ประหยัด memory
• Idle wander: ใช้ navmesh หรือ waypoint list ไม่ใช่ random — random = NPC เดินทะลุกำแพง
• Save NPC state ทุก 30s ไม่ใช่ทุก action — tick = 12 NPCs × 4 floats = 48 writes ต่อ action
• NPC ตาย/จากไป ต้องมี replacement — ไม่งั้น quest chain ขาด ตอน player reload
🚫 Anti-patterns
• NPC เป็นแค่ vendor (stand still, sell, repeat) — ไม่ใช่ RPG, เป็นแค่ storefront
• Relationship แบบ binary (0/1) — ไม่มี progress = ไม่มี reward loop
• Dialogue ยาวเกิน 5 บรรทัด — player กด skip, ไม่อ่าน, เสีย authoring effort
• Need decay เร็วเกินจน player ไม่ทันตอบ — Sims 'mwah' simulator ไม่ใช่ fun
• Quest chain linear ไม่มี choice — visual novel ที่มี combat เท่านั้น, ไม่ใช่ RPG
🔀 Alternatives
• 10.6 Fighting (no persistent NPCs)
• 10.10 Social (NPCs are other players)
• Visual novel (no behavior tree)
10.4 🗼 Tower Defense Domain Recipes Intermediate ⏱ 1 week

Path + waves + towers + A* pathfinding + range + DPS + targeting priority (first/last/closest/strongest) + economy.

🎯 When to use: เกมวางป้อม/ปืน/กับดักบน path เพื่อป้องกัน enemy เดินผ่าน. Use เมื่อ fun = positioning + upgrade timing + wave strategy.
✅ Pros
• A* pathfinding บน grid ให้ enemy เดิน optimal — visual debug ได้ (g/h/f overlay)
• Targeting priority 4 modes (first/last/closest/strongest) สร้าง build diversity — 'close' กับ boss แตกต่างกัน 100%
• Wave manager (timing + HP scaling) ทำ pacing ง่าย — 10 waves × 3 phases = 30 levels
• Tower upgrade tree 3×3 = 9 paths ให้ player เลือก — replay จากการ build ต่างกัน
• Economy (kill gold + interest) balance ง่าย — 1 hp = 1 gold baseline, scale 1.5×/wave
⚠️ Cons
• Late-game repetitive — wave 50+ เป็นแค่ HP x 100, ไม่มี mechanic ใหม่ ต้องเพิ่ม boss/mini-event
• A* recompute ตอน block path = expensive — precompute 2-3 alt paths แทน live recompute
• Path ที่ player block = soft-lock enemy เดินออกนอก grid — ต้อง fallback (cheapest path or die)
• Tower DPS math ซับซ้อน — fire rate × damage × splash × crit ต้อง table ในสเปรดชีต
• Tower stacking ทำให้ game trivial — max 2-3 of same type per level cap
⚡ Gotchas
• A* ใช้ Manhattan heuristic บน grid — Euclidean ช้ากว่า 2× และไม่จำเป็นบน 4-connect grid
• Path recompute: ใช้ incremental A* (block 1 cell = recompute suffix only) — full recompute 20ms
• Targeting priority: 'first' = enemy ที่ผ่าน path ไปได้ไกลสุด, 'last' = ใกล้ exit สุด, 'closest' = dist จริง
• Fire rate วัดเป็น shots/sec ไม่ใช่ ms — ms ทำให้ slow tower เลขเล็ก confusing
• Wave scaling: enemy.hp = baseHp × (1.4^waveNumber) — 1.4×/wave = ~3-4 min difficulty bump
• Interest on banked gold (1% per 10 gold, max 50) — passive saving mechanic
• Tower range visualized ตอน hover — ไม่ visualize = วางผิดบ่อย rage quit
• Sell tower refund = 70% — 100% = exploit (วาง/ลบวนไป), 50% = ผู้เล่นไม่ยอมลบทดลอง
🚫 Anti-patterns
• A* แบบ uniform cost บน grid ใหญ่ — ใช้ JPS (jump point search) ถ้า grid > 100×100
• Tower ที่ target เฉพาะ enemy แรกเสมอ — 'first' mode ใช้ได้แต่ต้องมี 3 mode อื่น
• Wave แบบ fixed interval (ทุก 30s) — ใช้ difficulty curve ตาม player gold/dps ratio
• Boss wave ที่ไม่มี visual cue (telegraph) — boss มาแบบเงียบ = 'unfair' feeling
• ให้ player ขาย tower ได้ 100% — exploit ในการเลือก optimal หลังเห็น wave
🔀 Alternatives
• 10.5 Platformer (no path AI)
• 10.1 Tycoon (no enemy AI)
• 10.2 City Builder (no enemy)
10.5 🦘 Platformer Domain Recipes Intermediate ⏱ 1 week

Gravity + variable jump height + coyote time + jump buffering + tile map collision + one-way platforms + moving platforms.

🎯 When to use: เกมกระโดด 2D (Mario/Celeste/Cuphead style). Use เมื่อ core = timing + reflex + level navigation.
✅ Pros
• Variable jump height (jump key hold = jump สูงขึ้น) ให้ 'feel' ที่ดี — Mario feel = 50% variable jump
• Coyote time 6 frames หลังพ้นขอบ = forgivable, player ไม่ rage quit
• Jump buffering 6 frames ก่อนถึงพื้น = input feel snappy, ไม่ต้อง frame-perfect
• Tile map (16×16) collision เร็วกว่า polygon — 200 tiles ใช้ 1ms
• One-way platforms (jump up only) ทำให้ level design ยืดหยุ่น — fall through = bug ต้อง guard
⚠️ Cons
• Physics tuning ใช้เวลานาน — gravity 800, jump -400, max-fall 600 ต้องลองผิดลองถูกจน feel right
• Wall jump / wall slide เพิ่ม complexity × 3 — collision normal vector, stick time, push-off
• Camera follow laggy ทำให้ motion sickness — lerp factor ต้อง 0.1-0.15 ไม่ใช่ 0.5
• One-way platform + moving platform = edge case hell — slope + 1-way = corner clip
• Animation transitions (idle/walk/jump/fall) ต้อง machine — 4 states × 8 frames = 32 sprites
⚡ Gotchas
• Variable jump: cut velocity y by 50% ตอน release jump key early — ไม่งั้น jump = on/off แข็ง
• Coyote time: 6 frames (100ms @60fps) หลังพ้น platform = player มี buffer
• Jump buffer: 6 frames (100ms) ก่อนแตะพื้น = press early ก็ขึ้นได้
• Gravity 800 px/s² + jump -400 = jump arc ~500ms — เร็วกว่านี้ feel snappy, ช้ากว่า feel floaty
• Max fall speed cap ที่ 600 px/s — ไม่ cap = tunneling through floor เมื่อ FPS drop
• Tile collision: separate X แล้ว Y — resolve Y ก่อนแล้ว X ไม่งั้น corner stuck
• One-way platform: check player.vy >= 0 ก่อน collision ไม่งั้น jump up = blocked
• Animation: transition idle↔walk = instant, walk→jump = interrupt (no blend), fall = looped
🚫 Anti-patterns
• Fixed jump height (jump key press = สูงเท่ากันทุกครั้ง) — feel arcade-y แต่ไม่ modern
• ไม่มี coyote time — player เดินตกขอบ = 'unfair' ทันที
• Square hitbox (player = rect 16×16 แน่น) — wide-pixel art ดูดี แต่ gameplay clunky ใช้ 12×14 hitbox
• Camera snap (no lerp) — เกม motion sickness, ไม่ smooth
• Apply gravity ใน fixed timestep ที่ไม่ consistent — tunneling + variable jump height bug
🔀 Alternatives
• 10.4 Tower Defense (no player physics)
• 10.6 Fighting (fixed 2D plane, no platform)
• 10.1 Tycoon (no player avatar)
10.6 🥊 Fighting Game Domain Recipes Advanced ⏱ 3 weeks

State machine (idle/walk/crouch/attack/hurt/block) + hitbox + frame data + cancel windows + block advantage + throw tech.

🎯 When to use: เกมต่อสู้ 1v1 (Street Fighter / Mortal Kombat / Smash). Use เมื่อ core = read opponent + punish + frame trap.
✅ Pros
• State machine 6-8 states = readable — แต่ละ state มี frame count ทำให้ prediction deterministic
• Cancel windows (2-4 frames) สร้าง combo depth — light→special ที่ frame 3 = 'combo' ที่ต้องจังหวะ
• Frame advantage (on-block, on-hit) ให้ risk/reward ที่ชัด — +3 on block = เริ่ม next move ได้ก่อน
• Hitbox visualization (toggle) ใน dev mode = essential สำหรับ balance team
• Throw tech (5-frame window) ตอบ anti-throw — counter-play ที่ design ได้
⚠️ Cons
• Balance ใช้เวลา 2-3 เดือน — character A vs B = 50/50 matchup ต้อง test 100+ games
• Frame data ต้อง consistent — input lag 16ms (1 frame) เปลี่ยน matchup ได้
• Animation cancel 1 frame off = 'macro' feel — bug ใน frame 6→7 cancel = exploit
• AI opponent ยาก — random AI = ชนะ 0%, scripted AI = predictable, behavior tree AI = 2 weeks
• Rollback netcode ต้อง GGPO (120+ ms latency) — local play ไม่มีปัญหา แต่ online = project ใหม่
⚡ Gotchas
• Frame data: light attack 5f startup / 8f active / 12f recovery = total 25f (416ms @60fps)
• Cancel window = 'special' button ได้ที่ frame 3-5 of recovery เท่านั้น — ก่อน/หลัง = no cancel
• Block advantage: +3f = opponent can act 3 frames later than you — frame trap ได้
• Hitbox: separate from hurtbox — 16×80 attack box (vertical slash) vs 24×56 hurtbox (body)
• Throw tech: กด throw ภายใน 5f หลังถูก throw = tech = both stagger, no damage
• Input buffer 4 frames — qcf+HP registered ถ้ากดใน 4f ของ forward+down — modern fighting standard
• Animation priority: hurt > block > attack — ถ้า hit ทั้งสอง side, hurt wins (no double-hit)
• Hitstop 6f on heavy hit — freeze 100ms ให้ impact feel + player อ่าน combo ได้
🚫 Anti-patterns
• Animation = collision — ถ้าใช้ sprite เป็น hitbox ทั้งตัว, combos ตายยาก ต้อง separate
• ไม่มี hitstun/blockstun — hit แล้วทุกคน recover เท่ากัน = no pressure = เกมน่าเบื่อ
• Input leniency 0 frame (frame-perfect) — qcf ต้อง exact down→down-forward→forward ไม่มี buffer
• AI แบบ reaction (ตอบทุก action ทันที) — AI เก่งเกินไป, player แพ้ตลอด rage quit
• ไม่มี training mode (dummy + frame data display) — competitive scene จะไม่เกิด
🔀 Alternatives
• 10.5 Platformer (no state machine)
• 10.4 Tower Defense (no PvP)
• 10.1 Tycoon (no combat)
10.7 🍜 Restaurant Sim Domain Recipes Intermediate ⏱ 1 week

Queue + 7 menu items + ingredient stock + cook time + tip multiplier + reputation + daily grade S/A/B/C + chef upgrade.

🎯 When to use: ร้านอาหาร/คาเฟ่/fast-food/fine-dining ที่ player serve customer + manage ingredient + build reputation. เน้นกว่า 10.1 (tycoon) ตรง menu depth + recipe unlock.
✅ Pros
• 7 menu items (Pad Thai, Tom Yum, Som Tum, Khao Soi, etc.) + recipe unlock tree ให้ progression 3-4 ชม.
• Ingredient stock (3-5 per dish) + supplier delivery สร้าง supply chain — ลืมสั่งของ = เมนูขาด
• Daily grade S/A/B/C (end-of-day summary) ให้ self-improvement loop — grade S = +10% tip tomorrow
• Chef upgrade (cook speed + quality + tip%) แตกต่างจาก restaurant layout upgrade — character growth
• Tip multiplier based on patience+food quality+atmosphere = 3 inputs → 1 output, satisfaction visible
⚠️ Cons
• Recipe balance 7 menu × 5 ingredients × 3 cook-times = 105 numbers ต้อง tune
• Ingredient stock race condition — 2 customers order same dish, 1 portion ขาด mid-cook
• Daily grade calc ต้อง fair — 1 complaint ใน 50 served ควร A ไม่ใช่ B (player rage)
• Tip overflow — late game ได้ 100k+ ต่อวัน ต้องมา sink (kitchen upgrade tier 5)
• Customer arrival wave (lunch 12:00, dinner 18:00) ต้อง ramp ไม่ใช่ step — step = frame stutter
⚡ Gotchas
• Recipe: { name, ingredients:{pork:1,noodle:1}, cookTime:8s, price:60, tipMult:1.0 } — store เป็น object
• Ingredient stock: deduct ตอน START cooking ไม่ใช่ตอน serve — late deduct = oversold
• Arrival wave: Poisson(λ=0.5) 12:00-13:00, λ=0.3 18:00-19:00, λ=0.1 otherwise
• Daily grade: S=0 complaints & ≥10 served, A=≤1 & ≥7, B=≤3 & ≥4, C=else
• Cook queue max 3 — overflow → customer leaves + complaint counter +1
• Tip = price × (patience/100) × chef.tipMod × reputation.modifier — clamp [0, 2×price]
• Menu unlock: tier 1 (3 items day 1), tier 2 (2 more day 3), tier 3 (2 more day 7)
• Save state: stock + reputation + chef.level + last 7 day grades — 7-10 ints/floats ต่อ save
🚫 Anti-patterns
• Menu 20+ items ตั้งแต่ day 1 — overwhelming, ไม่มี progression. ใช้ unlock tree 7 → 15
• Cook time 0 (instant) — ไม่มี tension, ไม่มี cook queue mechanic
• ไม่มี ingredient depletion — supply chain เป็นแค่ cosmetic, ไม่ใช่ mechanic
• Reputation ไม่มีผลต่อ spawn rate — 5★ vs 1★ ควร spawn ต่างกัน 2×
• Daily grade คำนวณจาก money only — ต้อง multi-factor: complaints + served + avg tip
🔀 Alternatives
• 10.1 Tycoon (no menu)
• 10.8 Medical Booking (no queue)
• 10.9 E-Commerce (no per-order state)
10.8 🏥 Medical Booking Domain Recipes Intermediate ⏱ 3 days

Doctor rotation + 12 สาขา + 10 แพทย์ + emergency slot 1669 + recurring appointment + no-show policy + queue priority.

🎯 When to use: คลินิก/รพ./salon/spa/แพทย์เฉพาะทาง ที่ patient book slot กับ doctor ที่มีเวลาจำกัด. Use เมื่อ core = schedule + doctor + slot conflict.
✅ Pros
• Doctor rotation (10 แพทย์ × 8 slots/day = 80 slots/day) — patient เลือกเฉพาะเจาะจง + admin balance load
• Emergency slot 1669 (always 1 reserve slot/doctor/day) — real-world inspired, urgent = jump queue
• Recurring appointment (ทุก 2 สัปดาห์ × 4 visits) auto-create — diabetes/HIV follow-up use case
• No-show policy (3 strikes → blacklist 30 วัน) — ลด fake booking, สำคัญใน real clinic
• 12 สาขา (สาขาในกรุงเทพ) — multi-location = ใหญ่ขึ้น, search by สาขา → doctor → slot
⚠️ Cons
• HIPAA-style privacy: encrypt at rest + audit log + 2FA — overhead 30-40% เทียบ regular booking
• Doctor schedule conflict — admin แก้ schedule = แจ้ง patient ที่จองแล้ว 50+ คน
• Timezone bug ถ้า multi-สาขา — กรุงเทพ vs เชียงใหม่ = 1 ชม. ต่าง, booking UTC shift
• Recurring skip: 'next Monday is holiday' = skip 1 + auto-pick next — edge case ซับซ้อน
• No-show counter fraud — patient สร้าง 3 email ปลอม + book 3 ครั้ง = exploit, ต้อง phone-verify
⚡ Gotchas
• Slot = { doctorId, start, duration, type:'normal'|'emergency'|'recurring' } — emergency slot = hidden จาก normal search
• Book: SELECT slot FOR UPDATE (SQL row lock) — ไม่งั้น 2 patient จอง slot เดียวกันพร้อมกัน
• Recurring: cron daily at 02:00 — generate next 30 days × 10 doctors = 300 slots — pre-cache
• No-show: cron every 10 min — check slot.start < now-15min && status='booked' → mark no-show + count++
• Time zone: store UTC in DB, render in สาขา local — ISO 8601 + Intl.DateTimeFormat with timeZone option
• Doctor schedule: separate work_hour (8:00-17:00) vs slot generation — admin แก้ work_hour กระทบ slot
• Reminder: push 24h + 1h ก่อน slot — push ทั้งสอง = no-show ลด 60% (medical research)
• Audit log: เก็บ every (patientId, action, slotId, ts) — 90-day retention ตาม HIPAA
🚫 Anti-patterns
• Slot = 1 hour fixed — doctor ที่ปรึกษา 15 นาที เสียเวลา 45 นาที, ต้อง flexible duration 15/30/60
• ไม่มี emergency slot — emergency ต้อง wait normal queue = patient เสียชีวิต, real-world liability
• Booking ไม่มี conflict check — 2 patient จอง slot เดียวกัน (race condition) = 2 angry patients
• ไม่เก็บ medical record — re-visit ต้องเริ่มใหม่, แพทย์โกรธ, patient โกรธ, เปลี่ยนคลินิก
• Recurring = manual repeat (admin ต้องสร้าง 10 visits เอง) — เปลืองเวลา 90%, auto-generate
🔀 Alternatives
• 10.9 E-Commerce (no time slot)
• 10.7 Restaurant Sim (no recurring)
• 10.1 Tycoon (no booking)
10.9 🛒 E-Commerce Domain Recipes Intermediate ⏱ 1 week

Product catalog + cart + checkout + Stripe payment + order + inventory race + shipping + 60-item POS (barcode scan) + tax/VAT.

🎯 When to use: ร้านค้า/marketplace/POS/grocery ที่ user browse + cart + pay. Use เมื่อ core = list/detail/cart/pay flow.
✅ Pros
• 60-item POS (grocery) — small enough barcode scan, big enough ให้รู้สึก real store
• Cart persist ใน localStorage — refresh = cart ยังอยู่, conversion เพิ่ม 25%
• Stripe Checkout (hosted) = PCI compliance ฟรี, ไม่ต้องเก็บ card เอง
• Inventory check atomic — SELECT FOR UPDATE หรือ version-number แก้ race condition
• Order confirmation email + receipt = professional UX, ฟรีด้วย SendGrid/Resend free tier
⚠️ Cons
• Stripe integration ใช้เวลา 3-5 วัน — webhook + idempotency key + currency + tax มี 10+ edge case
• Inventory race: 2 user buy last 1 item พร้อมกัน = oversell — ต้อง transactional DB หรือ lock
• Cart abandonment = 70% baseline — ต้อง email follow-up 1h/24h/72h เพื่อ recover 15-20%
• Refund flow = chargeback + accounting — refund API แยกจาก charge, accounting ต้อง sync
• VAT/tax calc — Thailand 7% VAT, EU OSS, US state tax — multi-region = tax engine (TaxJar)
⚡ Gotchas
• Stripe webhook: idempotency key = orderId — ป้องกัน double charge ตอน retry
• Inventory: deduct ใน same DB transaction as order — atomicity สำคัญที่สุด
• Cart: store in localStorage key 'cart_v1' with version, fallback v0 = array of {id,qty}
• Barcode scan: keyboard wedge input + 13-digit EAN-13 — debounce 50ms ป้องกัน multi-scan
• Order: status flow pending → paid → shipped → delivered — 5 states, webhook transitions
• Tax: Thailand 7% VAT ใน price หรือ add-on — most e-comm add-on, must show breakdown
• Stripe fee: 2.9% + 30¢ per charge — factor in price (THB 100 = รับจริง ~96.7)
• Empty cart UX — disable checkout button, show 'your cart is empty' message
🚫 Anti-patterns
• เก็บ card number เอง — ไม่ต้อง, ใช้ Stripe Elements (PCI ฟรี), เก็บเอง = liability
• ไม่มี idempotency — user double-click 'pay' = 2 charges — ต้อง unique idempotency key
• Cart ไม่ persist — refresh = cart หาย, conversion ลง 25%
• Inventory check หลัง charge — เรียงผิด, charge แล้ว refund = เสีย reputation
• ไม่มี order confirmation email — user ไม่แน่ใจว่าสั่งสำเร็จ, call support 10×
🔀 Alternatives
• 10.7 Restaurant Sim (in-game currency, not Stripe)
• 10.8 Medical Booking (no payment)
• 10.1 Tycoon (no real money)
10.10 👥 Social Network Domain Recipes Advanced ⏱ 2 weeks

Feed (paginated ts desc) + likes + comments + followers graph + post visibility + mention/hashtag + chat memory + community board.

🎯 When to use: ฟอรั่ม/community/social/feed-based app ที่ user post + follow + interact. Use เมื่อ core = feed + relationship + content.
✅ Pros
• Feed paginated by ts desc + limit 20 — query cheap, render 60fps ใน client
• Like = toggle ใน array ไม่ใช่ row ใหม่ — like counter O(1) read, write O(1)
• Follower graph (adjacency list) + mutual-friend suggestion — 'people you may know' จาก BFS depth 2
• Post visibility = public | followers | private — 3 modes, encode ใน 1 enum
• Chat memory (memo-ai) — context persist ใน session, AI จำ conversation ก่อนหน้าได้
⚠️ Cons
• Feed query ช้าเมื่อ user มี 10k posts — index (authorId, ts DESC) + cursor pagination แทน offset
• Moderation = human + AI — content moderation ใช้ OpenAI moderation API + report queue
• Comment tree = recursive — 100 comment ใน 1 post = nested render, ต้อง lazy load depth 3
• Real-time (new post notify) = WebSocket — 1000+ concurrent = scaling pain, Pusher/Ably ช่วย
• Privacy leak: like count public = 'soft' stalking — แก้โดย aggregate count only
• Notification spam — user โดน 100 mention ต่อวัน = mute แอป, rate limit mention/user/day
⚡ Gotchas
• Feed query: SELECT * FROM posts WHERE authorId IN (followeeIds) OR visibility='public' ORDER BY ts DESC LIMIT 20
• Like: posts[id].likes = array of userId — toggle แทน insert/delete, unique constraint ไม่ต้อง
• Comment: parent_comment_id NULL for top-level, set for reply — recursive CTE 1 query for tree
• Pagination: cursor = encodeURI(lastTs + ':' + lastId) — offset pagination skip bug ที่ N+1
• Mention: regex /@([\w]+)/g — validate user exists, ไม่งั้น notify user ที่ไม่มี = bug
• Hashtag: extract + lowercase, index in separate table, query posts JOIN post_tags
• Notification: per-user queue + mark-read — store ใน DB ไม่ใช่แค่ push (offline user)
• Chat memory (AI): store messages[] in session, send to AI as context — TTL 30 days then expire
🚫 Anti-patterns
• Pull full feed (no pagination) — 10k posts × 1MB = 10GB memory, ใช้ cursor pagination
• Like เป็นแถวใหม่ต่อ user เลย — millions of rows, count = SELECT COUNT(*) slow, ใช้ array
• Comment recursive render ทุก depth — 100 nested = 1 frame 6 seconds, lazy load depth > 2
• ไม่มี rate limit — user spam 1000 mention = DoS, rate limit 10 mentions/user/hour
• Public feed ทุกคนเห็น — privacy leak ในระบบที่มี personal data, ต้อง default 'followers only'
🔀 Alternatives
• 9.7 WebSocket (real-time push, not feed)
• 10.7 Restaurant Sim (no social)
• 10.8 Medical Booking (no social)
No sub-phases match your search.
Organized in 10 areas: Foundations, Design System, Game Engines, Game Patterns, Business Apps, Media Apps, AI Skills, Infrastructure, Data & APIs, Domain Recipes
Per-area pages: /skill/foundations/, /skill/game-engines/, etc. — full code snippets + CodeHub project examples.

🤖 AI-Readable JSON

All 100 sub-phases in JSON format — fetch from /api/skill (schema v3.0).

{
  "1": {
    "id": 1,
    "name": "Foundations",
    "icon": "📦",
    "slug": "foundations",
    "tagline": "Stack backbone — ต้องมีก่อนเริ่่มทุกโปรเชค",
    "sub_count": 10
  },
  "2": {
    "id": 2,
    "name": "Design System",
    "icon": "🎨",
    "slug": "design-system",
    "tagline": "Visual language for consistent UI",
    "sub_count": 10
  },
  "3": {
    "id": 3,
    "name": "Game Engines",
    "icon": "🎮",
    "slug": "game-engines",
    "tagline": "Pick the right engine, save 50%+ time",
    "sub_count": 10
  },
  "4": {
    "id": 4,
    "name": "Game Patterns",
    "icon": "🏛️",
    "slug": "game-patterns",
    "tagline": "Patterns that make games addictive",
    "sub_count": 10
  },
  "5": {
    "id": 5,
    "name": "Business Apps",
    "icon": "💼",
    "slug": "business-apps",
    "tagline": "Productivity + commerce",
    "sub_count": 10
  },
  "6": {
    "id": 6,
    "name": "Media Apps",
    "icon": "🎬",
    "slug": "media-apps",
    "tagline": "Audio + video + image",
    "sub_count": 10
  },
  "7": {
    "id": 7,
    "name": "AI Skills",
    "icon": "🤖",
    "slug": "ai-skills",
    "tagline": "OpenAI + embeddings + RAG",
    "sub_count": 10
  },
  "8": {
    "id": 8,
    "name": "Infrastructure",
    "icon": "🏗️",
    "slug": "infrastructure",
    "tagline": "Deploy + ops",
    "sub_count": 10
  },
  "9": {
    "id": 9,
    "name": "Data & APIs",
    "icon": "📊",
    "slug": "data-apis",
    "tagline": "Backend patterns",
    "sub_count": 10
  },
  "10": {
    "id": 10,
    "name": "Domain Recipes",
    "icon": "🎯",
    "slug": "domain-recipes",
    "tagline": "Copy patterns — reuse immediately",
    "sub_count": 10
  }
}
🔗 Open /api/skill 💾 Download skill.json (638KB)