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
git clone <your-repo-url> my-saas
cd my-saas
npm install2. Configure Environment
cp .env.example .env.localOpen .env.local and fill in your service keys:
| Variable | Service | Required |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | Supabase | Yes |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Supabase | Yes |
SUPABASE_SERVICE_ROLE_KEY | Supabase | Yes |
DATABASE_URL | Supabase (Postgres) | Yes |
LEMONSQUEEZY_API_KEY | LemonSqueezy | Yes |
LEMONSQUEEZY_WEBHOOK_SECRET | LemonSqueezy | Yes |
STRIPE_API_KEY | Stripe | Optional |
STRIPE_WEBHOOK_SECRET | Stripe | Optional |
STRIPE_PRICE_BASIC_MONTHLY | Stripe | Optional |
STRIPE_PRICE_PRO_MONTHLY | Stripe | Optional |
EMAIL_PROVIDER | Optional | |
EMAIL_FROM | Optional | |
REDIS_URL | Upstash / Redis | Optional |
ANTHROPIC_API_KEY | Anthropic (Claude) | Optional |
AWS_ACCESS_KEY_ID | AWS SES | Optional |
RESEND_API_KEY | Resend | Optional |
MAILGUN_API_KEY | Mailgun | Optional |
MAILGUN_DOMAIN | Mailgun | Optional |
3. Run the Dev Server
npm run devOpen http://localhost:3000 — your SaaS is live locally.
Project Structure
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 definitionsIncluded 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.
# 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-secretProtected 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
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/lemonsqueezyand/api/webhooks/stripeas needed
# 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_monthlyWebhook 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.
/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
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/orgs | Create organization |
| GET | /api/orgs | List user organizations |
| PATCH | /api/orgs/[orgId] | Update organization |
| POST | /api/orgs/[orgId]/members | Invite member |
| PATCH | /api/orgs/[orgId]/members | Update member role |
| DELETE | /api/orgs/[orgId]/members | Remove 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
// 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
# 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:typesBackground 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
| Worker | File | Purpose |
|---|---|---|
workers/email.worker.ts | Send emails via SES, Resend, or Mailgun | |
| Notification | workers/notification.worker.ts | In-app & push notifications |
| Automation | workers/automation.worker.ts | Scheduled tasks, drip campaigns |
| Webhook | workers/webhook.worker.ts | Outbound webhook delivery |
Running Workers
# Run all workers concurrently
npm run workers
# Run a single worker
npm run worker:emailAdding a Job to the Queue
import { emailQueue } from '@/lib/queue'
await emailQueue.add('welcome', {
to: user.email,
subject: 'Welcome to MyApp!',
template: 'welcome',
})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
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
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!' }],
}).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_PROVIDERtoses,resend, ormailgun - Set a shared sender with
EMAIL_FROM - Add the provider-specific credentials to
.env.local
# 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.comSending Emails
import { sendEmail } from '@/lib/email'
await sendEmail({
to: 'user@example.com',
subject: 'Welcome!',
html: '<h1>Welcome to MyApp</h1>',
text: 'Welcome to MyApp',
})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
| Protection | Implementation | Grade |
|---|---|---|
| CSRF Protection | Double-submit cookie pattern | A+ |
| Content Security Policy | Nonce-based CSP per request | A+ |
| Rate Limiting | Redis-backed with graceful degradation | A |
| Webhook Security | HMAC-SHA256 signature verification | A+ |
| Input Validation | Zod schemas on all endpoints | A+ |
| File Uploads | Magic byte validation, size limits | A+ |
| Security Headers | HSTS, X-Frame-Options, CSP, etc. | A+ |
| Error Handling | No stack traces or internals in responses | A+ |
| Logging | PII redaction in structured logs | A |
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.
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
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')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
npx vercel --prodAdd all your .env.local variables in Vercel → Settings → Environment Variables.
Deploy to Railway
# Install Railway CLI
npm i -g @railway/cli
railway login
railway init
railway upDeploy with Docker
docker build -t shipkit .
docker run -p 3000:3000 --env-file .env.local shipkitProduction 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=infofor 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.
curl https://yourdomain.com/api/health
# {"status":"healthy","services":{"database":"ok","redis":"ok"}}