From Zero to Production: Setting Up a Nuxt.js SaaS the Right Way
I built three SaaS products. With the first, I spent 6 months on boilerplate. With the third, I shipped in 6 weeks.
The difference? I didn't rebuild foundational systems every time.
This guide shares the exact blueprint I follow when launching a Nuxt.js SaaS from zero. You'll discover the 8 non-negotiable systems every SaaS needs, the mistakes that cost months, and how to architect your app so it scales from day one.
If you're a founder, freelancer, or engineer building your next product—this saves you months of setup.
Why Nuxt.js for SaaS? (And Why Most People Get It Wrong)
Nuxt.js is the framework for full-stack SaaS. You get Vue's developer experience, server-side rendering, auto-routing, and a Node.js backend (Nitro) all in one.
But here's the trap: Nuxt makes it easy to start, not easy to scale.
Most devs build fast → hit a wall → refactor everything → miss launch date.
The problem isn't Nuxt. It's that they skip the architectural foundation.
The 3 Wrong Ways People Build Nuxt SaaS
- "I'll add auth later" — Leads to rushed, insecure implementations. Email verification, session management, and OAuth integration are afterthoughts.
- "Payments are a feature, not a system" — Shipping without Stripe/Polar integration means rewriting checkout when it's 48 hours to launch.
- "The database schema can evolve" — No migrations, no version control, no rollback plan. One bad deploy and your production data is corrupted.
The Right Way: 8 Systems Before You Write Your First Feature
System 1: Type-Safe Authentication (Not Just Login/Logout)
Auth isn't a login form. It's sessions, OAuth, email verification, role-based access, and admin impersonation.
What You Actually Need
- Session Management: Secure HTTP-only cookies, expiry, refresh tokens
- Email Verification: Gate signup until user confirms their inbox
- OAuth (Social Login): Google, GitHub, Discord—reduce friction
- Magic Links: Email sign-in for users who forgot passwords
- Admin Impersonation: Debug user issues without knowing their password
- User Bans: Suspend bad actors with optional expiry
The Wrong Approach
// ❌ This is how most people start
async function login(email, password) {
const user = await db.users.findOne({ email })
if (user.password === password) {
return { userId: user.id } // Plaintext comparison? Sessions in localStorage?
}
}
The Right Approach
Use BetterAuth or similar battle-tested library:
// ✅ This is production-ready
import { betterAuth } from 'better-auth'
import { admin, magicLink, polar } from 'better-auth/plugins'
export const auth = betterAuth({
database: drizzle(client),
plugins: [
admin(), // Admin impersonation
magicLink(), // Email sign-in
polar(), // Auto-create payment customers
],
})
Then use in your API:
export default defineAuthenticatedEventHandler(async (event) => {
const session = await auth.api.getSession({ headers: event.headers })
if (!session?.user) {
throw createError({ status: 401 })
}
// Logged-in user is event.context.user
})
Time Saved: 4-6 weeks building auth from scratch vs. 1 day setting up BetterAuth.
System 2: Type-Safe Database (Drizzle + Postgres)
Your database is your system of truth. Get this wrong and you'll spend weeks debugging corrupted data.
Why Postgres + Drizzle?
Postgres: Most mature, feature-rich, predictable open-source database.
Drizzle ORM: Type-safe, migrations auto-generated, no runtime surprises.
Pattern: Schema + Migrations
Define your schema in TypeScript:
export const user = pgTable('user', {
id: serial().primaryKey(),
email: varchar(255).notNull().unique(),
name: varchar(255).notNull(),
role: pgEnum('user_role', ['user', 'admin']).default('user'),
createdAt: timestamp().defaultNow(),
deactivatedAt: timestamp(), // Soft delete
})
Generate migration:
pnpm drizzle-kit generate
Result: migrations/0001_users.sql (version controlled).
Deploy with confidence:
pnpm drizzle-kit migrate
Benefit: You can rollback, audit who changed what, and never lose data.
Queryable, Type-Safe Selects
const activeUsers = await db
.select({ id: user.id, email: user.email })
.from(user)
.where(and(
isNull(user.deactivatedAt),
eq(user.role, 'admin'),
))
// TypeScript auto-completes the result shape
Time Saved: 2-3 weeks building ORM + migration system from scratch.
System 3: Payments (Stripe, Polar, or Paddle)
You can't launch a SaaS without monetization. But integrating payments late is a nightmare.
Why Integrate Early?
- Your schema needs a
subscriptiontable from day one - User creation triggers must sync with payment processor
- Checkout flows need pre-configured products
- Webhooks (subscription renewed, canceled) need handlers
The Polar Approach (Recommended for Nuxt)
Polar is purpose-built for indie SaaS:
- Subscriptions, one-time sales, usage-based billing
- Merchant of Record (handles taxes, compliance)
- Entitlements (auto-grant license keys, GitHub access, downloads)
- Built-in checkout—no custom PCI compliance needed
Architecture:
User signs up → Polar customer created (auto)
↓
User subscribes → Webhook notifies your app
↓
Entitlements granted → License key issued
↓
User has access ✓
In your database:
export const subscription = pgTable('subscription', {
id: uuid().primaryKey(),
userId: integer().notNull().references(() => user.id),
pollarSubscriptionId: varchar(255).notNull().unique(),
status: pgEnum('sub_status', ['active', 'paused', 'canceled']),
productId: varchar(255), // Polar product ID
createdAt: timestamp().defaultNow(),
canceledAt: timestamp(),
})
Sync with webhook:
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { type, data } = body
if (type === 'subscription.created') {
await db.insert(subscription).values({
userId: data.metadata.userId,
pollarSubscriptionId: data.id,
status: 'active',
})
}
})
Time Saved: 3-4 weeks building payment infrastructure.
System 4: Email (The Underestimated System)
Every SaaS needs email:
- Welcome emails
- Password reset links
- Billing alerts
- Admin notifications
- User messages
Template-Based Email
Use Vue Email components + Resend:
<!-- EmailSecurityAlert.vue -->
<template>
<EContainer>
<EHeading as="h2">{{ title }}</EHeading>
<EText>Hi {{ user.name }},</EText>
<EButton :href="actionUrl">
{{ actionLabel }}
</EButton>
</EContainer>
</template>
<script setup>
defineProps({
user: Object,
title: String,
actionUrl: String,
actionLabel: String,
})
</script>
Send in your API:
import { renderEmailComponent } from '#app'
await sendEmail({
type: 'security',
to: { email: user.email },
subject: 'Reset Your Password',
html: await renderEmailComponent('EmailSecurityAlert', {
user,
title: 'Reset Your Password',
actionUrl: resetLink,
actionLabel: 'Reset Password',
}),
})
Key Mistakes:
- Sending emails synchronously → slow API responses
- Hardcoding email templates → unmaintainable HTML
- No audit trail → can't debug delivery issues
Time Saved: 1-2 weeks building email infrastructure.
System 5: File Storage (S3 + Metadata)
Users upload avatars, documents, exports. You need reliable, scalable storage.
S3-Compatible (AWS, DigitalOcean, Backblaze, Cloudflare)
const storage = useStorage('file') // Configured in nuxt.config
// Upload
const filePath = `avatars/${userId}/${uuid()}.jpg`
await storage.setItemRaw(filePath, buffer)
// Link in DB
await db.update(user)
.set({ avatar: filePath })
.where(eq(user.id, userId))
// Delete old file
const oldFile = await db.query.user.findFirst({ where: eq(user.id, userId) })
if (oldFile?.avatar) {
await storage.removeItem(oldFile.avatar)
}
Key Decisions:
- Public vs. private uploads (ACLs)?
- CDN for downloads?
- Cleanup policy for orphaned files?
Time Saved: 1 week integrating file storage.
System 6: Admin Panel (User Management, Not Just Pages)
You need tools to:
- Search users by email
- Impersonate users (debug issues)
- Ban/unban users
- Refund orders
- View audit logs
This isn't optional. You'll need it week 1 of production.
Minimal Admin Requirements
// pages/admin/users/index.vue
const users = await $fetch('/api/admin/users', {
query: { search, page, sort },
})
// Impersonate
await $fetch(`/api/admin/users/${userId}/impersonate`, {
method: 'POST',
})
// Ban with expiry
await $fetch(`/api/admin/users/${userId}`, {
method: 'PATCH',
body: {
banned: true,
banExpires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
},
})
Backend:
export default defineAdminEventHandler(async (event) => {
const { search, page = 1, sort = 'created' } = await getValidatedQuery(event, searchSchema.parse)
const users = await db.query.user.findMany({
where: like(user.email, `%${search}%`),
limit: 20,
offset: (page - 1) * 20,
})
return { users, total: users.length }
})
Time Saved: 2-3 weeks building admin tooling.
System 7: Monitoring & Error Tracking
In production, errors happen. You need to know immediately.
Essentials
- Health Checks:
/api/healthreturns system status
export default defineEventHandler(() => {
return { status: 'ok', timestamp: new Date().toISOString() }
})
- Error Notifications: Critical errors trigger email to admins
// In error handler
if (error.statusCode === 500) {
await sendEmail({
type: 'system',
to: { email: adminEmail },
subject: `⚠️ Critical Error: ${error.message}`,
html: buildErrorReport(error),
})
}
- Request Logging: Log slow/failed API calls
export default defineEventHandler((event) => {
const start = Date.now()
return () => {
const duration = Date.now() - start
if (duration > 1000 || event._error) {
console.warn({ path: event.node.req.url, duration, error: event._error })
}
}
})
Time Saved: 1 week building observability.
System 8: SEO & Content (Blog, Docs)
You need content to rank in Google and convert visitors to users.
Architecture
- Blog: Nuxt Content + auto-sitemap
- Docs: User-facing guides
- Legal: Terms, Privacy (required)
- Landing Page: Marketing copy
Each auto-generates:
sitemap.xml(search engines)robots.txt(crawl rules)- Meta tags (social preview)
- Open Graph images (dynamic)
// Automatic sitemap generation
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('sitemap:sources', async (ctx) => {
const posts = await queryCollection(ctx.event, 'blog').select('path').all()
ctx.sources.push({
urls: posts.map(p => p.path),
sourceType: 'app',
})
})
})
Result: Your blog posts rank in Google without manual SEO work.
Time Saved: 2-3 weeks of manual SEO setup.
The Complete Architecture (Your Starting Point)
Now that you understand the 8 systems, here's how they fit together:
User lands on site
↓
Reads blog post (SEO'd)
↓
Signs up (Auth system)
↓
Email verification (Email system)
↓
Creates account (Database system)
↓
Uploads avatar (File storage)
↓
Subscribes to plan (Payments)
↓
Webhook confirms subscription → Admin dashboard updates (Monitoring)
↓
User has access ✓
The Time Investment
Building each system from scratch:
- Auth: 4-6 weeks
- Database: 2-3 weeks
- Payments: 3-4 weeks
- Email: 1-2 weeks
- File Storage: 1 week
- Admin Panel: 2-3 weeks
- Monitoring: 1 week
- SEO/Content: 2-3 weeks
Total: 16-24 weeks of engineering before your first feature.
How to Actually Ship (3 Approaches)
Option 1: Build It Yourself (If You Have 6 Months)
Pros:
- Full control
- Learn deeply
- Ultimate flexibility
Cons:
- Months of setup
- Security mistakes (auth is hard)
- Opportunity cost
Option 2: Assemble from Boilerplates (1-2 Months)
Combine existing tools:
- Use Nuxt 4 template
- Add BetterAuth (auth)
- Add Drizzle + Postgres (database)
- Add Resend (email)
- Add Polar (payments)
Pros:
- Faster than building
- Good learning experience
- Flexible
Cons:
- Glue code between systems
- No UI/admin panel included
- Mistakes in integration
Option 3: Production-Ready Boilerplate (1-2 Weeks)
Use a SaaS boilerplate that includes all 8 systems pre-wired.
Example: NuxtStart
Includes:
- ✅ BetterAuth (with OAuth, magic links, admin impersonation)
- ✅ Drizzle + Postgres migrations
- ✅ Polar payments + webhooks
- ✅ Vue Email + Resend
- ✅ S3 file storage
- ✅ Admin panel (user management, impersonation, banning)
- ✅ Error monitoring
- ✅ Blog, docs, landing page (all SEO'd)
Plus:
- Type-safe API routes (Zod validation)
- Nuxt UI components
- ESLint + Husky (code quality)
- Scheduled tasks (ban expiry, cleanup jobs)
Result: Ship your first feature in week 2, not week 26.
The Hidden Cost of Rushing
Most founders try Option 1 or 2 to "save money" or "learn."
What actually happens:
- Week 4: Auth isn't secure; rewrite
- Week 8: Payments integration breaks; refactor database
- Week 12: No admin panel; ship without user management tools
- Week 16: Production errors going unnoticed; build monitoring
- Week 20: Realize you need a blog for SEO; start over
By week 20, you've spent 5 months on setup and still aren't live.
Meanwhile, competitors using a boilerplate shipped in week 6 and spent 14 weeks on features.
Common Mistakes (And How to Avoid Them)
❌ Mistake 1: "I'll Add Email Later"
Email verification is critical for security. Hardcoding email templates later is a nightmare.
✅ Set up templated email on day 1.
❌ Mistake 2: "Payments Can Wait"
Polar/Stripe integration touches auth, database, and webhooks. Adding it mid-project requires rewriting.
✅ Integrate payments before writing user features.
❌ Mistake 3: "I Don't Need an Admin Panel Yet"
You will need to debug users, refund orders, and ban bad actors. Day 1.
✅ Build minimal admin tools before launch.
❌ Mistake 4: "I'll Worry About Security Later"
Auth is hard. Mistakes in hashing, sessions, or CSRF will haunt you.
✅ Use battle-tested libraries like BetterAuth instead of rolling your own.
❌ Mistake 5: "My Database Schema Can Evolve"
No migrations = production data corruption = customer churn.
✅ Use Drizzle or similar from day 1. Version control migrations.
Your 2-Week Launch Plan
Week 1: Setup & Foundation
- Day 1-2: Choose framework/boilerplate
- Day 3-4: Deploy auth (sign up, email verification, OAuth)
- Day 5-6: Set up database & first migrations
- Day 7: Configure payments (Polar product + webhook)
Week 2: MVP & Launch
- Day 8-9: Build 1-2 core features (your unique value)
- Day 10: Admin panel, error monitoring
- Day 11: Blog/docs, SEO setup
- Day 12: Launch
Compare to 6 months building from scratch. That's a 12x speed difference.
Key Takeaway
Every SaaS needs 8 systems. The only question is: will you build them yourself or use a starting point?
Building yourself teaches you deeply but costs 6 months.
Using a production-ready boilerplate costs 2 weeks and lets you focus on what makes your SaaS unique.
If you're launching a SaaS, you're racing the clock. Don't spend it on boilerplate.
Spend it on product.
Further Reading
What system do you struggle with most when building SaaS? Drop a comment.
Building Profitable SaaS Pricing: A Complete Guide for Polar Founders
Building a SaaS with Polar? Design pricing that drives 70-80% revenue from expansions. Complete guide to upsell strategies & customer value optimization.
Making Nitro Scheduled Tasks Work Everywhere: A Complete Guide
Guide to implementing Nitro scheduled tasks across all deployment platforms including Vercel, Netlify, Cloudflare, Railway, and VPS setups.

