Production Checklist

Complete go-live checklist covering environment variables, database, security, payments, and monitoring

Use this checklist to verify every system before launching your Kit application. Each section covers a critical area — environment configuration, database readiness, security hardening, authentication, payments, performance, SEO, and monitoring. Complete all items before directing real users to your production URL.

Environment Variables

Every external service needs production credentials. Using development or test keys in production causes silent failures, data loss, or security vulnerabilities.
1

Set production app URL

bash
NEXT_PUBLIC_APP_URL=https://yourdomain.com
This value is used for absolute URLs in emails, sitemap generation, Open Graph tags, and webhook callbacks. Getting this wrong causes broken links across the entire application.
2

Configure CORS origins

bash
ALLOWED_ORIGINS=https://yourdomain.com
3

Enable rate limiting

bash
UPSTASH_REDIS_REST_URL=https://your-redis.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token
Rate limiting is required for production. Without Upstash Redis, all API rate limiting is disabled and your endpoints are exposed to abuse. Kit uses category-based rate limiting (auth, API, AI, webhooks) with different limits per category.
4

Set cron secret

bash
CRON_SECRET=your-random-32-char-string
Generate with openssl rand -base64 32. Vercel cron jobs send this value in the Authorization header. Without it, the trial check and credit expiry endpoints reject all requests.
5

Verify all service keys are production keys

Confirm that every API key is from the production instance of each service, not development or test:
ServiceDevelopment Key PatternProduction Key Pattern
Clerkpk_test_... / sk_test_...pk_live_... / sk_live_...
Lemon SqueezyTest mode in dashboardLive mode in dashboard
SupabaseLocal or staging projectProduction project
ResendDevelopment domainVerified production domain
UpstashAny instanceProduction-tier instance
AI ProvidersAI_API_KEY=... or individual keysProduction keys from each provider dashboard
6

Configure AI provider keys

If your application uses AI features (LLM Chat, RAG Chat, Vision Chat), ensure at least one provider key is configured:
SetupVariables Needed
Simple (single provider)AI_PROVIDER + AI_API_KEY
Advanced (multi-provider fallback)Provider-specific keys: OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.
RAG featuresOPENAI_API_KEY is required for embedding generation, even if your chat provider is Anthropic/Google/xAI
Configure AI feature flags based on your needs:
FlagDefaultPurpose
NEXT_PUBLIC_AI_LLM_CHAT_ENABLEDtrueLLM Chat on /dashboard/chat-llm
NEXT_PUBLIC_AI_RAG_CHAT_ENABLEDtrueRAG Chat on /dashboard/chat-rag
NEXT_PUBLIC_AI_VISION_ENABLEDtrueImage analysis in LLM Chat (requires LLM Chat enabled)
NEXT_PUBLIC_AI_AUDIO_INPUT_ENABLEDtrueVoice input in LLM Chat (requires LLM Chat enabled)
NEXT_PUBLIC_AI_PDF_CHAT_ENABLEDtruePDF analysis in LLM Chat (requires LLM Chat enabled)
NEXT_PUBLIC_AI_IMAGE_GEN_ENABLEDtrueImage Generation on /dashboard/image-gen (requires OPENAI_API_KEY)
NEXT_PUBLIC_AI_CONTENT_GEN_ENABLEDtrueContent Generator on /dashboard/content
See AI Providers for the complete provider setup guide.

Database

1

Run migrations in production

bash
cd apps/boilerplate && npx prisma migrate deploy
This applies all pending migrations to your production database. Unlike prisma db push (which is for development), migrate deploy is safe for production — it only applies new migrations and never resets data.
2

Verify connection pooling

Kit uses two database connection strings:
VariablePurposeConnection Type
DATABASE_URLApplication queriesPooled (via PgBouncer, port 6543)
DIRECT_URLPrisma migrationsDirect (port 5432)
Supabase provides both URLs in the project settings. The pooled connection handles concurrent requests efficiently, while the direct connection is needed for schema migrations.
3

Verify pgvector extension

