deployment-architect
cicdSafeAdvancedClaude
How to Install
Claude Code:git clone --depth 1 https://github.com/BFLabsAI/algoritmo-milionario.git && cp algoritmo-milionario/skills/deployment-architect ~/.claude/skills/deployment-architect -r
Claude Code (Windows):git clone --depth 1 https://github.com/BFLabsAI/algoritmo-milionario.git && xcopy algoritmo-milionario\skills\deployment-architect %USERPROFILE%\.claude\skills\deployment-architect\ /Y /E /I
Cursor:Open SKILL.md on GitHub → copy all content → paste into your project root .cursorrules file
---
name: deployment-architect
description: >
Complete deployment strategy skill covering platform selection, Cloudflare (Workers, Pages, KV, D1, R2, Wrangler),
Netlify (CLI, functions, edge functions, netlify.toml), reverse proxies (Caddy, Traefik), DNS configuration,
environment variable management, and VPS self-hosted deployments. Use when planning or executing any app deployment.
version: 1.0.0
tags:
- deployment
- cloudflare
- netlify
- caddy
- traefik
- dns
- reverse-proxy
- devops
- infrastructure
---
# Deployment Architect
Unified skill for planning and executing deployments across all major platforms. Start with the platform decision tree, then follow the platform-specific sections.
---
## Platform Decision: Which Platform for Which App
Use this decision tree before writing a single config file.
### Primary Question: Where Does Your Code Run?
```
What does your app need?
│
├─ Static files only (HTML/CSS/JS, no server logic)
│ ├─ Want Git-based deploys + global CDN → Cloudflare Pages or Netlify
│ └─ Need advanced CDN rules / WAF → Cloudflare Pages
│
├─ Serverless backend (API routes, SSR)
│ ├─ JavaScript/TypeScript only, edge-first, tight Cloudflare ecosystem → Cloudflare Workers
│ ├─ Node.js runtime, JAMstack-focused, forms/identity built-in → Netlify Functions
│ └─ Full-stack framework (Next.js, Nuxt, Astro, SvelteKit)
│ ├─ Want zero-config, open-source self-hostable → Vercel (separate skill)
│ ├─ Want Cloudflare edge + bindings (KV, D1, R2) → Cloudflare Pages Functions
│ └─ Want Netlify primitives (Blobs, edge functions) → Netlify
│
├─ Long-running processes / WebSockets / Docker containers
│ ├─ Want managed containers at the edge → Cloudflare Containers
│ └─ Want full control (VPS/bare metal)
│ ├─ Need automatic SSL + simple config → Caddy
│ └─ Need Kubernetes autodiscovery / dynamic scaling → Traefik
│
├─ AI / ML workloads
│ ├─ Inference at the edge without GPU server → Cloudflare Workers AI
│ └─ Need GPU cluster or fine-tuning → VPS / cloud GPU provider
│
└─ Database / Storage
├─ SQLite-compatible, serverless, global → Cloudflare D1
├─ S3-compatible object storage → Cloudflare R2
├─ Key-value, low-latency reads → Cloudflare KV
└─ Postgres/MySQL, managed → Neon / Supabase / PlanetScale
```
### Platform Comparison Matrix
| Criteria | Cloudflare Pages/Workers | Netlify | Caddy (VPS) | Traefik (K8s) |
|---|---|---|---|---|
| Static sites | Excellent | Excellent | Good | Good |
| Serverless functions | Workers (edge JS) | Node.js functions | N/A | N/A |
| SSR / full-stack | Pages Functions | Adapters | Any runtime | Any runtime |
| Database | D1, KV, R2 (native) | External only | External only | External only |
| AI bindings | Workers AI, Vectorize | No | No | No |
| Auto HTTPS | Yes (via Cloudflare) | Yes (Let's Encrypt) | Yes (Let's Encrypt) | Yes (ACME) |
| Free tier | Generous | Generous | Self-host cost | Self-host cost |
| Cold starts | None (always warm) | Sub-100ms | None | None |
| Max execution time | 30s (50ms CPU free) | 10s / 26s (Pro) | No limit | No limit |
| Vendor lock-in | High (runtime APIs) | Medium | None | None |
| Best for | Edge-first, global, AI | JAMstack, forms, identity | VPS single-server | K8s multi-service |
### When to Self-Host (Caddy/Traefik vs Managed Platform)
Self-host when:
- Long-running processes (websockets, video encoding, jobs > 30s)
- Docker containers are part of the stack
- You need direct filesystem access
- Cost at scale is prohibitive on managed platforms
- Compliance requires data in specific regions
- Running databases alongside app logic
Stay managed when:
- Team is small and ops burden matters
- Need global edge presence without infrastructure work
- Budget is unpredictable (pay-per-request)
---
## Cloudflare Platform
### Product Decision Trees
**Need to run code?**
```
├─ Serverless functions at the edge → Workers
├─ Full-stack web app with Git deploys → Pages
├─ Stateful coordination / real-time → Durable Objects
├─ Long-running multi-step jobs → Workflows
├─ Run containers → Containers
├─ Scheduled tasks (cron) → Cron Triggers
└─ Lightweight edge logic (modify HTTP) → Snippets
```
**Need to store data?**
```
├─ Key-value (config, sessions, cache) → KV
├─ Relational SQL → D1 (SQLite) or Hyperdrive (existing Postgres)
├─ Object / file storage (S3-compatible) → R2
├─ Message queue (async) → Queues
├─ Vector embeddings (AI search) → Vectorize
├─ Strongly consistent per-entity state → Durable Objects storage
└─ Persistent cache (long-term retention) → Cache Reserve
```
**Need AI/ML?**
```
├─ Run inference (LLMs, embeddings, images) → Workers AI
├─ Vector database for RAG / search → Vectorize
├─ Build stateful AI agents → Agents SDK
└─ Gateway for any AI provider (caching, routing) → AI Gateway
```
**Need security?**
```
├─ Web Application Firewall → WAF
├─ DDoS protection → DDoS Protection
├─ Bot detection → Bot Management
├─ API protection → API Shield
└─ CAPTCHA alternative → Turnstile
```
### Wrangler CLI
```bash
# Install
npm install wrangler --save-dev
# Auth
wrangler login
wrangler whoami
# Project lifecycle
wrangler init [name] # Create new project
wrangler dev # Local dev server (simulated)
wrangler dev --remote # Dev with real remote resources
wrangler deploy # Deploy to production
wrangler deploy --env staging # Deploy to named environment
wrangler versions list # List deployed versions
wrangler rollback [version-id] # Rollback
wrangler tail # Real-time logs
wrangler tail --status error # Filter error logs
```
**Resource management:**
```bash
# KV
wrangler kv namespace create NAME
wrangler kv key put "key" "value" --namespace-id=
# D1
wrangler d1 create NAME
wrangler d1 migrations create NAME "description"
wrangler d1 migrations apply NAME --remote
wrangler d1 execute NAME --remote --command="SELECT * FROM users"
wrangler d1 export NAME --remote --output=./backup.sql
# R2
wrangler r2 bucket create NAME
wrangler r2 object put BUCKET/key --file path
# Secrets
wrangler secret put NAME # Set encrypted secret
wrangler secret list
wrangler secret bulk FILE.json # Bulk upload from JSON
# Secrets Store (centralized, reusable)
wrangler secrets-store secret create --name SECRET_NAME --scopes workers --remote
# Pages
wrangler pages project create NAME
wrangler pages deployment create --project NAME --branch main
# Other resources
wrangler queues create NAME
wrangler vectorize create NAME --dimensions N --metric cosine
wrangler hyperdrive create NAME --connection-string "..."
wrangler workflows create NAME
```
### wrangler.jsonc / wrangler.toml Configuration
```jsonc
// wrangler.jsonc — preferred format
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2024-01-01",
// KV bindings
"kv_namespaces": [
{ "binding": "MY_KV", "id": "kv-namespace-id" }
],
// D1 database
"d1_databases": [
{ "binding": "DB", "database_name": "my-db", "database_id": "db-id" }
],
// R2 bucket
"r2_buckets": [
{ "binding": "MY_BUCKET", "bucket_name": "my-bucket" }
],
// Workers AI
"ai": { "binding": "AI" },
// Durable Objects
"durable_objects": {
"bindings": [{ "name": "MY_DO", "class_name": "MyDurableObject" }]
},
// Environment variables (not secret)
"vars": {
"API_URL": "https://api.example.com",
"NODE_ENV": "production"
},
// Multiple environments
"env": {
"staging": {
"vars": { "API_URL": "https://api-staging.example.com" },
"d1_databases": [
{ "binding": "DB", "database_name": "my-db-staging", "database_id": "staging-db-id" }
]
}
}
}
```
**Generate TypeScript types for bindings:**
```bash
npx wrangler types
```
**Access bindings in a Worker:**
```typescript
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise {
// KV
const value = await env.MY_KV.get('key');
await env.MY_KV.put('key', 'value', { expirationTtl: 3600 });
// D1
const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();
const { results } = await env.DB.prepare('SELECT * FROM users').all();
// D1 batch (atomic)
await env.DB.batch([
env.DB.prepare('INSERT INTO logs VALUES (?)').bind(Date.now()),
env.DB.prepare('UPDATE users SET last_seen = ? WHERE id = ?').bind(Date.now(), userId)
]);
// R2
const object = await env.MY_BUCKET.get('file.pdf');
await env.MY_BUCKET.put('file.pdf', fileData);
// Workers AI
const response = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
messages: [{ role: 'user', content: 'Hello' }]
});
// Environment variables and secrets (same access pattern)
const apiKey = env.API_KEY; // secret set via wrangler secret put
const apiUrl = env.API_URL; // var set in wrangler.jsonc
return new Response('OK');
}
};
```
### Cloudflare Pages
**Git-based deployment:**
- Connect repo in Cloudflare Dashboard → Pages → Create project
- Set build command and output directory per framework
- Automatic deploys on push; preview URLs for every PR
**Common framework build settings:**
| Framework | Build Command | Output Dir |
|---|---|---|
| React (Vite) | `npm run build` | `dist` |
| Next.js | `npm run build` | `.next` |
| Nuxt | `npm run build` | `.output/public` |
| Astro | `npm run build` | `dist` |
| SvelteKit | `npm run build` | `build` |
| Hugo | `hugo` | `public` |
**Pages Functions (full-stack):**
```
functions/
api/
hello.ts # → /api/hello
users/
[id].ts # → /api/users/:id (dynamic route)
_middleware.ts # Middleware for all routes
```
```typescript
// functions/api/users/[id].ts
export async function onRequest(context: EventContext>) {
const { params, env } = context;
const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(params.id).first();
return Response.json(user);
}
```
**Bindings for Pages Functions** — configure in Cloudflare Dashboard → Pages → Settings → Functions, or via wrangler.toml:
```toml
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "db-id"
```
### Cloudflare Workers — Routing
**Route matching (wrangler.jsonc):**
```jsonc
{
"routes": [
{ "pattern": "example.com/api/*", "zone_name": "example.com" },
{ "pattern": "*.example.com/*", "zone_name": "example.com" }
]
}
```
**Custom domain via dashboard:**
Dashboard → Workers → your worker → Triggers → Custom Domains
### D1 Database — Key Limits
| Limit | Free | Paid |
|---|---|---|
| Database size | 500 MB | 10 GB |
| Row size | 1 MB | 1 MB |
| Query timeout | 30s | 30s |
| Batch size | 1,000 statements | 10,000 statements |
| Time Travel | 7 days | 30 days |
Pricing: $0.001/million rows read + $1.00/million rows written + $0.75/GB storage/month.
### Workers AI — Common Models
```typescript
// Text generation
const result = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
messages: [{ role: 'user', content: prompt }]
});
// Embeddings
const embedding = await env.AI.run('@cf/baai/bge-base-en-v1.5', {
text: inputText
});
// Image classification
const classification = await env.AI.run('@cf/microsoft/resnet-50', {
image: imageData
});
```
### Cloudflare Environment Variable Scoping
- **Vars** (wrangler.jsonc `vars`) — committed to code, not encrypted, public config only
- **Secrets** (wrangler secret put) — encrypted at rest, never appear in code or logs
- **Per-environment** — configure separate values in `env.staging`, `env.production` sections
- **Pages env vars** — set in Dashboard → Pages → Site Settings → Environment Variables; scope to Production or Preview
---
## Netlify Platform
### Deployment Workflow
```bash
# Install CLI
npm install -g netlify-cli # global
# or
npm install netlify-cli -D # local (for CI use npx netlify)
# Auth
netlify login # Browser OAuth
netlify status # Check auth + linked site
# For CI: export NETLIFY_AUTH_TOKEN=your_token
# Site setup
netlify init # Create new site with Git CI/CD
netlify init --manual # Create without Git CI/CD
netlify link # Link to existing site (interactive)
netlify link --git-remote-url # Link by Git remote
# Deploy
netlify deploy # Draft/preview deploy
netlify deploy --prod # Production deploy
netlify deploy --dir=dist --prod # Specify output directory
netlify deploy --message="Fix bug" # Add deploy message
netlify build # Run build locally (mirrors Netlify env)
# Local dev
netlify dev # Wraps framework dev server + injects env + functions
netlify dev --port 3000
netlify dev --live # Live session sharing
netlify dev:exec # Run command with Netlify env loaded
# Manage
netlify open # Open site in Netlify dashboard
netlify rollback # Rollback to previous deploy
netlify logs # Stream function logs
netlify logs:function NAME
```
**For Vite-based projects**, use the Vite plugin instead of `netlify dev`:
```bash
npm install @netlify/vite-plugin
```
```typescript
// vite.config.ts
import netlify from "@netlify/vite-plugin";
export default defineConfig({ plugins: [netlify()] });
```
Then run `npm run dev` normally — provides Blobs, DB, Functions, env vars in dev.
**CLI flags:** `--json`, `--silent`, `--debug`, `--force`
**CI environment variables:**
```bash
netlify env:set API_KEY "value"
netlify env:set API_KEY "value" --secret # Hidden from logs
netlify env:set API_URL "https://prod.com" --context production
netlify env:set API_URL "https://staging.com" --context deploy-preview
netlify env:set DEBUG "true" --context branch:feature-x
netlify env:list
netlify env:list --plain > .env # Export
netlify env:import .env # Import from file
netlify env:unset API_KEY
netlify env:get API_KEY
```
**Functions CLI:**
```bash
netlify functions:list
netlify functions:invoke FUNCTION_NAME
netlify functions:invoke hello --payload '{"name":"World"}'
netlify functions:serve # Serve functions only
netlify functions:create NAME
```
### netlify.toml Reference
```toml
[build]
command = "npm run build"
publish = "dist" # Output directory
functions = "netlify/functions" # Functions directory
base = "packages/web" # Monorepo base dir (optional)
# Skip build if nothing changed in these paths
ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF packages/web"
[build.environment]
NODE_VERSION = "20"
NODE_OPTIONS = "--max-old-space-size=4096"
[functions]
node_bundler = "esbuild"
[dev]
command = "npm run dev"
port = 3000
targetPort = 5173
# Context-specific overrides
[context.production]
command = "npm run build:prod"
[context.production.environment]
NODE_ENV = "production"
API_URL = "https://api.example.com"
[context.deploy-preview]
command = "npm run build:preview"
[context.deploy-preview.environment]
API_URL = "https://api-staging.example.com"
[context.branch-deploy]
command = "npm run build"
# Named branch context
[context.staging]
command = "npm run build:staging"
[context.staging.environment]
API_URL = "https://api-staging.example.com"
# Redirects (processed top-to-bottom, first match wins)
[[redirects]]
from = "/old-path"
to = "/new-path"
status = 301
[[redirects]]
from = "/api/*"
to = "https://api.example.com/:splat"
status = 200
force = true
# SPA fallback — MUST be last
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
# Conditional redirects
[[redirects]]
from = "/"
to = "/uk"
status = 302
conditions = {Country = ["GB"]}
# Role-based redirects (Netlify Identity)
[[redirects]]
from = "/admin/*"
to = "/admin/dashboard"
status = 200
conditions = {Role = ["admin"]}
# Security headers
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"
X-XSS-Protection = "1; mode=block"
Referrer-Policy = "strict-origin-when-cross-origin"
Permissions-Policy = "geolocation=(), microphone=(), camera=()"
Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:"
# Long-term cache for hashed assets
[[headers]]
for = "/assets/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
# Build processing
[build.processing]
skip_processing = false
[build.processing.css]
bundle = true
minify = true
[build.processing.js]
bundle = true
minify = true
[build.processing.images]
compress = true
# Edge functions
[[edge_functions]]
function = "geolocation"
path = "/api/location"
# Build plugins
[[plugins]]
package = "@netlify/plugin-lighthouse"
[plugins.inputs]
output_path = "reports/lighthouse.html"
```
**SPA + API proxy pattern:**
```toml
[build]
command = "npm run build"
publish = "dist"
functions = "netlify/functions"
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
```
**Monorepo:**
```toml
[build]
base = "packages/web"
command = "npm run build"
publish = "dist"
ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF packages/web"
```
**`_redirects` file** (place in publish directory — alternative to netlify.toml):
```
/old-path /new-path 301
/api/* https://api.example.com/:splat 200
/* /index.html 200
```
### Netlify Functions
**Always use the modern default export + Config pattern.** Never use the legacy `exports.handler`.
```typescript
// netlify/functions/hello.mts
import type { Context, Config } from "@netlify/functions";
export default async (req: Request, context: Context): Promise => {
try {
if (req.method !== "POST") {
return new Response("Method Not Allowed", { status: 405 });
}
const body = await req.json();
return Response.json({ message: `Hello ${body.name}` });
} catch (error) {
console.error("Function error:", error);
return Response.json({ error: "Internal Server Error" }, { status: 500 });
}
};
export const config: Config = {
path: "/api/hello", // Custom path (replaces /.netlify/functions/hello)
method: ["GET", "POST"], // Restrict methods
rateLimit: {
windowSize: 60,
windowLimit: 100,
},
};
```
**File structure:**
```
netlify/functions/
_shared/ # Non-function shared code (underscore = not a function)
auth.ts
db.ts
items.ts # → /.netlify/functions/items (or custom path via config)
users/index.ts # → /.netlify/functions/users
```
Use `.ts` or `.mts` extensions. If both `.ts` and `.js` exist, `.js` wins.
**Path parameters:**
```typescript
// config: { path: "/api/items/:id" }
export default async (req: Request, context: Context) => {
const { id } = context.params;
const { name } = context.params; // multi-segment
return Response.json({ id });
};
```
**Multiple paths + method routing:**
```typescript
export const config: Config = {
path: ["/api/items", "/api/items/:id"],
};
export default async (req: Request, context: Context) => {
switch (req.method) {
case "GET": return handleGet(context.params.id);
case "POST": return handlePost(await req.json());
case "DELETE": return handleDelete(context.params.id);
default: return new Response("Method not allowed", { status: 405 });
}
};
```
**Background functions** (up to 15 minutes, client gets immediate 202):
```typescript
// MUST have -background suffix: netlify/functions/process-video-background.mts
import { getStore } from "@netlify/blobs";
export default async (req: Request, context: Context) => {
const { videoId } = await req.json();
const result = await processVideo(videoId); // long-running
const store = getStore("processed-videos");
await store.setJSON(videoId, result);
// Return value ignored
return new Response("Processing complete");
};
export const config: Config = { path: "/api/process-video" };
```
**Scheduled functions:**
```typescript
export default async (req: Request) => {
const { next_run } = await req.json();
console.log("Next invocation at:", next_run);
await performCleanup();
return new Response("Done");
};
export const config: Config = {
schedule: "@daily", // shortcuts: @hourly, @weekly, @monthly, @yearly
// schedule: "0 9 * * 1-5", // cron syntax (UTC)
};
```
**Scheduled function timeout: 30 seconds. Only runs on published deploys.**
**Streaming:**
```typescript
export default async (req: Request) => {
const stream = new ReadableStream({ /* ... */ });
return new Response(stream, {
headers: { "Content-Type": "text/event-stream" },
});
};
```
**Function resource limits:**
| Resource | Limit |
|---|---|
| Synchronous timeout | 60 seconds (26s on Pro for older runtimes) |
| Background timeout | 15 minutes |
| Scheduled timeout | 30 seconds |
| Memory | 1024 MB |
| Buffered payload | 6 MB |
| Streamed payload | 20 MB |
**Context object:**
```typescript
context.params // Path parameters
context.geo // { city, country: {code, name}, latitude, longitude, subdivision, timezone, postalCode }
context.ip // Client IP
context.cookies // .get(), .set(), .delete()
context.deploy // { context, id, published }
context.site // { id, name, url }
context.requestId // Unique request ID
context.waitUntil(p) // Extend execution after response
```
**Environment variables in functions:**
```typescript
// Preferred
const apiKey = Netlify.env.get("API_KEY");
// Also works
const dbUrl = process.env.DATABASE_URL;
```
**Client-side env var rules:**
- Vite: Only `VITE_`-prefixed vars via `import.meta.env.VITE_VAR`
- Astro: Only `PUBLIC_`-prefixed vars via `import.meta.env.PUBLIC_VAR`
- Never use `VITE_` or `PUBLIC_` prefixes for secrets
### Edge Functions (Deno runtime)
```typescript
// netlify/edge-functions/geo-redirect.ts
import type { Context } from "@netlify/edge-functions";
export default async (request: Request, context: Context) => {
const country = context.geo.country?.code || "US";
if (country === "DE") {
return Response.redirect(new URL("/de", request.url));
}
// Pass through to origin
return context.next();
};
export const config = {
path: "/*",
excludedPath: ["/api/*", "/_next/*"],
};
```
**Transform response:**
```typescript
export default async (request: Request, context: Context) => {
const response = await context.next();
response.headers.set("X-Custom-Header", "value");
const html = await response.text();
const modified = html.replace("