Release Checklist
Security and configuration review before production deploys
Complete each section before promoting a build to production. This checklist prevents configuration errors and security oversights.
Pro Tip
Run pnpm validate
first to catch linting, type errors, and test failures. Then verify environment variables, security headers, and CSRF protection.
Environment and Configuration
Platform Environment Variables
Verify production values in deployment platform (Vercel, Netlify, AWS):
# Core URLs (must match deployed domains)
APP_BASE_URL=https://your-production-domain.com
DASHBOARD_BASE_URL=https://dashboard.your-domain.com
NEXT_PUBLIC_DASHBOARD_BASE_URL=https://dashboard.your-domain.com
ALLOWED_WEB_ORIGINS=https://your-domain.com,https://www.your-domain.com
# Security (production only)
ENABLE_HSTS=1
HSTS_PRELOAD=0 # Set to 1 after HSTS preload submission
TRUSTED_IP_HEADER=x-vercel-forwarded-for # Platform-specific
# Analytics (optional)
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
NEXT_PUBLIC_POSTHOG_KEY=phc_<your-key>
Preview Domains
Leave ENABLE_HSTS=0
and HSTS_PRELOAD=0
for preview deployments. HSTS headers break authentication on temporary domains.
Convex Environment Sync
Mirror credentials to Convex for BetterAuth and billing runtime:
# Verify current environment
npx convex env list --prod
# Set required variables
npx convex env set APP_BASE_URL https://your-domain.com --prod
npx convex env set DASHBOARD_BASE_URL https://dashboard.your-domain.com --prod
npx convex env set ALLOWED_WEB_ORIGINS https://your-domain.com,https://www.your-domain.com --prod
npx convex env set BETTER_AUTH_SECRET <same-as-platform> --prod
npx convex env set GOOGLE_CLIENT_ID <same-as-platform> --prod
npx convex env set GOOGLE_CLIENT_SECRET <same-as-platform> --prod
npx convex env set AUTUMN_SECRET_KEY <same-as-platform> --prod
Environment Parity
Mismatched secrets between platform and Convex cause authentication failures. Use a password manager to sync values across systems.
Security Headers and CSP
Verify Middleware Headers
Inspect headers produced by apps/marketing/middleware.ts
and apps/dashboard/middleware.ts
.
Development environment produces:
Content-Security-Policy-Report-Only
(logs violations without blocking)
Production environment produces:
Content-Security-Policy
(enforces policy)Content-Security-Policy-Report-Only
(monitors additional directives)Strict-Transport-Security
(whenENABLE_HSTS=1
)X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Header Inspection
Use browser DevTools → Network tab → Response Headers to verify middleware configuration.
Validate CSP Directives
Confirm these domains appear in Content-Security-Policy:
script-src 'self' https://app.posthog.com
connect-src 'self' https://*.convex.cloud https://app.posthog.com
img-src 'self' data: https:
frame-ancestors 'none'
Adjust directives in apps/marketing/middleware.ts
and apps/dashboard/middleware.ts
based on actual integrations.
Test CSP Reporting
Trigger a sample violation to verify reporting pipeline:
<!-- Add to any page temporarily -->
<script>eval('console.log("CSP test")')</script>
Verify /api/csp-report
logs the violation payload:
{
"blocked-uri": "eval",
"violated-directive": "script-src",
"source-file": "https://your-domain.com"
}
Remove test script after verification.
CSRF and CORS Protection
Verify Mutation Protection
Every Convex mutation route must call enforceMutationRequest
:
// packages/convex/src/http.ts
export const http = httpRouter();
http.route({
path: "/api/mutations/*",
method: "POST",
handler: httpAction(async (ctx, request) => {
await enforceMutationRequest(request); // Required
// ... mutation logic
}),
});
Grep for unprotected POST handlers:
grep -r "method: \"POST\"" packages/convex/src --include="*.ts" | \
grep -v enforceMutationRequest
Expected result: No matches (all POST routes protected).
Run CSRF Tests
Execute Convex CSRF test suite:
cd packages/convex
pnpm test __tests__/http.csrf.test.ts
Tests verify:
- POST requests from disallowed origins rejected (403)
- Requests without
Origin
header rejected (403) - Valid origin requests succeed (200)
- CORS preflight requests handled correctly
Test Failures
CSRF test failures indicate critical security vulnerabilities. Do not deploy until tests pass.
Row-Level Security
Verify User-Scoped Tables
Tables requiring automatic scoping must have by_user_id
index and appear in USER_SCOPED_TABLES
:
// packages/convex/schema.ts
defineTable({
userId: v.string(),
content: v.string(),
}).index("by_user_id", ["userId"])
// packages/convex/src/security.ts
export const USER_SCOPED_TABLES = [
"notes",
"documents",
"projects",
] as const;
Verify index coverage:
grep -r "USER_SCOPED_TABLES" packages/convex/src
Validate Ownership Assertions
Handlers reading single documents must call assertOwnerByUserId
:
// Example mutation
export const updateNote = mutation({
args: { id: v.id("notes"), content: v.string() },
handler: async (ctx, args) => {
const userId = await requireUser(ctx);
const note = await ctx.db.get(args.id);
assertOwnerByUserId(note, userId); // Required before mutation
await ctx.db.patch(args.id, { content: args.content });
},
});
Search for potentially unprotected mutations:
grep -r "db.patch\|db.replace" packages/convex/src --include="*.ts" -A 3 | \
grep -v assertOwnerByUserId
Sanitization and Redirects
Verify HTML Sanitization
User-generated HTML must render through <SanitizedHtml>
:
// apps/dashboard/components/user-content.tsx
import { SanitizedHtml } from "@workspace/security";
export function UserContent({ html }: { html: string }) {
return <SanitizedHtml html={html} />;
}
Search for dangerous patterns:
grep -r "dangerouslySetInnerHTML" apps/ --include="*.tsx"
Expected result: No matches (use <SanitizedHtml>
instead).
Validate Redirect Security
Redirect flows must call validateRedirect
:
// apps/dashboard/app/api/redirect/route.ts
import { validateRedirect } from "@workspace/security";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const target = searchParams.get("to");
validateRedirect(target); // Throws on open redirect
return redirect(target);
}
Search for unvalidated redirects:
grep -r "redirect(" apps/ --include="*.ts" --include="*.tsx" | \
grep -v validateRedirect
Validation Commands
Run complete validation suite from repository root:
# Comprehensive validation (CI equivalent)
pnpm validate
This runs:
- Linting (Ultracite)
- Type checking (TypeScript)
- Unit tests (Vitest)
- E2E smoke tests (Playwright)
# Linting
pnpm lint
# Type checking
pnpm typecheck
# Unit tests
pnpm test
# E2E smoke tests
pnpm test:e2e:smoke
# Full E2E suite (optional, ~5 minutes)
pnpm test:e2e
Zero Tolerance
All validation commands must exit with code 0. Fix errors before deploying to production.
Observability and Health Checks
Monitor CSP Violations
After deployment, browse the application and check logs for CSP violations:
# Platform logs (Vercel)
vercel logs <production-url> --follow
# Platform logs (Netlify)
netlify logs --site <site-id>
# Search for violations
grep "CSP VIOLATION" logs.txt
Expected result: Zero violations during normal browsing.
Configure Alerts
Set up monitoring for production issues:
// apps/dashboard/instrumentation.ts (Next.js 15)
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const Sentry = await import("@sentry/nextjs");
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1,
});
}
}
Monitor:
- Error rate spikes
- CSP violation patterns
- Authentication failure rates
- API response times
Post-Deployment Verification
Ready for Production
After completing all checklist items: Environment variables match across platform and Convex, security headers enforce in production (not report-only), CSRF protection active on all mutations, row-level security prevents unauthorized access, all validation commands pass locally, critical flows tested in production, and monitoring alerts configured.