The Decisions That Cost You Later
Most technical decisions in a new SaaS feel equally important. They're not. A handful of early choices compound over time—they shape everything that comes after.
Here are the ones that actually matter.
1. Auth Library vs Build Your Own
Build your own auth: 3-4 weeks. Session management, token rotation, OAuth providers, password reset flows, MFA.
Use a library (NextAuth, Clerk, Auth0):
// NextAuth — 30 minutes to working auth
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
GitHub({ clientId: '...', clientSecret: '...' }),
Google({ clientId: '...', clientSecret: '...' }),
Resend({ apiKey: '...' }), // magic link
],
adapter: PrismaAdapter(prisma),
callbacks: {
session({ session, token }) {
session.user.id = token.sub!;
return session;
},
},
});
Decision: Use a library. Auth is not a differentiator. Spend those 3 weeks on product.
2. TypeScript From Day One
Adding TypeScript to a JavaScript codebase is painful. Starting with TypeScript is free.
npx create-next-app@latest my-saas --typescript
# Done. You have TypeScript.
The ROI compounds: faster refactoring, fewer production bugs, better IDE support. No SaaS regrets starting with TypeScript.
3. Your Database Choice
PostgreSQL: The default right answer.
→ Prisma ORM, migrations, JSON support, full-text search
→ Supabase (managed) or self-host on any VPS
MySQL/MariaDB: Fine, slightly less features
SQLite: Great for development, risky for production (single writer)
MongoDB: Only if you truly need document model
PlanetScale: MySQL-compatible, great scaling story
Decision: PostgreSQL + Prisma. There's almost no reason to choose differently for a new SaaS.
4. Multi-Tenancy Architecture
This is the one you can't easily change later.
// Every model gets tenant_id
model Project {
id String @id @default(cuid())
orgId String // tenant_id
name String
org Organization @relation(fields: [orgId], references: [id])
@@index([orgId])
}
// Every query includes orgId
const projects = await prisma.project.findMany({
where: { orgId: session.user.orgId }, // never forget this
});
Decide on day one: are you building for individual users or organizations? If organizations, add orgId to everything now.
5. Payments First or Last?
Many founders add payments late. This is a mistake.
Adding Stripe after launch means:
- Retrofitting data models
- Migrating users who signed up free
- Building upgrade flows under pressure
Add payments on week 2:
// Stripe checkout — 1 hour to implement
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${baseUrl}/dashboard?upgraded=true`,
cancel_url: `${baseUrl}/pricing`,
customer_email: user.email,
metadata: { userId: user.id },
});
Even if you don't charge initially, having the plumbing in place means you can flip a switch.
6. Observability From the Start
// 30 minutes to set up:
// 1. Sentry for errors
import * as Sentry from '@sentry/nextjs';
Sentry.init({ dsn: process.env.SENTRY_DSN });
// 2. PostHog for analytics
import posthog from 'posthog-js';
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!);
// 3. Structured logging
import pino from 'pino';
const logger = pino();
Without observability, you're flying blind. You won't know which features users actually use, what errors they hit, or where they drop off.
The Stack That Works
Frontend: Next.js 14 (App Router)
Auth: NextAuth.js or Clerk
DB: PostgreSQL + Prisma
Payments: Stripe
Email: Resend + React Email
Deployment: Vercel + Supabase
Monitoring: Sentry + PostHog
Style: Tailwind + shadcn/ui
This stack has solved all the hard infrastructure problems. Every hour you spend on alternatives is an hour not spent on your actual product.
Ship the boring stack. Differentiate on your product.
This entire stack, pre-configured and production-ready: Whoff Agents AI SaaS Starter Kit — skip the setup, ship the product.
Top comments (0)