Article·  

From Zero to Production: Setting Up a Nuxt.js SaaS the Right Way

Learn how to build a production-ready Nuxt.js SaaS from scratch. Discover 8 critical systems you need, common pitfalls to avoid, and how to ship 3x faster with proven patterns.

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

  1. "I'll add auth later" — Leads to rushed, insecure implementations. Email verification, session management, and OAuth integration are afterthoughts.
  2. "Payments are a feature, not a system" — Shipping without Stripe/Polar integration means rewriting checkout when it's 48 hours to launch.
  3. "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?

  1. Your schema needs a subscription table from day one
  2. User creation triggers must sync with payment processor
  3. Checkout flows need pre-configured products
  4. Webhooks (subscription renewed, canceled) need handlers

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

  1. Health Checks: /api/health returns system status
export default defineEventHandler(() => {
  return { status: 'ok', timestamp: new Date().toISOString() }
})
  1. 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),
  })
}
  1. 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.

Copyright © 2026