If you use RAG features (AI document chat), the pgvector extension must be enabled:
sql
CREATE EXTENSION IF NOT EXISTS vector;
Supabase enables this automatically, but verify it exists if you migrated from another provider.

Security

Kit ships with a comprehensive security layer that is active by default. Verify these settings are properly configured for your production environment.
1

Security headers (auto-configured)

The following headers are applied to all routes via next.config.mjs:
next.config.mjs — Production Security Headers
// Security Headers
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'X-DNS-Prefetch-Control',
            value: 'on',
          },
          {
            key: 'X-Frame-Options',
            value: 'SAMEORIGIN',
          },
          {
            key: 'X-Content-Type-Options',
            value: 'nosniff',
          },
          {
            key: 'X-XSS-Protection',
            value: '1; mode=block',
          },
          {
            key: 'Referrer-Policy',
            value: 'strict-origin-when-cross-origin',
          },
          {
            key: 'Permissions-Policy',
            value: 'camera=(), microphone=(self), geolocation=(), interest-cohort=(), payment=(), usb=()',
          },
          {
            key: 'Content-Security-Policy',
            value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim(),
          },
        ],
      },
    ]
  },
HeaderProtection
Content-Security-PolicyPrevents XSS by restricting script/style/image sources
X-Frame-OptionsPrevents clickjacking by blocking iframe embedding
X-Content-Type-OptionsPrevents MIME-type sniffing attacks
X-XSS-ProtectionLegacy XSS filter for older browsers
Referrer-PolicyControls information sent in the Referer header
Permissions-PolicyRestricts camera and geolocation; microphone allowed for same-origin audio input
These are applied automatically — no configuration needed. If you add new trusted script sources (analytics, chat widgets), update the TRUSTED_SCRIPT_SOURCES array in next.config.mjs.
2

CORS origins restricted

Verify ALLOWED_ORIGINS contains only your production domain. The security middleware rejects API requests from origins not in this list.
3

Rate limiting active

Verify Upstash Redis credentials are set. Test by checking the health endpoint:
bash
curl https://yourdomain.com/api/health
If rate limiting is active, rapid repeated requests return 429 Too Many Requests.
4

Plan API key rotation

Set reminders for regular key rotation:
CategoryRotation IntervalServices
CriticalEvery 90 daysClerk, Lemon Squeezy, Supabase service role
StandardEvery 180 daysResend, Upstash, Vercel Blob, AI providers
See the Security Overview for detailed rotation procedures.
5

Input sanitization (built-in)

Kit sanitizes all user input server-side against XSS and SQL injection. This is active by default in API routes and Server Actions — no configuration needed.

Authentication

1

Use Clerk production instance

Clerk separates development and production instances. Your production deployment must use the production instance:
  • Publishable key starts with pk_live_
  • Secret key starts with sk_live_
Using pk_test_ keys in production shows a development banner and limits functionality.
2

Configure webhook endpoint

Set the Clerk webhook URL to your production domain:
https://yourdomain.com/api/webhooks/clerk
Add the webhook secret as CLERK_WEBHOOK_SECRET in your Vercel environment variables. Kit uses Clerk webhooks to sync user data to the local database.
3

Enable social login providers

In the Clerk Dashboard, enable and configure at least:
  • Google — Covers most users
  • GitHub — Important for developer-focused SaaS
Each provider requires OAuth credentials from the respective platform. Clerk's dashboard guides you through the setup.
4

Test authentication flow

After deployment, verify the complete flow:
  1. Register a new account
  2. Log in with email and password
  3. Log in with a social provider
  4. Reset password via email
  5. Access a protected route (/dashboard)
  6. Log out and verify redirect

Payments

1

Switch to Lemon Squeezy production mode

In the Lemon Squeezy Dashboard, ensure your store is in live mode (not test mode). Test mode transactions do not process real payments.
2

Configure production webhook

Set the webhook URL in Lemon Squeezy Dashboard:
https://yourdomain.com/api/webhooks/lemonsqueezy
Verify the webhook secret matches LEMONSQUEEZY_WEBHOOK_SECRET in your Vercel environment.
3

Set all plan variant IDs

