
Migrating from Custom Next.js to ShipFast
Stop reinventing the wheel. Move your handcrafted Next.js SaaS to ShipFast for production-ready auth, payments, and email out of the box.
Prerequisites
- •Existing Next.js application
- •Understanding of your current authentication and payment setup
- •Basic knowledge of Prisma or your current ORM
Migrating from Custom Next.js to ShipFast
You've built a Next.js app from scratch and it works. But maintaining custom auth, payment integration, and email sending is taking time away from your core product. ShipFast provides these foundations so you can focus on what makes your app unique.
Why Migrate?#
Migrate to ShipFast if:
- Auth bugs and edge cases are consuming your time
- Stripe integration feels fragile or incomplete
- Email deliverability is a constant headache
- You want battle-tested patterns for common SaaS features
- Speed of iteration matters more than custom implementations
Keep your custom setup if:
- Your auth/payment needs are truly unique
- You've already invested heavily in robust infrastructure
- ShipFast's opinions conflict with your architecture
- You prefer full control over all dependencies
What You Get from ShipFast#
| Your Custom Code | ShipFast Provides |
|---|---|
| DIY NextAuth config | Pre-configured OAuth + Magic Links |
| Custom Stripe webhooks | Complete subscription handling |
| Email sending logic | Resend/Postmark integration |
| Landing page | Ready-made marketing templates |
| Database setup | Prisma + migrations ready |
Step 1: Inventory Your Current App#
Before migrating, document what you've built:
## My Current App Inventory
### Authentication
- [ ] Email/password login
- [ ] Google OAuth
- [ ] GitHub OAuth
- [ ] Magic link
- [ ] Password reset
### Payments
- [ ] Stripe Checkout
- [ ] Subscription management
- [ ] Webhook handling
- [ ] Customer portal
### Email
- [ ] Transactional emails
- [ ] Marketing emails
- [ ] Email templates
### Custom Features
- [ ] List your business-specific features
Step 2: Set Up Fresh ShipFast Project#
# Clone ShipFast to a new directory
git clone [your-shipfast-repo] my-app-v2
cd my-app-v2
# Install dependencies
npm install
# Set up environment variables
cp .env.example .env.local
# Configure your existing credentials
Critical Environment Variables#
# .env.local
# Database - use your existing database or create new
DATABASE_URL="postgresql://..."
# Auth - your existing OAuth credentials
NEXTAUTH_SECRET="..."
GOOGLE_CLIENT_ID="..."
GOOGLE_CLIENT_SECRET="..."
# Stripe - your existing Stripe account
STRIPE_SECRET_KEY="sk_live_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_live_..."
# Email - configure as needed
RESEND_API_KEY="..."
Step 3: Migrate Your Database#
Option A: Fresh Start with Data Import#
// scripts/migrate-data.ts
import { PrismaClient as OldDB } from './old-prisma-client';
import { PrismaClient as NewDB } from '@prisma/client';
async function migrateData() {
const oldDb = new OldDB();
const newDb = new NewDB();
// Migrate users
const users = await oldDb.user.findMany();
for (const user of users) {
await newDb.user.create({
data: {
id: user.id,
email: user.email,
name: user.name,
image: user.avatar,
emailVerified: user.emailVerified,
// Map to ShipFast's user fields
},
});
}
// Migrate your custom models
// ...
}
Option B: Extend ShipFast Schema#
Add your custom models to ShipFast's Prisma schema:
// prisma/schema.prisma
// ShipFast's built-in models
model User {
// ... ShipFast fields
// Add your custom relations
projects Project[]
workspaces Workspace[]
}
// Your custom models
model Project {
id String @id @default(cuid())
name String
description String?
userId String
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Workspace {
id String @id @default(cuid())
name String
userId String
user User @relation(fields: [userId], references: [id])
settings Json?
createdAt DateTime @default(now())
}
Step 4: Migrate Authentication#
Replace Your Custom Auth#
// Your custom auth (probably)
// lib/auth.ts
import { getServerSession } from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
export const authOptions = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
callbacks: {
// Your custom callbacks
},
};
ShipFast Auth (pre-configured)#
ShipFast already handles this. Just update the environment variables:
# Your existing OAuth credentials work directly
GOOGLE_CLIENT_ID="your-existing-id"
GOOGLE_CLIENT_SECRET="your-existing-secret"
# ShipFast may add more providers easily
GITHUB_CLIENT_ID="..."
GITHUB_CLIENT_SECRET="..."
Update OAuth Callback URLs#
In your OAuth provider consoles, update callback URLs:
# Google Cloud Console
https://your-new-domain.com/api/auth/callback/google
# GitHub OAuth Apps
https://your-new-domain.com/api/auth/callback/github
Step 5: Migrate Payments#
Your Custom Stripe Integration#
// Your custom checkout
export async function POST(req: Request) {
const { priceId } = await req.json();
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_URL}/success`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
});
return Response.json({ url: session.url });
}
Use ShipFast's Checkout#
ShipFast provides checkout components. Update your pricing page:
// Before: Your custom checkout button
<button onClick={() => handleCheckout(priceId)}>
Subscribe
</button>
// After: ShipFast's checkout (check their docs for exact component)
import { CheckoutButton } from '@/components/payment/CheckoutButton';
<CheckoutButton priceId={priceId}>
Subscribe
</CheckoutButton>
Migrate Existing Stripe Data#
Your existing Stripe customers and subscriptions are in Stripe, not your code. You need to sync them:
// scripts/sync-stripe-customers.ts
import Stripe from 'stripe';
import { prisma } from '@/lib/prisma';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
async function syncStripeCustomers() {
// Get all users
const users = await prisma.user.findMany({
where: { stripeCustomerId: { not: null } },
});
for (const user of users) {
// Fetch subscription from Stripe
const subscriptions = await stripe.subscriptions.list({
customer: user.stripeCustomerId!,
status: 'all',
});
const activeSub = subscriptions.data.find(
s => s.status === 'active' || s.status === 'trialing'
);
if (activeSub) {
// Update user with subscription status
// ShipFast may store this differently - check their schema
await prisma.user.update({
where: { id: user.id },
data: {
subscriptionStatus: activeSub.status,
priceId: activeSub.items.data[0].price.id,
currentPeriodEnd: new Date(activeSub.current_period_end * 1000),
},
});
}
}
}
Update Webhook Endpoint#
# Update webhook endpoint in Stripe Dashboard
# Old: https://your-app.com/api/webhooks/stripe
# New: https://your-app.com/api/webhook/stripe (check ShipFast's path)
# Get new webhook secret and update .env
STRIPE_WEBHOOK_SECRET="whsec_..."
Step 6: Migrate Your API Routes#
Move Custom Endpoints#
// Your custom API route
// pages/api/projects/index.ts (or app/api/projects/route.ts)
export async function GET(req: Request) {
const session = await getServerSession(authOptions);
if (!session) {
return new Response('Unauthorized', { status: 401 });
}
const projects = await prisma.project.findMany({
where: { userId: session.user.id },
});
return Response.json(projects);
}
Adapt to ShipFast Patterns#
ShipFast may have helper utilities. Use them:
// Using ShipFast's auth helpers (example - check their docs)
import { getCurrentUser } from '@/lib/session';
export async function GET(req: Request) {
const user = await getCurrentUser();
if (!user) {
return new Response('Unauthorized', { status: 401 });
}
const projects = await prisma.project.findMany({
where: { userId: user.id },
});
return Response.json(projects);
}
Step 7: Migrate UI Components#
Inventory Your Components#
src/components/
├── ui/
│ ├── Button.tsx → Use ShipFast's Button
│ ├── Card.tsx → Use ShipFast's Card
│ └── Modal.tsx → Use ShipFast's Modal
├── forms/
│ └── ContactForm.tsx → Keep or adapt
├── features/
│ └── ProjectList.tsx → Keep - this is your business logic
└── layout/
└── Navbar.tsx → Adapt to ShipFast's layout
Replace with ShipFast Components#
// Before: Your custom button
import { Button } from '@/components/ui/Button';
// After: ShipFast's button (they likely use shadcn/ui)
import { Button } from '@/components/ui/button';
Keep Your Business Components#
Your feature-specific components should migrate with minimal changes:
// components/features/ProjectList.tsx
// This is YOUR business logic - move it as-is
import { Button } from '@/components/ui/button'; // Update imports
import { Card } from '@/components/ui/card';
export function ProjectList({ projects }) {
return (
<div className="grid gap-4">
{projects.map(project => (
<Card key={project.id}>
<CardHeader>
<CardTitle>{project.name}</CardTitle>
</CardHeader>
<CardContent>
{project.description}
</CardContent>
</Card>
))}
</div>
);
}
Step 8: Migrate Pages#
Map Your Routes#
| Your Route | ShipFast Route | Action |
|---|---|---|
/ | / | Use ShipFast's landing or customize |
/login | /auth/signin | Use ShipFast's auth pages |
/dashboard | /dashboard | Adapt your dashboard |
/settings | /dashboard/settings | Merge with ShipFast's settings |
/projects/* | /dashboard/projects/* | Move your pages |
Dashboard Integration#
// app/dashboard/projects/page.tsx
// Move your projects page into ShipFast's dashboard structure
import { getCurrentUser } from '@/lib/session';
import { prisma } from '@/lib/prisma';
import { ProjectList } from '@/components/features/ProjectList';
export default async function ProjectsPage() {
const user = await getCurrentUser();
if (!user) {
redirect('/auth/signin');
}
const projects = await prisma.project.findMany({
where: { userId: user.id },
orderBy: { createdAt: 'desc' },
});
return (
<div className="container py-8">
<h1 className="text-2xl font-bold mb-6">Your Projects</h1>
<ProjectList projects={projects} />
</div>
);
}
Step 9: Update Email Sending#
Your Custom Email#
// Your custom email sending
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function sendWelcomeEmail(email: string, name: string) {
await resend.emails.send({
from: 'noreply@yourapp.com',
to: email,
subject: 'Welcome!',
html: `<h1>Welcome, ${name}!</h1>`,
});
}
ShipFast Email Templates#
ShipFast likely has email templates and helpers:
// Use ShipFast's email utilities (check their docs)
import { sendEmail } from '@/lib/email';
import { WelcomeEmail } from '@/emails/Welcome';
export async function sendWelcomeEmail(email: string, name: string) {
await sendEmail({
to: email,
subject: 'Welcome!',
react: WelcomeEmail({ name }),
});
}
Step 10: Test Everything#
Critical Tests#
// __tests__/migration.test.ts
describe('Migration Verification', () => {
test('users can sign in with existing credentials', async () => {
// Test OAuth flow
});
test('existing subscriptions are recognized', async () => {
// Test subscription status
});
test('custom features work', async () => {
// Test your business logic
});
test('payments process correctly', async () => {
// Test with Stripe test mode
});
});
Manual Testing Checklist#
- Sign in with Google (existing user)
- Sign up new user
- Password reset flow
- Existing subscription recognized
- New subscription checkout
- Webhook events processed
- Dashboard loads correctly
- Custom features work
- Email sending works
Common Challenges#
1. Database Field Mismatches#
Problem: Your User model has different fields than ShipFast's.
Solution: Create a migration to map fields:
// Add missing fields to ShipFast schema
model User {
// ShipFast fields
id String @id
email String @unique
// Your additional fields
company String?
role String @default("user")
bio String?
// Add any custom fields you need
}
2. Different Authentication Tokens#
Problem: Active sessions become invalid.
Solution: Users will need to re-authenticate. Communicate this:
// Show migration notice
export function MigrationBanner() {
return (
<div className="bg-blue-100 p-4">
We've upgraded our app! Please sign in again to continue.
</div>
);
}
3. Stripe Customer IDs#
Problem: ShipFast stores Stripe data differently.
Solution: Map during migration:
// Ensure stripeCustomerId is in the right place
await prisma.user.update({
where: { id: user.id },
data: {
stripeCustomerId: existingStripeCustomerId,
},
});
Timeline Estimate#
| Phase | Estimated Time |
|---|---|
| Inventory & Planning | 2 hours |
| Project Setup | 1 hour |
| Database Migration | 4 hours |
| Auth Migration | 2 hours |
| Payment Migration | 4 hours |
| Component Migration | 4 hours |
| Testing & Fixes | 3 hours |
| Total | ~20 hours |
Conclusion#
Migrating from a custom Next.js setup to ShipFast is an investment that pays off in reduced maintenance burden. You get production-tested auth, payments, and email infrastructure, freeing you to focus on the features that make your app unique.
The key is methodical migration: inventory what you have, map it to ShipFast's patterns, and test thoroughly before switching production traffic.
Not sure which boilerplate to choose?
Take our 2-minute quiz and get personalized recommendations.
Take the Quiz