vercel-deployment — quality + safety report

In the Skillier index (antigravity__vercel-deployment) · scanned 2026-06-03 · engine: builtin+triage

A
Quality
92/100
Safety

✓ Clean — no heuristic safety flags surfaced.

Heuristic flags from the builtin scanner, which is known to over-flag (it trips on legitimate env-reading integrations, security skills, and library .eval calls). This is NOT an authoritative malicious verdict — re-scan with SkillSpector for the authoritative result. Run the authoritative scan →

Skillproof quality grade A

📇 This skill is in the Skillier index (curated · deduped · quality-filtered). Install Skillier to route & load it into your AI client.

Quality notes

Skill is large (~4361 tokens)
medium · quality · body
→ Tighten to the essential procedure; move long reference material to linked files.

About this skill

Expert knowledge for deploying to Vercel with Next.js

📄 Read the SKILL.md
---
name: vercel-deployment
description: Expert knowledge for deploying to Vercel with Next.js
risk: safe
source: vibeship-spawner-skills (Apache 2.0)
date_added: 2026-02-27
---

# Vercel Deployment

Expert knowledge for deploying to Vercel with Next.js

## Capabilities

- vercel
- deployment
- edge-functions
- serverless
- environment-variables

## Prerequisites

- Required skills: nextjs-app-router

## Patterns

### Environment Variables Setup

Properly configure environment variables for all environments

**When to use**: Setting up a new project on Vercel

// Three environments in Vercel:
// - Development (local)
// - Preview (PR deployments)
// - Production (main branch)

// In Vercel Dashboard:
// Settings → Environment Variables

// PUBLIC variables (exposed to browser)
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...

// PRIVATE variables (server only)
SUPABASE_SERVICE_ROLE_KEY=eyJ...  // Never NEXT_PUBLIC_!
DATABASE_URL=postgresql://...

// Per-environment values:
// Production: Real database, production API keys
// Preview: Staging database, test API keys
// Development: Local/dev values (also in .env.local)

// In code, check environment:
const isProduction = process.env.VERCEL_ENV === 'production'
const isPreview = process.env.VERCEL_ENV === 'preview'

### Edge vs Serverless Functions

Choose the right runtime for your API routes

**When to use**: Creating API routes or middleware

// EDGE RUNTIME - Fast cold starts, limited APIs
// Good for: Auth checks, redirects, simple transforms

// app/api/hello/route.ts
export const runtime = 'edge'

export async function GET() {
  return Response.json({ message: 'Hello from Edge!' })
}

// middleware.ts (always edge)
export function middleware(request: NextRequest) {
  // Fast auth checks here
}

// SERVERLESS (Node.js) - Full Node APIs, slower cold start
// Good for: Database queries, file operations, heavy computation

// app/api/users/route.ts
export const runtime = 'nodejs'  // Default, can omit

export async function GET() {
  const users = await db.query('SELECT * FROM users')
  return Response.json(users)
}

### Build Optimization

Optimize build for faster deployments and smaller bundles

**When to use**: Preparing for production deployment

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Minimize output
  output: 'standalone',  // For Docker/self-hosting

  // Image optimization
  images: {
    remotePatterns: [
      { hostname: 'your-cdn.com' },
    ],
  },

  // Bundle analyzer (dev only)
  // npm install @next/bundle-analyzer
  ...(process.env.ANALYZE === 'true' && {
    webpack: (config) => {
      const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
      config.plugins.push(new BundleAnalyzerPlugin())
      return config
    },
  }),
}

// Reduce serverless function size:
// - Use dynamic imports for heavy libs
// - Check bundle with: npx @next/bundle-analyzer

### Preview Deployment Workflow

Use preview deployments for PR reviews

**When to use**: Setting up team development workflow

// Every PR gets a unique preview URL automatically

// Protect preview deployments with password:
// Vercel Dashboard → Settings → Deployment Protection

// Use different env vars for preview:
// - PREVIEW: Use staging database
// - PRODUCTION: Use production database

// In code, detect preview:
if (process.env.VERCEL_ENV === 'preview') {
  // Show "Preview" banner
  // Use test payment processor
  // Disable analytics
}

// Comment preview URL on PR (automatic with Vercel GitHub integration)

### Custom Domain Setup

