Documentation

ShipKit Docs

Code, deployment notes, and operational guides to take ShipKit from clone to production.

Getting Started

Get your SaaS running locally in minutes. ShipKit is a standard Next.js app with a broader product package around it: clone it, add your keys, then use the included setup and deployment guidance as you ship.

1. Clone & Install

bash
git clone <your-repo-url> my-saas
cd my-saas
npm install

2. Configure Environment

bash
cp .env.example .env.local

Open .env.local and fill in your service keys:

VariableServiceRequired
NEXT_PUBLIC_SUPABASE_URLSupabaseYes
NEXT_PUBLIC_SUPABASE_ANON_KEYSupabaseYes
SUPABASE_SERVICE_ROLE_KEYSupabaseYes
DATABASE_URLSupabase (Postgres)Yes
LEMONSQUEEZY_API_KEYLemonSqueezyYes
LEMONSQUEEZY_WEBHOOK_SECRETLemonSqueezyYes
STRIPE_API_KEYStripeOptional
STRIPE_WEBHOOK_SECRETStripeOptional
STRIPE_PRICE_BASIC_MONTHLYStripeOptional
STRIPE_PRICE_PRO_MONTHLYStripeOptional
EMAIL_PROVIDEREmailOptional
EMAIL_FROMEmailOptional
REDIS_URLUpstash / RedisOptional
ANTHROPIC_API_KEYAnthropic (Claude)Optional
AWS_ACCESS_KEY_IDAWS SESOptional
RESEND_API_KEYResendOptional
MAILGUN_API_KEYMailgunOptional
MAILGUN_DOMAINMailgunOptional
For a first launch, you mainly need Supabase and one payment processor: LemonSqueezy or Stripe. Redis, email providers like SES, Resend, or Mailgun, and AI keys are optional, and the broader product docs cover how to add those production concerns later.

3. Run the Dev Server

bash
npm run dev

Open http://localhost:3000 — your SaaS is live locally.

Project Structure

text
my-saas/
├── app/                  # Next.js App Router pages & API routes
│   ├── (auth)/           # Login, signup, password reset
│   ├── (dashboard)/      # Authenticated dashboard pages
│   └── api/              # REST API endpoints
├── components/           # Shared UI components
├── lib/                  # Core utilities (auth, db, payments, etc.)
├── workers/              # BullMQ background job workers
├── supabase/migrations/  # Database migrations
├── tests/                # Unit, integration, and E2E coverage
└── types/                # TypeScript type definitions

Included Guides

  • Quickstart and production-ready setup walkthroughs
  • Deployment, troubleshooting, and production checklists
  • Customization and implementation notes for extending the product
  • API and scaling references for operational handoff

Authentication

ShipKit uses Supabase Auth with server-side session validation. Authentication is fully pre-built — email/password, OAuth providers, password reset, and protected routes.

Supported Auth Methods

  • Email & password registration with email confirmation
  • Google OAuth (one-click sign in)
  • GitHub OAuth (one-click sign in)
  • Password reset via email link

Setting Up OAuth

Go to your Supabase dashboard → Authentication → Providers. Enable Google and/or GitHub, then add your OAuth client ID and secret from each provider.

text
# Supabase Dashboard → Authentication → Providers
# Enable Google:
#   Client ID:     your-google-client-id
#   Client Secret: your-google-client-secret
#
# Enable GitHub:
#   Client ID:     your-github-client-id
#   Client Secret: your-github-client-secret

Protected Routes

The proxy at proxy.ts automatically redirects unauthenticated users away from /dashboard routes to /login, and redirects authenticated users away from /login and /signup to /dashboard.

Using Auth in Server Components

typescript
import { createClient } from '@/lib/supabase/server'

export default async function Page() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  // user is null if not authenticated
}

Payments

Payments support both LemonSqueezy and Stripe. The boilerplate includes checkout flows, billing interval selection, webhook processing, and subscription lifecycle handling with idempotency protections.

Setup

  • Configure LemonSqueezy, Stripe, or both depending on how you want to sell
  • Create your products and price IDs for Basic and Pro plans
  • Copy processor keys and price identifiers into your .env.local
  • Set webhook URLs for /api/webhooks/lemonsqueezy and /api/webhooks/stripe as needed