Every pricing plan needs its Lemon Squeezy variant ID configured:
bash
# Subscription-based pricing
NEXT_PUBLIC_LEMONSQUEEZY_BASIC_MONTHLY_VARIANT_ID=...
NEXT_PUBLIC_LEMONSQUEEZY_BASIC_YEARLY_VARIANT_ID=...
NEXT_PUBLIC_LEMONSQUEEZY_PRO_MONTHLY_VARIANT_ID=...
NEXT_PUBLIC_LEMONSQUEEZY_PRO_YEARLY_VARIANT_ID=...
NEXT_PUBLIC_LEMONSQUEEZY_ENTERPRISE_MONTHLY_VARIANT_ID=...
NEXT_PUBLIC_LEMONSQUEEZY_ENTERPRISE_YEARLY_VARIANT_ID=...

# Credit-based pricing (if using NEXT_PUBLIC_PRICING_MODEL=credit_based)
NEXT_PUBLIC_CREDIT_BASIC_MONTHLY_VARIANT_ID=...
NEXT_PUBLIC_CREDIT_PRO_MONTHLY_VARIANT_ID=...
NEXT_PUBLIC_CREDIT_ENTERPRISE_MONTHLY_VARIANT_ID=...

# Bonus credit packages (if NEXT_PUBLIC_BONUS_CREDITS_ENABLED=true)
NEXT_PUBLIC_BONUS_BASIC_PACKAGE1_VARIANT_ID=...
NEXT_PUBLIC_BONUS_BASIC_PACKAGE2_VARIANT_ID=...
NEXT_PUBLIC_BONUS_PRO_PACKAGE1_VARIANT_ID=...
NEXT_PUBLIC_BONUS_PRO_PACKAGE2_VARIANT_ID=...
NEXT_PUBLIC_BONUS_ENTERPRISE_PACKAGE1_VARIANT_ID=...
NEXT_PUBLIC_BONUS_ENTERPRISE_PACKAGE2_VARIANT_ID=...
Missing variant IDs cause the checkout button to fail silently. Bonus package variant IDs are only required when NEXT_PUBLIC_BONUS_CREDITS_ENABLED=true — see Lemon Squeezy Setup for creating the one-time purchase products.
4

Set customer portal URL

bash
NEXT_PUBLIC_LEMONSQUEEZY_CUSTOMER_PORTAL_URL=https://your-store.lemonsqueezy.com/billing
This URL lets customers manage subscriptions, update payment methods, and download invoices directly from Lemon Squeezy's hosted portal.
5

Test payment flow

Complete a real transaction on production:
  1. Visit the pricing page
  2. Click upgrade on a plan
  3. Complete checkout with a real payment method
  4. Verify the webhook processes the subscription
  5. Confirm the user's plan updates in the dashboard
  6. Test cancellation and refund through the customer portal

Performance

1

Analyze bundle size

Run the bundle analyzer to verify your production bundle meets Kit's performance targets:
bash
ANALYZE=true pnpm build:boilerplate
This generates HTML reports in apps/boilerplate/.next/analyze/ showing the exact size of every module.
MetricTarget
Initial JS load< 200 KB (gzipped)
Per-route chunks< 50 KB (gzipped)
Lighthouse Performance> 90
2

Verify image optimization

Kit auto-negotiates the best image format per browser:
next.config.mjs — Image Configuration
images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
        port: '',
        pathname: '/**',
      },
      {
        protocol: 'https',
        hostname: 'images.clerk.dev',
        port: '',
        pathname: '/**',
      },
      {
        protocol: 'https',
        hostname: 'img.clerk.com',
        port: '',
        pathname: '/**',
      },
      {
        protocol: 'https',
        hostname: '*.clerkstatic.com',
        port: '',
        pathname: '/**',
      },
      {
        protocol: 'https',
        hostname: 'lh3.googleusercontent.com',
        port: '',
        pathname: '/**',
      },
      {
        protocol: 'https',
        hostname: 'avatars.githubusercontent.com',
        port: '',
        pathname: '/**',
      },
    ],
    formats: ['image/avif', 'image/webp'],
  },
