StarterApp Docs
Deployment

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.

OAuth Callback URLs

Update OAuth providers with production domains:

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 (when ENABLE_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.

Verify Critical Flows

Test production endpoints:

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.