generated.
Phase 2: Database Orchestration & Schema Management
Database migrations represent the highest-risk component of any deployment. Schema changes must be applied before code that depends on them, and they must be backward-compatible with the previous application version.
# Development workflow (fast iteration)
npx drizzle-kit generate
npx drizzle-kit migrate
# Production workflow (auditable, reversible)
npx drizzle-kit generate --name add_user_preferences_table
npx drizzle-kit migrate --dir ./drizzle/migrations
Architecture Rationale: The generate command creates timestamped migration files that serve as an audit trail. The migrate command applies them sequentially. Direct schema synchronization tools (like push) bypass migration history, making rollbacks impossible and obscuring change tracking. In production, every migration must be backward-compatible: new columns should be nullable, new tables should not be immediately queried by existing code paths, and column renames should use a two-step migration (add new, copy data, drop old).
Production Tip: Never drop columns or rename tables in a single deployment. Use a deprecation window: deploy code that stops writing to the old column, run a migration to copy data, deploy code that reads from the new column, then remove the old column in a subsequent release.
Phase 3: Runtime Safeguards & Traffic Control
Production traffic requires defensive programming at the network and application layers. Feature flags, rate limiting, and health verification form the core of runtime resilience.
Feature Flag Resolution:
// src/lib/feature-flags.ts
import { get } from '@vercel/edge-config';
interface FlagConfig {
rolloutPercentage: number;
allowList: string[];
denyList: string[];
}
export async function evaluateFlag(flagKey: string, userId: string): Promise<boolean> {
const config = await get<FlagConfig>(flagKey);
if (!config) return false;
if (config.denyList.includes(userId)) return false;
if (config.allowList.includes(userId)) return true;
const hash = Math.abs(userId.split('').reduce((a, b) => a + b.charCodeAt(0), 0) % 100);
return hash < config.rolloutPercentage;
}
Rate Limiting on Authentication:
// src/app/api/v1/auth/verify/route.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const authLimiter = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(8, '10 m'),
prefix: 'auth:brute-force',
});
export async function POST(request: Request) {
const clientIp = request.headers.get('x-forwarded-for')?.split(',')[0] ?? 'unknown';
const { success, pending } = await authLimiter.limit(`ip:${clientIp}`);
if (!success) {
return new Response(JSON.stringify({ error: 'rate_limited' }), {
status: 429,
headers: { 'Retry-After': '600' },
});
}
// Proceed with credential verification
}
Architecture Rationale: Feature flags decouple deployment from release. Code ships disabled, allowing smoke testing in production without user exposure. Gradual percentage-based rollout isolates failures to a small cohort. Rate limiting prevents credential stuffing and brute-force attacks by enforcing sliding-window constraints at the edge. Both mechanisms operate independently of the deployment pipeline, enabling real-time traffic control.
Phase 4: External Service Integration & Observability
Third-party integrations require strict contract verification. Payment webhooks, in particular, must validate signatures and process payloads idempotently.
// src/lib/webhooks/stripe-handler.ts
import Stripe from 'stripe';
import { runtimeEnv } from '@/config/runtime-env';
const stripe = new Stripe(runtimeEnv.PAYMENT_GATEWAY_KEY);
export async function verifyAndParseWebhook(rawBody: string, signature: string) {
try {
return stripe.webhooks.constructEvent(
rawBody,
signature,
runtimeEnv.WEBHOOK_VERIFICATION_KEY
);
} catch (error) {
throw new Error('Invalid webhook signature');
}
}
export async function handlePaymentIntentSucceeded(event: Stripe.Event) {
const paymentIntent = event.data.object as Stripe.PaymentIntent;
// Idempotency check: verify transaction hasn't been processed
const exists = await checkTransactionRecord(paymentIntent.id);
if (exists) return { status: 'duplicate', handled: true };
// Process payment logic
await recordTransaction(paymentIntent);
return { status: 'processed', handled: true };
}
Observability Initialization:
// src/instrumentation/sentry.ts
import * as Sentry from '@sentry/nextjs';
export function initializeObservability() {
Sentry.init({
dsn: runtimeEnv.OBSERVABILITY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.15 : 1.0,
beforeSend: (event) => {
if (process.env.NODE_ENV === 'development') return null;
if (event.exception?.values?.[0]?.value?.includes('404')) return null;
return event;
},
});
}
Architecture Rationale: Webhook handlers must parse raw request bodies. JSON parsing alters byte sequences and invalidates cryptographic signatures. Idempotency prevents duplicate processing when payment providers retry deliveries. Observability initialization should sample traces strategically in production to balance insight with cost, while filtering known noise (like 404s) to prevent alert fatigue.
Pitfall Guide
1. Schema-Code Desynchronization
Explanation: Deploying application code before database migrations creates a dual-instance window where new code queries an outdated schema. This triggers runtime errors, data corruption, or silent failures.
Fix: Always run migrations in a pre-deploy CI step. Ensure migrations are backward-compatible so the previous application version continues functioning during the transition.
2. Hardcoded Secrets & Git Leaks
Explanation: Embedding API keys, database credentials, or webhook secrets directly in source code or configuration files exposes them to version control history, even if later removed.
Fix: Store all secrets in a dedicated environment manager. Reference them exclusively through validated environment loaders. Rotate credentials without code changes.
3. Webhook Payload Parsing Errors
Explanation: Using request.json() or automatic body parsing on webhook endpoints modifies the raw payload, breaking signature verification and causing authentication failures.
Fix: Always read the raw request body as text or buffer before passing it to the provider's verification utility. Disable automatic body parsing for webhook routes.
4. Ignoring the Dual-Instance Deployment Window
Explanation: Modern platforms spin up new instances before terminating old ones. Assuming atomic deployment leads to race conditions, cache inconsistencies, and schema mismatches.
Fix: Design deployments for concurrent execution. Use database-level locks for critical transitions, implement cache versioning, and ensure both old and new code paths function simultaneously during rollout.
5. Alert Fatigue & Silent Monitoring
Explanation: Routing all errors to a single dashboard without prioritization causes teams to ignore alerts. Critical failures get buried under noise, delaying response times.
Fix: Implement alert routing taxonomy. Route P0 incidents to paging systems, P1 to dedicated Slack channels, and informational events to daily digests. Filter known noise at the ingestion layer.
6. Feature Flag Sprawl Without Cleanup
Explanation: Accumulating unused feature flags increases code complexity, testing surface area, and cognitive load. Dead flags become technical debt that complicates future refactors.
Fix: Treat feature flags as temporary contracts. Set expiration dates, automate cleanup in subsequent releases, and remove flags immediately after full rollout.
7. Assuming Code Rollback Reverts Database State
Explanation: Rolling back application code does not reverse database migrations. The new schema remains active, causing the rolled-back code to fail when querying modified tables.
Fix: Design migrations to be reversible and backward-compatible. If a rollback is necessary, ensure the previous code version can operate against the new schema. Maintain a documented rollback procedure that includes schema verification.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High-traffic e-commerce checkout | Canary deployment with 5% flag rollout | Isolates payment failures to minimal user cohort | Low (infrastructure) / High (avoided revenue loss) |
| Internal admin dashboard | Direct deployment with post-deploy smoke test | Low user impact, internal traffic predictable | Minimal |
| Database schema expansion | Pre-deploy migration + backward-compatible code | Prevents dual-instance schema mismatches | Low (CI compute) |
| Third-party webhook integration | Signature verification + idempotent handler + raw body parsing | Prevents spoofing and duplicate processing | Low (development time) |
| Authentication endpoint | Sliding-window rate limit + IP tracking | Blocks credential stuffing without blocking legitimate users | Low (Redis/Edge compute) |
Configuration Template
# .github/workflows/release-pipeline.yml
name: Production Release Pipeline
on:
push:
branches: [main]
permissions:
contents: read
deployments: write
jobs:
validate-environment:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- run: npm ci
- name: Validate environment schema
run: npm run typecheck
env:
DATABASE_CONNECTION_STRING: ${{ secrets.DB_URL }}
AUTH_PROVIDER_SECRET: ${{ secrets.AUTH_SECRET }}
PAYMENT_GATEWAY_KEY: ${{ secrets.STRIPE_KEY }}
WEBHOOK_VERIFICATION_KEY: ${{ secrets.WEBHOOK_SECRET }}
OBSERVABILITY_DSN: ${{ secrets.SENTRY_DSN }}
run-migrations:
needs: validate-environment
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- name: Apply database migrations
run: npx drizzle-kit migrate --dir ./drizzle/migrations
env:
DATABASE_CONNECTION_STRING: ${{ secrets.DB_URL }}
deploy-application:
needs: run-migrations
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm run build
- name: Deploy to production
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
github-token: ${{ secrets.GITHUB_TOKEN }}
post-deploy-verification:
needs: deploy-application
runs-on: ubuntu-latest
steps:
- name: Health check verification
run: |
curl -f https://your-domain.com/api/health || exit 1
- name: Smoke test critical path
run: npm run test:smoke
env:
E2E_BASE_URL: https://your-domain.com
Quick Start Guide
- Initialize environment validation: Install
@t3-oss/env-core and zod. Create a centralized schema file that maps all required variables to Zod validators. Import this schema in your application entry point to fail fast on missing configuration.
- Configure migration ordering: Update your CI pipeline to run
drizzle-kit migrate in a dedicated job that executes before the deployment step. Commit migration files alongside the code that requires them. Verify backward-compatibility before merging.
- Implement runtime safeguards: Add a feature flag resolver using your platform's edge configuration or a dedicated flag service. Apply sliding-window rate limiting to authentication routes using Upstash Redis. Verify webhook signatures using raw request bodies before processing.
- Establish observability & alerting: Initialize Sentry with environment-aware sampling. Create a
/api/health endpoint that pings the database and returns a 200 or 503 status. Route P0 incidents to a paging system, P1 to a dedicated Slack channel, and filter known noise at ingestion. Execute a critical-path smoke test after every deployment.