The formats: ['image/avif', 'image/webp'] setting automatically serves AVIF to browsers that support it (40-50% smaller than WebP) and falls back to WebP for others. No manual conversion needed — Next.js handles this at the edge.
3

Run Lighthouse audit

Run a Lighthouse audit against your production URL:
  1. Open Chrome DevTools > Lighthouse
  2. Select Performance, Accessibility, Best Practices, SEO
  3. Click Analyze page load
Target scores:
CategoryTarget
Performance> 90
Accessibility> 90
Best Practices> 90
SEO> 90

SEO

1

Verify robots.txt

Kit generates robots.txt dynamically, blocking sensitive routes from search engines:
src/app/robots.ts
import { MetadataRoute } from 'next'

/**
 * robots.txt Generator
 *
 * Blocks search engines from accessing protected routes:
 * - /api/* - API endpoints (internal)
 * - /dashboard/* - Authenticated app pages
 * - /(auth)/* - Authentication flow pages
 * - /admin/* - Admin pages
 * - /_next/* - Next.js internals
 * - /private/* - Private resources
 *
 * AI Crawlers: llms.txt available at /llms.txt per llmstxt.org standard.
 */
export default function robots(): MetadataRoute.Robots {
  const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://localhost:3000'

  return {
    rules: [
      {
        userAgent: '*',
        allow: '/',
        disallow: [
          '/api/',
          '/dashboard/',
          '/(dashboard)/',
          '/(auth)/',
          '/admin/',
          '/_next/',
          '/private/',
        ],
      },
    ],
    sitemap: `${baseUrl}/sitemap.xml`,
    host: baseUrl,
  }
}
Key rules:
  • Allows all public pages (/, /login, /register, /privacy, /terms, /imprint)
  • Blocks API routes (/api/), dashboard (/dashboard/), auth pages, admin, and internal Next.js routes
2

Verify sitemap

Kit generates sitemap.xml dynamically from your routes:
src/app/sitemap.ts
import { MetadataRoute } from 'next'

export default function sitemap(): MetadataRoute.Sitemap {
  const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://localhost:3000'

  // Static pages
  const staticPages = [
    '',
    '/login',
    '/register',
    '/privacy',
    '/terms',
    '/imprint',
  ].map((route) => ({
    url: `${baseUrl}${route}`,
    lastModified: new Date().toISOString(),
    changeFrequency: (route === '' ? 'daily' : 'weekly') as 'daily' | 'weekly',
    priority: route === '' ? 1 : 0.8,
  }))

  return [...staticPages]
}
The sitemap uses NEXT_PUBLIC_APP_URL for all URLs. Verify it returns your production domain by visiting https://yourdomain.com/sitemap.xml.
3

Verify llms.txt

Kit generates llms.txt following the llmstxt.org standard, providing AI crawlers (ChatGPT, Claude, Perplexity) with structured access to your site:
src/app/llms.txt/route.ts
/**
 * ============================================================================
 * llms.txt Route Handler
 * ============================================================================
 *
 * Generates a Markdown file following the llmstxt.org standard.
 * Provides AI crawlers (ChatGPT, Claude, Perplexity) with structured
 * access to your site's public content.
 *
 * CUSTOMIZATION AFTER PURCHASE:
 * 1. Update the description text to match your product
 * 2. Add your public pages/features to the "Pages" section
 * 3. Add a "## Docs" section if you have public documentation
 * 4. Add a "## API" section if you have a public API
 *
 * @see https://llmstxt.org
 * ============================================================================
 */

import { siteConfig } from '@/config/site'

export const dynamic = 'force-static'

