🚀 Deployment & DevOps
Deployment strategy should align with scale expectations and team capabilities from discovery
Modern Deployment Stack
Vercel
Best for Next.js apps with global edge deployment
Railway
Full-stack deployment with built-in databases
Cloud Run
Serverless containers for ultimate flexibility
Environment Configuration
1
Environment Setup
Environment Configuration
Set up environments based on discovery requirements
Copy
# .env.local
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_SUPABASE_URL=https://[project].supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=[anon-key]
SUPABASE_SERVICE_ROLE_KEY=[service-key]
# Feature flags
NEXT_PUBLIC_FEATURE_CHAT=true
NEXT_PUBLIC_FEATURE_ANALYTICS=false
# Development tools
NEXT_PUBLIC_DEBUG=true
NEXT_PUBLIC_API_MOCKING=true
2
Secret Management
Copy
// lib/secrets.ts
import { SecretManagerServiceClient } from '@google-cloud/secret-manager'
const client = new SecretManagerServiceClient()
export async function getSecret(name: string): Promise<string> {
if (process.env.NODE_ENV === 'development') {
return process.env[name] || ''
}
const [version] = await client.accessSecretVersion({
name: `projects/${process.env.GCP_PROJECT}/secrets/${name}/versions/latest`,
})
return version.payload?.data?.toString() || ''
}
// Usage
const apiKey = await getSecret('STRIPE_SECRET_KEY')
CI/CD Pipeline
- GitHub Actions
- GitLab CI
Complete CI/CD Pipeline
Automated testing, deployment, and monitoring
Copy
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
PNPM_VERSION: '8'
jobs:
# Dependency installation cache
install:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Cache dependencies
uses: actions/cache@v3
id: cache
with:
path: |
~/.pnpm-store
node_modules
key: deps-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: pnpm install --frozen-lockfile
# Code quality checks
quality:
needs: install
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- name: Lint
run: pnpm lint
- name: Type check
run: pnpm type-check
- name: Format check
run: pnpm format:check
- name: Security audit
run: pnpm audit --production
# Testing
test:
needs: install
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- name: Unit tests
run: pnpm test:unit --shard=${{ matrix.shard }}/4
- name: Integration tests
run: pnpm test:integration
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
# E2E Testing
e2e:
needs: [quality, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- name: Install Playwright
run: pnpm exec playwright install --with-deps
- name: Run E2E tests
run: pnpm test:e2e
env:
PLAYWRIGHT_TEST_BASE_URL: ${{ secrets.STAGING_URL }}
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
# Build
build:
needs: [quality, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- name: Build application
run: pnpm build
env:
NEXT_PUBLIC_ENV: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build-output
path: .next/
# Deploy to staging
deploy-staging:
needs: [build, e2e]
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com
steps:
- uses: actions/checkout@v4
- name: Deploy to Vercel Staging
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
alias-domains: pr-${{ github.event.pull_request.number }}.example.vercel.app
- name: Comment PR with preview URL
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '🚀 Preview deployed to https://pr-${{ github.event.pull_request.number }}.example.vercel.app'
})
# Deploy to production
deploy-production:
needs: [build, e2e]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- uses: actions/checkout@v4
- name: Deploy to Vercel Production
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'
- name: Purge CDN Cache
run: |
curl -X POST "https://api.cloudflare.com/client/v4/zones/${{ secrets.CF_ZONE }}/purge_cache" \
-H "Authorization: Bearer ${{ secrets.CF_TOKEN }}" \
-H "Content-Type: application/json" \
--data '{"purge_everything":true}'
- name: Run smoke tests
run: |
npm install -g newman
newman run postman-collection.json \
--environment production.json \
--bail
- name: Create release
uses: actions/create-release@v1
with:
tag_name: v${{ github.run_number }}
release_name: Release ${{ github.run_number }}
draft: false
prerelease: false
- name: Notify deployment
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: |
Production deployment ${{ job.status }}
Commit: ${{ github.sha }}
Author: ${{ github.actor }}
if: always()
Platform Configuration
- Vercel
- Railway
- Docker & K8s
Copy
// vercel.json
{
"framework": "nextjs",
"buildCommand": "pnpm build",
"devCommand": "pnpm dev",
"installCommand": "pnpm install",
"regions": ["iad1", "sfo1"], // Based on audience location
"functions": {
"app/api/*/route.ts": {
"maxDuration": 10
},
"app/api/heavy-task/route.ts": {
"maxDuration": 60
},
"app/api/background/*/route.ts": {
"maxDuration": 300
}
},
"crons": [
{
"path": "/api/cron/daily-report",
"schedule": "0 2 * * *"
},
{
"path": "/api/cron/cleanup",
"schedule": "0 */6 * * *"
}
],
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "X-XSS-Protection",
"value": "1; mode=block"
}
]
}
],
"redirects": [
{
"source": "/old-path",
"destination": "/new-path",
"permanent": true
}
],
"rewrites": [
{
"source": "/api/proxy/:path*",
"destination": "https://api.external.com/:path*"
}
]
}
Monitoring & Observability
1
Error Tracking
Copy
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_ENV,
// Performance monitoring
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
// Session replay
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
// From discovery requirements
beforeSend(event, hint) {
// Filter sensitive data
if (event.request?.cookies) {
delete event.request.cookies
}
if (event.user?.email) {
event.user.email = '[REDACTED]'
}
return event
},
integrations: [
new Sentry.BrowserTracing({
tracingOrigins: ['localhost', 'example.com', /^\//],
}),
new Sentry.Replay({
maskAllText: true,
blockAllMedia: true,
maskAllInputs: true,
}),
new Sentry.ProfilerIntegration(),
],
// Ignore certain errors
ignoreErrors: [
'ResizeObserver loop limit exceeded',
'Non-Error promise rejection captured',
],
})
2
Analytics & Metrics
Copy
// lib/analytics.ts
import mixpanel from 'mixpanel-browser'
import posthog from 'posthog-js'
import * as amplitude from '@amplitude/analytics-browser'
export class Analytics {
constructor() {
if (typeof window === 'undefined') return
// Initialize based on discovery KPIs
if (process.env.NEXT_PUBLIC_MIXPANEL_TOKEN) {
mixpanel.init(process.env.NEXT_PUBLIC_MIXPANEL_TOKEN, {
debug: process.env.NODE_ENV === 'development',
track_pageview: true,
persistence: 'localStorage',
})
}
if (process.env.NEXT_PUBLIC_POSTHOG_KEY) {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: 'https://app.posthog.com',
loaded: (posthog) => {
if (process.env.NODE_ENV === 'development') {
posthog.opt_out_capturing()
}
},
})
}
if (process.env.NEXT_PUBLIC_AMPLITUDE_KEY) {
amplitude.init(process.env.NEXT_PUBLIC_AMPLITUDE_KEY)
}
}
identify(userId: string, traits?: Record<string, any>) {
mixpanel?.identify(userId)
mixpanel?.people.set(traits)
posthog?.identify(userId, traits)
amplitude?.setUserId(userId)
}
track(event: string, properties?: Record<string, any>) {
// Track across all providers
mixpanel?.track(event, properties)
posthog?.capture(event, properties)
amplitude?.track(event, properties)
// Google Analytics 4
if (window.gtag) {
window.gtag('event', event, properties)
}
}
// Track KPIs from discovery
trackSignup(method: string) {
this.track('user_signup', {
method,
timestamp: new Date().toISOString(),
})
}
trackPurchase(amount: number, plan: string, currency = 'USD') {
this.track('purchase_completed', {
amount,
plan,
currency,
timestamp: new Date().toISOString(),
})
}
trackFeatureUsage(feature: string, metadata?: any) {
this.track('feature_used', {
feature,
...metadata,
})
}
trackError(error: Error, context?: any) {
this.track('error_occurred', {
error_message: error.message,
error_stack: error.stack,
...context,
})
}
}
export const analytics = new Analytics()
3
Performance Monitoring
Copy
// lib/performance.ts
export class PerformanceMonitor {
private metrics: Map<string, number[]> = new Map()
measurePageLoad() {
if (typeof window === 'undefined') return
// Core Web Vitals
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Log to analytics
analytics.track('web_vital', {
name: entry.name,
value: entry.startTime,
rating: this.getRating(entry.name, entry.startTime),
})
// Send to monitoring service
if (window.gtag) {
window.gtag('event', entry.name, {
value: Math.round(entry.startTime),
metric_rating: this.getRating(entry.name, entry.startTime),
})
}
}
})
observer.observe({ entryTypes: ['largest-contentful-paint'] })
// First Input Delay
new PerformanceObserver((list) => {
const firstInput = list.getEntries()[0]
const fid = firstInput.processingStart - firstInput.startTime
analytics.track('web_vital', {
name: 'FID',
value: fid,
rating: this.getRating('FID', fid),
})
}).observe({ type: 'first-input', buffered: true })
// Cumulative Layout Shift
let cls = 0
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
cls += entry.value
}
}
analytics.track('web_vital', {
name: 'CLS',
value: cls,
rating: this.getRating('CLS', cls),
})
}).observe({ type: 'layout-shift', buffered: true })
}
private getRating(metric: string, value: number): 'good' | 'needs-improvement' | 'poor' {
const thresholds = {
LCP: [2500, 4000],
FID: [100, 300],
CLS: [0.1, 0.25],
TTFB: [800, 1800],
}
const [good, poor] = thresholds[metric] || [0, 0]
if (value <= good) return 'good'
if (value <= poor) return 'needs-improvement'
return 'poor'
}
// API Performance
async measureAPICall<T>(
name: string,
fn: () => Promise<T>
): Promise<T> {
const start = performance.now()
try {
const result = await fn()
const duration = performance.now() - start
this.recordMetric(name, duration)
analytics.track('api_call', {
endpoint: name,
duration,
success: true,
})
return result
} catch (error) {
const duration = performance.now() - start
analytics.track('api_call', {
endpoint: name,
duration,
success: false,
error: error.message,
})
throw error
}
}
private recordMetric(name: string, value: number) {
if (!this.metrics.has(name)) {
this.metrics.set(name, [])
}
const values = this.metrics.get(name)!
values.push(value)
// Keep only last 100 measurements
if (values.length > 100) {
values.shift()
}
}
getMetricStats(name: string) {
const values = this.metrics.get(name) || []
if (values.length === 0) return null
const sorted = [...values].sort((a, b) => a - b)
return {
min: Math.min(...values),
max: Math.max(...values),
avg: values.reduce((a, b) => a + b, 0) / values.length,
p50: sorted[Math.floor(sorted.length * 0.5)],
p95: sorted[Math.floor(sorted.length * 0.95)],
p99: sorted[Math.floor(sorted.length * 0.99)],
}
}
}
export const performance = new PerformanceMonitor()
4
Health Checks
Copy
// app/api/health/route.ts
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { redis } from '@/lib/redis'
import { supabase } from '@/lib/supabase'
export async function GET() {
const checks = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: process.env.NODE_ENV,
version: process.env.npm_package_version,
checks: {
app: 'unknown',
database: 'unknown',
cache: 'unknown',
storage: 'unknown',
external: 'unknown',
},
metrics: {
memory: process.memoryUsage(),
cpu: process.cpuUsage(),
}
}
// Check application
checks.checks.app = 'healthy'
// Check database
try {
await prisma.$queryRaw`SELECT 1`
checks.checks.database = 'healthy'
} catch (error) {
checks.checks.database = 'unhealthy'
checks.status = 'degraded'
}
// Check cache
try {
await redis.ping()
checks.checks.cache = 'healthy'
} catch (error) {
checks.checks.cache = 'unhealthy'
checks.status = 'degraded'
}
// Check storage
try {
const { error } = await supabase.storage
.from('health-check')
.list('', { limit: 1 })
checks.checks.storage = error ? 'unhealthy' : 'healthy'
} catch (error) {
checks.checks.storage = 'unhealthy'
checks.status = 'degraded'
}
// Check external APIs
try {
const response = await fetch('https://api.example.com/health', {
signal: AbortSignal.timeout(5000)
})
checks.checks.external = response.ok ? 'healthy' : 'unhealthy'
} catch (error) {
checks.checks.external = 'unhealthy'
// Don't degrade status for external services
}
const statusCode = checks.status === 'healthy' ? 200 : 503
return NextResponse.json(checks, { status: statusCode })
}
// Readiness check
export async function HEAD() {
try {
await prisma.$queryRaw`SELECT 1`
return new Response(null, { status: 200 })
} catch {
return new Response(null, { status: 503 })
}
}
Infrastructure as Code
Copy
# terraform/main.tf
terraform {
required_providers {
vercel = {
source = "vercel/vercel"
version = "~> 0.4"
}
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
}
backend "s3" {
bucket = "terraform-state"
key = "production/terraform.tfstate"
region = "us-east-1"
}
}
# Vercel Project
resource "vercel_project" "main" {
name = var.project_name
framework = "nextjs"
git_repository = {
type = "github"
repo = var.github_repo
}
environment_variables = {
NEXT_PUBLIC_APP_URL = {
value = var.app_url
target = ["production"]
}
}
build_command = "pnpm build"
output_directory = ".next"
install_command = "pnpm install"
}
# Cloudflare DNS
resource "cloudflare_record" "main" {
zone_id = var.cloudflare_zone_id
name = "@"
value = vercel_project.main.alias[0].domain
type = "CNAME"
proxied = true
}
resource "cloudflare_record" "www" {
zone_id = var.cloudflare_zone_id
name = "www"
value = vercel_project.main.alias[0].domain
type = "CNAME"
proxied = true
}
# Cloudflare Page Rules
resource "cloudflare_page_rule" "cache" {
zone_id = var.cloudflare_zone_id
target = "${var.domain}/_next/static/*"
actions {
cache_level = "cache_everything"
edge_cache_ttl = 86400
}
}
Next Step
With deployment configured, proceed to agent collaboration strategies →

