Building a SaaS in 30 Days: What I Learned
Building a SaaS in 30 Days: What I Learned
Shipping a full SaaS product solo in 30 days sounds insane. It kind of is. But it's also one of the best ways to sharpen every layer of your stack simultaneously.
The Stack I Chose
After building several platforms, I've settled into a stack that lets me move fast without compromising quality:
- Next.js 14 (App Router) — full-stack in one repo
- PostgreSQL + Prisma — type-safe DB with real relations
- NextAuth.js — auth done right, fast
- Stripe — subscriptions, webhooks, customer portal
- Vercel — zero-config deploys
The key insight: avoid choice fatigue. Pick a stack you know deeply and go.
Week 1: Foundation
The first week is all about boring infrastructure that will save you a thousand headaches later.
// The auth middleware pattern that saved me
export async function middleware(req: NextRequest) {
const session = await getToken({ req });
if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', req.url));
}
return NextResponse.next();
}
Don't skip this. Every protected route needs to be locked down before you touch a single feature.
Week 2: Core Features
This is where most people go wrong. They try to build everything. Build the one thing that makes your product worth paying for. Everything else is noise.
For my product, that was a single dashboard view that showed users their most important metric at a glance. Nothing else mattered until that worked perfectly.
Week 3: Payments
Stripe is your friend if you read the docs. The biggest mistake I see is developers building payments as an afterthought.
// Webhook handling — get this right first time
export async function POST(req: Request) {
const body = await req.text();
const sig = headers().get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch (err) {
return new Response(`Webhook Error: ${err}`, { status: 400 });
}
// Handle subscription events
switch (event.type) {
case 'customer.subscription.created':
case 'customer.subscription.updated':
await handleSubscriptionChange(event.data.object as Stripe.Subscription);
break;
}
return new Response(null, { status: 200 });
}
Week 4: Polish and Launch
The last week is 20% building and 80% testing edge cases you never imagined. What happens when a user's payment fails on day 14 of their trial? What happens when two users try to claim the same username?
Key Takeaways
- Ship something ugly first — perfection is the enemy of launched
- Instrument everything — you can't improve what you can't measure
- Talk to users on day 1 — not day 30
- The boring stack wins — Next.js + PostgreSQL still beats the hype every time
The full stack is available as a starter template. Drop me a message if you want early access.