
Migrating from Supastarter to ShipFast
Simplify your stack by moving from Supastarter to ShipFast when you don't need i18n or complex team features.
Prerequisites
- •Familiarity with Next.js App Router
- •Understanding of your current Supastarter codebase
- •Basic knowledge of Prisma/database migrations
Migrating from Supastarter to ShipFast
Sometimes simpler is better. If you started with Supastarter but found you don't need i18n, multiple payment providers, or complex organization structures, ShipFast offers a leaner alternative that's easier to maintain.
Why Migrate?#
You might want to migrate if:
- You don't need internationalization (i18n)
- Single payment provider (Stripe) is sufficient
- You want a simpler, flatter codebase
- Team/organization features are overkill for your use case
- You prefer less abstraction and more direct code
Stay with Supastarter if:
- You're planning to expand internationally
- You need flexible payment provider options
- Multi-tenant/organization features are important
- You've already built heavily on the organization model
Migration Overview#
| Aspect | Supastarter | ShipFast |
|---|---|---|
| Auth | NextAuth with org context | NextAuth (simpler) |
| Payments | Stripe, Lemon Squeezy, Paddle | Stripe/Lemon Squeezy |
| Database | Prisma + Supabase/Postgres | Prisma + various DBs |
| i18n | First-class support | Not built-in |
| Teams | Full organization model | Basic or none |
| Complexity | Higher | Lower |
Step 1: Set Up a Fresh ShipFast Project#
# Clone or download ShipFast to a new directory
git clone [your-shipfast-repo] my-app-simplified
cd my-app-simplified
npm install
Step 2: Flatten Your Database Schema#
The main change is removing the organization layer:
Supastarter Schema (Complex)#
model User {
id String @id @default(cuid())
email String @unique
memberships Membership[]
}
model Organization {
id String @id @default(cuid())
name String
memberships Membership[]
subscription Subscription?
}
model Membership {
id String @id @default(cuid())
userId String
organizationId String
role Role
}
ShipFast Schema (Simple)#
model User {
id String @id @default(cuid())
email String @unique
name String?
subscription Subscription? // Direct relationship
}
model Subscription {
id String @id @default(cuid())
userId String @unique
stripeCustomerId String?
stripePriceId String?
status String
user User @relation(fields: [userId], references: [id])
}
Migration Script#
// scripts/flatten-to-shipfast.ts
import { PrismaClient as OldPrisma } from './old-prisma-client';
import { PrismaClient as NewPrisma } from '@prisma/client';
const oldDb = new OldPrisma();
const newDb = new NewPrisma();
async function migrateUsers() {
// Get all users with their primary organization
const users = await oldDb.user.findMany({
include: {
memberships: {
where: { role: 'OWNER' },
include: { organization: { include: { subscription: true } } },
},
},
});
for (const user of users) {
// Create flattened user
const newUser = await newDb.user.create({
data: {
id: user.id,
email: user.email,
name: user.name,
emailVerified: user.emailVerified,
},
});
// Migrate subscription from organization to user
const orgSub = user.memberships[0]?.organization?.subscription;
if (orgSub) {
await newDb.subscription.create({
data: {
userId: newUser.id,
stripeCustomerId: orgSub.stripeCustomerId,
stripePriceId: orgSub.stripePriceId,
status: orgSub.status,
currentPeriodEnd: orgSub.currentPeriodEnd,
},
});
}
}
console.log(`Migrated ${users.length} users`);
}
migrateUsers();
Step 3: Simplify Authentication#
Remove organization context from sessions:
Before (Supastarter)#
callbacks: {
async session({ session, user }) {
const membership = await getActiveOrganization(user.id);
session.user.organizationId = membership?.organizationId;
session.user.role = membership?.role;
return session;
},
},
After (ShipFast)#
callbacks: {
async session({ session, user }) {
session.user.id = user.id;
// No organization context needed
return session;
},
},
Step 4: Update API Routes#
Remove organization scoping from your queries:
Before (Supastarter)#
export async function GET(req: Request) {
const session = await getServerSession(authOptions);
const { organizationId } = session.user;
const data = await prisma.project.findMany({
where: { organizationId },
});
return Response.json(data);
}
After (ShipFast)#
export async function GET(req: Request) {
const session = await getServerSession(authOptions);
const data = await prisma.project.findMany({
where: { userId: session.user.id },
});
return Response.json(data);
}
Step 5: Update Payment Webhooks#
Move subscriptions from organization to user level:
// ShipFast webhook handler
export async function POST(req: Request) {
const event = stripe.webhooks.constructEvent(...);
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object;
// Tie directly to user, not organization
await prisma.subscription.upsert({
where: { userId: session.metadata.userId },
update: { status: 'active', ... },
create: { userId: session.metadata.userId, ... },
});
break;
}
}
Step 6: Remove i18n (If Not Needed)#
Delete translation files and simplify components:
# Remove i18n files
rm -rf locales/
rm -rf src/lib/i18n/
Update components to use plain strings:
// Before (Supastarter)
import { useTranslation } from 'next-intl';
export function Header() {
const t = useTranslation('common');
return <h1>{t('welcome')}</h1>;
}
// After (ShipFast)
export function Header() {
return <h1>Welcome to My SaaS</h1>;
}
Step 7: Migrate Your Business Logic#
Copy your custom code, removing organization references:
# Identify custom files to migrate
ls -la src/app/api/
ls -la src/components/
# For each custom file:
# 1. Copy to new project
# 2. Replace organizationId with userId
# 3. Remove i18n hooks
# 4. Simplify team/role logic
Testing Checklist#
- User authentication (sign up, sign in, sign out)
- Password reset flow
- Subscription checkout
- Subscription management (cancel, upgrade)
- Webhook handling
- All custom features work without org context
- Email notifications
Common Challenges#
1. Data Belongs to Organizations#
Problem: Your business data is tied to organizationId.
Solution: Migrate data to user ownership:
// Find the owner of each organization and assign data to them
for (const project of projects) {
const owner = await oldDb.membership.findFirst({
where: { organizationId: project.organizationId, role: 'OWNER' },
});
await newDb.project.create({
data: {
...project,
userId: owner.userId, // Replace org with user
},
});
}
2. Multi-User Organizations#
Problem: Some organizations have multiple users.
Solution: Either pick the owner, or create separate copies:
// Option A: Assign to owner only
// Option B: Duplicate data for each member (if applicable)
3. Role-Based Features#
Problem: You have admin-only features based on organization roles.
Solution: Simplify to user-level checks or remove:
// Before: Check org role
if (session.user.role === 'ADMIN') { ... }
// After: Check user property or simplify
if (session.user.isAdmin) { ... }
// Or just remove if not needed
Timeline Estimate#
| Phase | Estimated Time |
|---|---|
| Setup & Planning | 1 hour |
| Database Migration | 2 hours |
| Auth Simplification | 1 hour |
| API Route Updates | 2 hours |
| Testing | 2 hours |
| Total | ~8 hours |
Conclusion#
Migrating from Supastarter to ShipFast is relatively straightforward since you're removing complexity rather than adding it. The main work is flattening your data model from organization-based to user-based ownership.
This migration makes sense if you've realized the extra features of Supastarter are adding maintenance overhead without providing value for your specific use case.
Not sure which boilerplate to choose?
Take our 2-minute quiz and get personalized recommendations.
Take the Quiz