bash
# LemonSqueezy
LEMONSQUEEZY_API_KEY=your-api-key
LEMONSQUEEZY_WEBHOOK_SECRET=your-webhook-secret
LEMONSQUEEZY_STORE_ID=your-store-id
LEMONSQUEEZY_VARIANT_BASIC=variant-id-1
LEMONSQUEEZY_VARIANT_PRO=variant-id-2
LEMONSQUEEZY_VARIANT_MONTHLY=variant-id-3
LEMONSQUEEZY_VARIANT_YEARLY=variant-id-4

# Stripe
STRIPE_API_KEY=sk_live_or_test
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_BASIC_MONTHLY=price_basic_monthly
STRIPE_PRICE_PRO_MONTHLY=price_pro_monthly

Webhook Handler

The boilerplate includes dedicated webhook handlers for both app/api/webhooks/lemonsqueezy/route.ts and app/api/webhooks/stripe/route.ts. Both follow the same idempotent event-processing pattern and update the shared billing model.

The billing layer is organized so both processors feed the same subscription system. Checkout lives under /api/billing/checkout, with processor-specific logic isolated in the payment libraries and webhook handlers.

Teams & Organizations

Multi-tenant team system with invitations, roles, organization-scoped data, and dashboard flows for managing members and workspace settings.

Features

  • Create organizations with unique slugs
  • Invite team members by email
  • Role-based access: Owner, Admin, Member
  • Organization-scoped queries via RLS policies

API Endpoints

MethodEndpointDescription
POST/api/orgsCreate organization
GET/api/orgsList user organizations
PATCH/api/orgs/[orgId]Update organization
POST/api/orgs/[orgId]/membersInvite member
PATCH/api/orgs/[orgId]/membersUpdate member role
DELETE/api/orgs/[orgId]/membersRemove member

Database & RLS

ShipKit uses Supabase (Postgres) with Row-Level Security policies, typed queries, and migration-based schema management so schema updates stay explicit and reviewable.

Setup

  • Create a project at supabase.com (free tier works)
  • Copy your project URL, anon key, and service role key to .env.local
  • Run migrations: npx supabase db push

Database Clients

typescript
// Server-side (with user's auth context)
import { createClient } from '@/lib/supabase/server'
const supabase = await createClient()

// Admin client (bypasses RLS — use for background jobs)
import { createAdminClient } from '@/lib/supabase/admin'
const admin = createAdminClient()

Migrations

bash
# Create a new migration
npm run db:migrate my_migration_name

# Push migrations to Supabase
npm run db:push

# Pull remote schema changes
npm run db:pull

# Generate TypeScript types from schema
npm run db:types

Background Jobs

Four BullMQ workers handle async tasks: email delivery, notifications, automations, and webhook dispatch. They keep slow or bursty work out of the request path and run with concurrency controls.

Workers

WorkerFilePurpose
Emailworkers/email.worker.tsSend emails via SES, Resend, or Mailgun
Notificationworkers/notification.worker.tsIn-app & push notifications
Automationworkers/automation.worker.tsScheduled tasks, drip campaigns
Webhookworkers/webhook.worker.tsOutbound webhook delivery

Running Workers

bash
# Run all workers concurrently
npm run workers

# Run a single worker
npm run worker:email

Adding a Job to the Queue

typescript
import { emailQueue } from '@/lib/queue'

await emailQueue.add('welcome', {
  to: user.email,
  subject: 'Welcome to MyApp!',
  template: 'welcome',
})
Workers require Redis. Set REDIS_URL in your env. Use Upstash for a free serverless Redis instance.

AI Integration

Claude (Anthropic), Groq, and ElevenLabs TTS are pre-wired. Call AI from any API route with typed clients.

Using Claude

typescript
import Anthropic from '@anthropic-ai/sdk'

const client = new Anthropic({
  apiKey: env.ANTHROPIC_API_KEY,
})

const message = await client.messages.create({
  model: 'claude-sonnet-4-20250514',
  max_tokens: 1024,
  messages: [{ role: 'user', content: 'Hello!' }],
})

Using Groq

typescript
import Groq from 'groq-sdk'

const groq = new Groq({ apiKey: env.GROQ_API_KEY })