export async function GET() {
  const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://localhost:3000'

  const lines: string[] = []

  // Title (H1) — per llmstxt.org spec
  lines.push(`# ${siteConfig.name}`)
  lines.push('')

  // Blockquote description
  lines.push(`> ${siteConfig.tagline} — ${siteConfig.description}`)
  lines.push('')

  // Detailed description
  lines.push(
    `${siteConfig.name} is a SaaS application built with Next.js.`
  )
  lines.push('')

  // Public pages
  lines.push('## Pages')
  lines.push('')
  lines.push(`- [Login](${baseUrl}/login): Sign in to your account`)
  lines.push(`- [Register](${baseUrl}/register): Create a new account`)
  lines.push(
    `- [Privacy Policy](${baseUrl}/privacy): Privacy policy and data handling`
  )
  lines.push(
    `- [Terms of Service](${baseUrl}/terms): Terms and conditions`
  )
  lines.push(
    `- [Legal Notice](${baseUrl}/imprint): Legal notice and company information`
  )
  lines.push('')

  const content = lines.join('\n')

  return new Response(content, {
    headers: {
      'Content-Type': 'text/plain; charset=utf-8',
      'Cache-Control':
        'public, s-maxage=86400, stale-while-revalidate=604800',
    },
  })
}
Verify it returns valid Markdown by visiting https://yourdomain.com/llms.txt. A .well-known/llms.txt redirect is also included for crawlers that check that path.
4

Verify custom error pages

Kit includes custom 404 and 500 error pages. Test them:
  • 404 — Visit https://yourdomain.com/nonexistent-page
  • 500 — Check that the error boundary renders a user-friendly message (test in development with a deliberate error)

Monitoring

1

Verify health endpoint

Kit includes a health check endpoint that monitors database connectivity and memory usage:
src/app/api/health/route.ts
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/db'

export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'

interface HealthCheck {
  status: 'healthy' | 'unhealthy'
  timestamp: string
  version: string
  environment: string
  checks: {
    database: 'healthy' | 'unhealthy' | 'unknown'
    memory: {
      used: number
      limit: number
      percentage: number
    }
  }
  error?: string
}

export async function GET() {
  const startTime = Date.now()

  try {
    // Check database connection
    let databaseStatus: 'healthy' | 'unhealthy' | 'unknown' = 'unknown'

    try {
      await prisma.$queryRaw`SELECT 1`
      databaseStatus = 'healthy'
    } catch (dbError) {
      console.error('Database health check failed:', dbError)
      databaseStatus = 'unhealthy'
    }

    // Memory usage
    const memoryUsage = process.memoryUsage()
    const memoryLimit = 512 * 1024 * 1024 // 512MB default for Vercel
    const memoryPercentage = (memoryUsage.heapUsed / memoryLimit) * 100

    const healthCheck: HealthCheck = {
      status: databaseStatus === 'healthy' ? 'healthy' : 'unhealthy',
      timestamp: new Date().toISOString(),
      version: process.env.NEXT_PUBLIC_APP_VERSION || '1.0.0',
      environment: process.env.NODE_ENV || 'development',
      checks: {
        database: databaseStatus,
        memory: {
          used: Math.round(memoryUsage.heapUsed / 1024 / 1024),
          limit: Math.round(memoryLimit / 1024 / 1024),
          percentage: Math.round(memoryPercentage),
        },
      },
    }

    const responseTime = Date.now() - startTime

    return NextResponse.json(
      {
        ...healthCheck,
        responseTime: `${responseTime}ms`,
      },
      {
        status: healthCheck.status === 'healthy' ? 200 : 503,
        headers: {
          'Cache-Control': 'no-cache, no-store, must-revalidate',
        },
      }
    )
  } catch (error) {
    console.error('Health check error:', error)

    return NextResponse.json(
      {
        status: 'unhealthy',
        timestamp: new Date().toISOString(),
        version: process.env.NEXT_PUBLIC_APP_VERSION || '1.0.0',
        environment: process.env.NODE_ENV || 'development',
        error: error instanceof Error ? error.message : 'Unknown error',
        responseTime: `${Date.now() - startTime}ms`,
      },
      {
        status: 503,
        headers: {
          'Cache-Control': 'no-cache, no-store, must-revalidate',
        },
      }
    )
  }
}
Test it after deployment:
bash
curl https://yourdomain.com/api/health
Expected response:
json
{
  "status": "healthy",
  "timestamp": "2026-01-15T10:30:00.000Z",
  "version": "1.0.0",
  "environment": "production",
  "checks": {
    "database": "healthy",
    "memory": {
      "used": 85,
      "limit": 512,
      "percentage": 17
    }
  },
  "responseTime": "45ms"
}
A 200 response means the database is connected and memory is within limits. A 503 response means the database check failed.
2