Configure custom domains with proper SSL

**When to use**: Going to production

// In Vercel Dashboard → Domains

// Add domains:
// - example.com (apex/root)
// - www.example.com (subdomain)

// DNS Configuration (at your registrar):
// Type: A, Name: @, Value: 76.76.21.21
// Type: CNAME, Name: www, Value: cname.vercel-dns.com

// Redirect www to apex (or vice versa):
// Vercel handles this automatically

// In next.config.js for redirects:
module.exports = {
  async redirects() {
    return [
      {
        source: '/old-page',
        destination: '/new-page',
        permanent: true,  // 308
      },
    ]
  },
}

## Sharp Edges

### NEXT_PUBLIC_ exposes secrets to the browser

Severity: CRITICAL

Situation: Using NEXT_PUBLIC_ prefix for sensitive API keys

Symptoms:
- Secrets visible in browser DevTools → Sources
- Security audit finds exposed keys
- Unexpected API access from unknown sources

Why this breaks:
Variables prefixed with NEXT_PUBLIC_ are inlined into the JavaScript
bundle at build time. Anyone can view them in browser DevTools.
This includes all your users and potential attackers.

Recommended fix:

Only use NEXT_PUBLIC_ for truly public values:

// SAFE to use NEXT_PUBLIC_
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...  // Anon key is designed to be public
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
NEXT_PUBLIC_GA_ID=G-XXXXXXX

// NEVER use NEXT_PUBLIC_
SUPABASE_SERVICE_ROLE_KEY=eyJ...     // Full database access!
STRIPE_SECRET_KEY=sk_live_...         // Can charge cards!
DATABASE_URL=postgresql://...          // Direct DB access!
JWT_SECRET=...                         // Can forge tokens!

// Access server-only vars in:
// - Server Components (app router)
// - API Routes
// - Server Actions ('use server')
// - getServerSideProps (pages router)

### Preview deployments using production database

Severity: HIGH

Situation: Not configuring separate environment variables for preview

Symptoms:
- Test data appearing in production
- Production data corrupted after PR merge
- Users seeing test accounts/content

Why this breaks:
Preview deployments run untested code. If they use production database,
a bug in a PR can corrupt production data. Also, testers might create
test data that shows up in production.

Recommended fix:

Set up separate databases for each environment:

// In Vercel Dashboard → Settings → Environment Variables

// Production (production env only):
DATABASE_URL=postgresql://prod-host/prod-db

// Preview (preview env only):
DATABASE_URL=postgresql://staging-host/staging-db

// Or use Vercel's branching databases:
// - Neon, PlanetScale, Supabase all support branch databases
// - Auto-create preview DB for each PR

// For Supabase, create a staging project:
// Production:
NEXT_PUBLIC_SUPABASE_URL=https://prod-xxx.supabase.co

// Preview:
NEXT_PUBLIC_SUPABASE_URL=https://staging-xxx.supabase.co

### Serverless function too large, slow cold starts

Severity: HIGH

Situation: API route or server component has slow initial load

Symptoms:
- First request takes 3-10+ seconds
- Subsequent requests are fast
- Function size limit exceeded error
- Deployment fails with size error

Why this breaks:
Vercel serverless functions have a 50MB limit (compressed).
Large functions mean slow cold starts (1-5+ seconds).
Heavy dependencies like puppeteer, sharp can cause this.

Recommended fix:

Reduce function size:

// 1. Use dynamic imports for heavy libs
export async function GET() {
  const sharp = await import('sharp')  // Only loads when needed
  // ...
}

// 2. Move heavy processing to edge or external service
export const runtime = 'edge'  // Much smaller, faster cold start

// 3. Check bundle size
// npx @next/bundle-analyzer
// Look for large dependencies

// 4. Use external services for heavy tasks
// - Image processing: Cloudinary, imgix
// - PDF generation: API service
// - Puppeteer: Browserless.io

// 5. Split into multiple functions
// /api/heavy-task/start - Queue the job
// /api/heavy-task/status - Check progress

### Edge runtime missing Node.js APIs

Severity: HIGH

Situation: Using Node.js APIs in edge runtime functions

Symptoms:
- X is not defined at runtime
- Cannot find module fs
- Works locally, fails deployed
- Middleware crashes