const completion = await groq.chat.completions.create({
  model: 'llama-3.3-70b-versatile',
  messages: [{ role: 'user', content: 'Hello!' }],
})
AI keys are optional. Your app works without them — just leave them empty in .env.local and add them when you need AI features.

Email System

Transactional emails can be sent through AWS SES, Resend, or Mailgun. The boilerplate uses a provider switch and the same template flow for welcome emails, payment confirmations, team invitations, and contact messages.

Setup

  • Set EMAIL_PROVIDER to ses, resend, or mailgun
  • Set a shared sender with EMAIL_FROM
  • Add the provider-specific credentials to .env.local
bash
# Shared
EMAIL_PROVIDER=resend
EMAIL_FROM=hello@yourdomain.com

# SES
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_REGION=us-east-1
SES_FROM_EMAIL=hello@yourdomain.com

# Resend
RESEND_API_KEY=re_...

# Mailgun
MAILGUN_API_KEY=key-...
MAILGUN_DOMAIN=mg.yourdomain.com

Sending Emails

typescript
import { sendEmail } from '@/lib/email'

await sendEmail({
  to: 'user@example.com',
  subject: 'Welcome!',
  html: '<h1>Welcome to MyApp</h1>',
  text: 'Welcome to MyApp',
})
The provider routing happens inside lib/email.ts. Once EMAIL_PROVIDER is set, existing email flows continue to call sendEmail(...) without needing app-level changes.

Security

Every endpoint is hardened. ShipKit implements defense-in-depth security that scores A+ across audit categories.

Built-in Protections

ProtectionImplementationGrade
CSRF ProtectionDouble-submit cookie patternA+
Content Security PolicyNonce-based CSP per requestA+
Rate LimitingRedis-backed with graceful degradationA
Webhook SecurityHMAC-SHA256 signature verificationA+
Input ValidationZod schemas on all endpointsA+
File UploadsMagic byte validation, size limitsA+
Security HeadersHSTS, X-Frame-Options, CSP, etc.A+
Error HandlingNo stack traces or internals in responsesA+
LoggingPII redaction in structured logsA

CSP Nonces

Every request gets a unique nonce via the edge proxy. Use the ScopedStyle component for inline styles to comply with the strict CSP policy.

typescript
import { ScopedStyle } from '@/components/shared/ScopedStyle'

// Always use ScopedStyle instead of raw <style> tags
<ScopedStyle>{`
  .my-class { color: #7c3aed; }
`}</ScopedStyle>

Caching

Redis-backed caching with typed helpers for common patterns — cached reads, invalidation, and temporary storage.

Cache Helpers

typescript
import { getCached, invalidateCache, setTemp, getTemp } from '@/lib/cache'

// Cache a database query for 5 minutes
const users = await getCached('users:list', 300, async () => {
  return await supabase.from('users').select('*')
})

// Invalidate when data changes
await invalidateCache('users:list')

// Temporary storage (e.g., OTP codes)
await setTemp('otp:user123', '491823', 600) // 10 min TTL
const code = await getTemp('otp:user123')
Caching is optional. Without Redis, cache functions return null and your app falls back to direct database queries.

Deployment

ShipKit deploys cleanly to modern Node.js platforms and container hosts. Vercel is the fastest path, but Railway, Render, Fly.io, Coolify, and standard Docker hosts all work.

Deploy to Vercel

bash
npx vercel --prod

Add all your .env.local variables in Vercel → Settings → Environment Variables.

Deploy to Railway

bash
# Install Railway CLI
npm i -g @railway/cli
railway login
railway init
railway up

Deploy with Docker

bash
docker build -t shipkit .
docker run -p 3000:3000 --env-file .env.local shipkit

Production Checklist

  • Set all required env vars in your hosting provider
  • Configure your LemonSqueezy and Stripe webhook URLs for your production domain
  • Set up a custom domain and enable HTTPS
  • Configure OAuth redirect URLs for your production domain
  • Verify the health endpoint: GET /api/health
  • Set LOG_LEVEL=info for production
  • Keep the deployment and troubleshooting guides nearby for your first live rollout

Health Endpoint

The /api/health endpoint checks database and Redis connectivity. Use it for uptime monitoring.

bash
curl https://yourdomain.com/api/health
# {"status":"healthy","services":{"database":"ok","redis":"ok"}}