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.
This checklist assumes you have already deployed to Vercel (see Vercel Deployment) and configured your CI/CD pipeline (see CI/CD Pipeline). The focus here is on production-specific configuration that goes beyond the initial deployment.
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
Never use
* for ALLOWED_ORIGINS in production. This disables CORS protection entirely. List only your production domain and any trusted subdomains.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:
| Service | Development Key Pattern | Production Key Pattern |
|---|---|---|
| Clerk | pk_test_... / sk_test_... | pk_live_... / sk_live_... |
| Lemon Squeezy | Test mode in dashboard | Live mode in dashboard |
| Supabase | Local or staging project | Production project |
| Resend | Development domain | Verified production domain |
| Upstash | Any instance | Production-tier instance |
| AI Providers | AI_API_KEY=... or individual keys | Production 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:
| Setup | Variables Needed |
|---|---|
| Simple (single provider) | AI_PROVIDER + AI_API_KEY |
| Advanced (multi-provider fallback) | Provider-specific keys: OPENAI_API_KEY, ANTHROPIC_API_KEY, etc. |
| RAG features | OPENAI_API_KEY is required for embedding generation, even if your chat provider is Anthropic/Google/xAI |
Configure AI feature flags based on your needs:
| Flag | Default | Purpose |
|---|---|---|
NEXT_PUBLIC_AI_LLM_CHAT_ENABLED | true | LLM Chat on /dashboard/chat-llm |
NEXT_PUBLIC_AI_RAG_CHAT_ENABLED | true | RAG Chat on /dashboard/chat-rag |
NEXT_PUBLIC_AI_VISION_ENABLED | true | Image analysis in LLM Chat (requires LLM Chat enabled) |
NEXT_PUBLIC_AI_AUDIO_INPUT_ENABLED | true | Voice input in LLM Chat (requires LLM Chat enabled) |
NEXT_PUBLIC_AI_PDF_CHAT_ENABLED | true | PDF analysis in LLM Chat (requires LLM Chat enabled) |
NEXT_PUBLIC_AI_IMAGE_GEN_ENABLED | true | Image Generation on /dashboard/image-gen (requires OPENAI_API_KEY) |
NEXT_PUBLIC_AI_CONTENT_GEN_ENABLED | true | Content 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.prisma db push can drop columns and tables to match the schema. Always use prisma migrate deploy for production databases.2
Verify connection pooling
Kit uses two database connection strings:
| Variable | Purpose | Connection Type |
|---|---|---|
DATABASE_URL | Application queries | Pooled (via PgBouncer, port 6543) |
DIRECT_URL | Prisma migrations | Direct (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(),
},
],
},
]
},
| Header | Protection |
|---|---|
| Content-Security-Policy | Prevents XSS by restricting script/style/image sources |
| X-Frame-Options | Prevents clickjacking by blocking iframe embedding |
| X-Content-Type-Options | Prevents MIME-type sniffing attacks |
| X-XSS-Protection | Legacy XSS filter for older browsers |
| Referrer-Policy | Controls information sent in the Referer header |
| Permissions-Policy | Restricts 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:
| Category | Rotation Interval | Services |
|---|---|---|
| Critical | Every 90 days | Clerk, Lemon Squeezy, Supabase service role |
| Standard | Every 180 days | Resend, 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:
- Register a new account
- Log in with email and password
- Log in with a social provider
- Reset password via email
- Access a protected route (
/dashboard) - 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:
- Visit the pricing page
- Click upgrade on a plan
- Complete checkout with a real payment method
- Verify the webhook processes the subscription
- Confirm the user's plan updates in the dashboard
- 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.| Metric | Target |
|---|---|
| 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:
- Open Chrome DevTools > Lighthouse
- Select Performance, Accessibility, Best Practices, SEO
- Click Analyze page load
Target scores:
| Category | Target |
|---|---|
| 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:
| Job | Schedule | Next Run |
|---|---|---|
/api/cron/check-trials | 0 2 * * * (2 AM UTC) | Shows next execution time |
/api/cron/expire-bonus-credits | 0 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:
| Service | Integration |
|---|---|
| Sentry | @sentry/nextjs — Automatic error capture for client and server |
| Vercel Analytics | Built-in — Enable in Vercel Dashboard |
| Vercel Speed Insights | Built-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:
- Signup — Register a new account (email + social login)
- Login — Sign in and verify redirect to dashboard
- Payment — Complete a subscription purchase or credit purchase
- 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)
- Image Generation — Generate an image and verify it displays correctly (if Image Gen enabled)
- Content Generator — Generate content from a template and verify streaming output (if Content Gen enabled)
- File Upload — Upload a file and verify it persists (if storage enabled)
- 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
Cron jobs run at 2 AM and 3 AM UTC. If you deploy in the afternoon, the first run happens overnight. Check the Vercel Cron tab the next morning to confirm success.
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:
| Category | Item | Status |
|---|---|---|
| Environment | NEXT_PUBLIC_APP_URL set to production domain | |
| Environment | ALLOWED_ORIGINS configured (no wildcards) | |
| Environment | Upstash Redis credentials set | |
| Environment | CRON_SECRET configured | |
| Environment | All service keys are production keys | |
| Environment | AI provider key(s) configured | |
| Environment | AI feature flags set (LLM_CHAT_ENABLED, RAG_CHAT_ENABLED, VISION_ENABLED, AUDIO_INPUT_ENABLED, PDF_CHAT_ENABLED, IMAGE_GEN_ENABLED, CONTENT_GEN_ENABLED) | |
| Database | prisma migrate deploy run | |
| Database | Connection pooling verified | |
| Security | CORS origins restricted | |
| Security | Rate limiting active | |
| Security | Key rotation schedule planned | |
| Auth | Clerk production instance active | |
| Auth | Webhook endpoint configured | |
| Auth | Social login providers enabled | |
| Payments | Lemon Squeezy in live mode | |
| Payments | Webhook URL set to production domain | |
| Payments | All variant IDs configured | |
| Payments | Customer portal URL set | |
| Performance | Bundle size under targets | |
| Performance | Lighthouse score > 90 | |
| SEO | robots.txt verified | |
| SEO | sitemap.xml verified | |
| SEO | llms.txt verified | |
| Monitoring | Health endpoint returning 200 | |
| Monitoring | Cron jobs registered | |
| Monitoring | Error tracking configured | |
| Post-Launch | Critical flows tested | |
| Post-Launch | Uptime monitoring active |