Why this breaks:
Edge runtime runs on V8, not Node.js. Many Node APIs are missing:
fs, path, crypto (partial), child_process, and most native modules.
Your code will fail at runtime with "X is not defined".

Recommended fix:

Check API compatibility before using edge:

// SUPPORTED in Edge:
// - fetch, Request, Response
// - crypto.subtle (Web Crypto)
// - TextEncoder, TextDecoder
// - URL, URLSearchParams
// - Headers, FormData
// - setTimeout, setInterval

// NOT SUPPORTED in Edge:
// - fs, path, os
// - Buffer (use Uint8Array)
// - crypto.createHash (use crypto.subtle)
// - Most npm packages with native deps

// If you need Node.js APIs:
export const runtime = 'nodejs'  // Use Node runtime instead

// For crypto hashing in edge:
// WRONG
import { createHash } from 'crypto'  // Fails in edge

// RIGHT
async function hash(message: string) {
  const encoder = new TextEncoder()
  const data = encoder.encode(message)
  const hashBuffer = await crypto.subtle.digest('SHA-256', data)
  return Array.from(new Uint8Array(hashBuffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('')
}

### Function timeout causes incomplete operations

Severity: MEDIUM

Situation: Long-running operations timing out

Symptoms:
- Task timed out after X seconds
- Incomplete database operations
- Partial file uploads
- Function killed mid-execution

Why this breaks:
Vercel has timeout limits:
- Hobby: 10 seconds
- Pro: 60 seconds (can increase to 300)
- Enterprise: 900 seconds

Operations exceeding this are killed mid-execution.

Recommended fix:

Handle long operations properly:

// 1. Return early, process async
export async function POST(request: Request) {
  const data = await request.json()

  // Queue for background processing
  await queue.add('process-data', data)

  // Return immediately
  return Response.json({ status: 'queued' })
}

// 2. Use streaming for long responses
export async function GET() {
  const stream = new ReadableStream({
    async start(controller) {
      for (const chunk of generateChunks()) {
        controller.enqueue(chunk)
        await sleep(100)  // Prevents timeout
      }
      controller.close()
    }
  })
  return new Response(stream)
}

// 3. Use external services for heavy processing
// - Trigger serverless function, return job ID
// - Process in background (Inngest, Trigger.dev)
// - Client polls for completion

// 4. Increase timeout (Pro plan)
// vercel.json:
{
  "functions": {
    "app/api/slow/route.ts": {
      "maxDuration": 60
    }
  }
}

### Environment variable missing at runtime but present at build

Severity: MEDIUM

Situation: Environment variable works in build but undefined at runtime

Symptoms:
- Env var is undefined in production
- Value doesn't change after updating in dashboard
- Works in dev, wrong value in production
- Requires redeploy to update value

Why this breaks:
Some env vars are only available at build time (hardcoded into bundle).
If you expect a runtime value but it was baked in at build, you get
the build-time value or undefined.

Recommended fix:

Understand when env vars are read:

// BUILD TIME (baked into bundle):
// - NEXT_PUBLIC_* variables
// - next.config.js
// - generateStaticParams
// - Static pages

// RUNTIME (read on each request):
// - Server Components (without cache)
// - API Routes
// - Server Actions
// - Middleware

// To force runtime reading:
export const dynamic = 'force-dynamic'

// For config that must be runtime:
// Don't use NEXT_PUBLIC_, read on server and pass to client

// Check which env vars you need:
// Build: URLs, public keys, feature flags (if static)
// Runtime: Secrets, database URLs, user-specific config

### CORS errors calling API routes from different domain

Severity: MEDIUM

Situation: Frontend on different domain can't call API routes

Symptoms:
- CORS policy error in browser console
- No Access-Control-Allow-Origin header
- Requests work in Postman but not browser
- Works same-origin, fails cross-origin

Why this breaks:
By default, browsers block cross-origin requests. Vercel doesn't
automatically add CORS hea

… (truncated)
Scan or optimize your own skill →

Want a live grade + an embeddable README badge? Run your skill through the free scanner.

Graded independently by Skillproof — nothing to sell the author. Quality is mechanical + corpus-grounded; safety flags are heuristic (builtin+triage), not a malicious verdict.