Verify cron jobs

Check the Vercel Dashboard under the Cron tab to confirm both scheduled jobs are registered:
JobScheduleNext Run
/api/cron/check-trials0 2 * * * (2 AM UTC)Shows next execution time
/api/cron/expire-bonus-credits0 3 * * * (3 AM UTC)Shows next execution time
You can manually trigger each cron job from the dashboard to verify they execute correctly.
3

Set up error tracking (recommended)

Kit does not include a specific error tracking service, but we recommend adding one for production visibility:
ServiceIntegration
Sentry@sentry/nextjs — Automatic error capture for client and server
Vercel AnalyticsBuilt-in — Enable in Vercel Dashboard
Vercel Speed InsightsBuilt-in — Real user performance monitoring
Sentry is the most comprehensive option. Add the @sentry/nextjs package and configure it with your DSN to capture unhandled errors, performance traces, and session replays.

Post-Launch

After your application is live, monitor these areas during the first 24-48 hours:
1

Verify deployment

bash
# Health check
curl https://yourdomain.com/api/health

# Verify HTTPS redirect
curl -I http://yourdomain.com
# Should return 301/308 redirect to https://
2

Test critical user flows

Walk through the complete user journey:
  1. Signup — Register a new account (email + social login)
  2. Login — Sign in and verify redirect to dashboard
  3. Payment — Complete a subscription purchase or credit purchase
  4. AI Chat — Send a message and verify streaming response. If Vision Chat is enabled, test image upload and analysis. If Audio Input is enabled, test voice recording and transcription (if AI enabled)
  5. Image Generation — Generate an image and verify it displays correctly (if Image Gen enabled)
  6. Content Generator — Generate content from a template and verify streaming output (if Content Gen enabled)
  7. File Upload — Upload a file and verify it persists (if storage enabled)
  8. Email — Trigger a transactional email and verify delivery
3

Monitor error rates

Watch for spikes in:
  • Vercel Functions — Check for 500 errors in the Functions tab
  • Webhook delivery — Verify Clerk and Lemon Squeezy webhooks are succeeding
  • Cron execution — Confirm the first scheduled run completes successfully
4

Set up uptime monitoring (recommended)

Point an uptime monitor at your health endpoint:
GET https://yourdomain.com/api/health
Expected: 200 OK
Interval: 5 minutes
Free options include UptimeRobot, Better Stack, or Checkly. Configure alerts for downtime so you are notified immediately if the database connection fails or the application crashes.

Complete Checklist

Use this summary to track your progress:
CategoryItemStatus
EnvironmentNEXT_PUBLIC_APP_URL set to production domain
EnvironmentALLOWED_ORIGINS configured (no wildcards)
EnvironmentUpstash Redis credentials set
EnvironmentCRON_SECRET configured
EnvironmentAll service keys are production keys
EnvironmentAI provider key(s) configured
EnvironmentAI feature flags set (LLM_CHAT_ENABLED, RAG_CHAT_ENABLED, VISION_ENABLED, AUDIO_INPUT_ENABLED, PDF_CHAT_ENABLED, IMAGE_GEN_ENABLED, CONTENT_GEN_ENABLED)
Databaseprisma migrate deploy run
DatabaseConnection pooling verified
SecurityCORS origins restricted
SecurityRate limiting active
SecurityKey rotation schedule planned
AuthClerk production instance active
AuthWebhook endpoint configured
AuthSocial login providers enabled
PaymentsLemon Squeezy in live mode
PaymentsWebhook URL set to production domain
PaymentsAll variant IDs configured
PaymentsCustomer portal URL set
PerformanceBundle size under targets
PerformanceLighthouse score > 90
SEOrobots.txt verified
SEOsitemap.xml verified
SEOllms.txt verified
MonitoringHealth endpoint returning 200
MonitoringCron jobs registered
MonitoringError tracking configured
Post-LaunchCritical flows tested
Post-LaunchUptime monitoring active