DEV Community

Sergi Ullastre
Sergi Ullastre

Posted on

I Wasted 6 Weeks Building SaaS Boilerplate (Again). Here's What Finally Made Me Stop.

Every side project started the same way.
"This time I'll finally ship fast."

Then I'd spend the first month building:

  • Authentication with email verification
  • "Forgot password" flows
  • Team invitations
  • Stripe subscriptions
  • Email templates that don't look terrible

By week 6, I still hadn't written a single line of my actual product. Sound familiar?

The Graveyard of Almost-Shipped Ideas

I've been building SaaS products for years. My GitHub is a cemetery of half-finished projects—not because the ideas were bad, but because I ran out of steam rebuilding the same infrastructure every time.

The worst part? I knew it was happening. I'd tell myself "this is the last time I build auth from scratch." Then three months later, I'm debugging magic link token expiration at 2 AM. Again.

Why Existing Solutions Didn't Work For Me

I tried the alternatives:

  • Next.js starter kits — Great ecosystem, but I wanted Vue. Personal preference, but it matters when you're spending months in a codebase.
  • Laravel starters — Solid, but I wanted TypeScript end-to-end. No context switching between PHP and JavaScript.
  • Firebase/Supabase — Fantastic for MVPs, but I always hit walls when I needed custom business logic. Vendor lock-in anxiety is real.

Building "just the auth part" myself — LOL. "Just auth" becomes auth + email verification + password reset + magic links + rate limiting + session management. There's no "just."

What I Actually Needed

After yet another abandoned project, I wrote down what I kept rebuilding:

Authentication (The Iceberg)

What I thought: "Add a login form"
What it actually is:
├── Email/password registration
├── Email verification flow
├── Magic link authentication  
├── Password reset
├── Session management
├── Rate limiting
├── Social OAuth (Google, GitHub, Facebook)
└── Account deletion (GDPR compliance)
Enter fullscreen mode Exit fullscreen mode

Team Management

Real SaaS products have teams. That means:

  • Creating/deleting teams
  • Inviting members via email
  • Role-based permissions (owner, admin, member)
  • Handling invitation acceptance/rejection
  • Team switching in the UI

Billing

Stripe is powerful but complex. Every project needs:

  • Checkout sessions
  • Subscription management
  • Customer portal integration
  • Webhook handling
  • Invoice display
  • Plan upgrades/downgrades

The "Boring" Stuff That Takes Forever

  • Beautiful, responsive email templates
  • Dark mode that actually works
  • Form validation everywhere
  • Loading states and error handling
  • A landing page that doesn't look like a developer made it

The Stack I Landed On

After experimenting with many combinations, here's what clicked for me:

Backend: AdonisJS 6

I wanted Laravel's developer experience but in TypeScript. AdonisJS delivers exactly that:

// Clean, expressive syntax
Route.group(() => {
  Route.post('/teams', [CreateTeamController])
  Route.get('/teams/:id/members', [GetTeamMembersController])
}).middleware('auth')
Enter fullscreen mode Exit fullscreen mode

Batteries included: ORM, validation, auth, mail, queues—all first-party, all TypeScript.

Frontend: Nuxt 4 + Vue 3

Vue's reactivity model just makes sense to my brain. Nuxt adds:

  • File-based routing
  • SSR when I need it
  • Auto-imports that don't feel magical

UI: Tailwind CSS 4 + shadcn-vue

I'm not a designer. shadcn gives me beautiful, accessible components I can actually customize:

<Dialog>
  <DialogTrigger as-child>
    <Button>Invite Member</Button>
  </DialogTrigger>
  <DialogContent>
    <!-- Accessible, animated, customizable -->
  </DialogContent>
</Dialog>
Enter fullscreen mode Exit fullscreen mode

40+ components, all with dark mode, all following the same design system.

AI: Vercel AI SDK

Every SaaS will need AI features soon. I integrated OpenAI, Anthropic, and Google from day one:

// Switch between providers seamlessly
const result = await aiService.streamText({
  messages,
  modelName: 'claude-sonnet-4-20250514' // or 'gpt-4o', 'gemini-2.5-pro'
})
Enter fullscreen mode Exit fullscreen mode

The Technical Decisions That Mattered

1. Background Jobs for Everything Email

Never send emails synchronously. Ever.

// Don't do this
await mail.send(new VerifyEmailNotification(user))

// Do this
await queue.dispatch(SendVerificationEmailJob, { userId: user.id })
Enter fullscreen mode Exit fullscreen mode

Your API responds instantly, emails go out reliably, and one slow SMTP server doesn't tank your request.

2. Separate API Services from Controllers

Controllers handle HTTP. Services handle business logic.

// Controller: thin
async handle({ request, response }) {
  const data = await request.validateUsing(createTeamValidator)
  const team = await teamService.create(data, auth.user)
  return response.created(team)
}

// Service: all the logic
class TeamService {
  async create(data: CreateTeamData, owner: User) {
    const team = await Team.create(data)
    await team.related('members').create({ 
      userId: owner.id, 
      role: 'owner' 
    })
    return team
  }
}
Enter fullscreen mode Exit fullscreen mode

Controllers stay readable. Services become testable and reusable.

3. Type Everything

Full TypeScript means:

// API responses are typed
interface TeamMember {
  id: number
  userId: number
  role: 'owner' | 'admin' | 'member'
  user: Pick<User, 'id' | 'firstName' | 'lastName' | 'email'>
}

// Frontend knows exactly what to expect
const { data } = await getTeamMembers(teamId)
// data.role is 'owner' | 'admin' | 'member', not string
Enter fullscreen mode Exit fullscreen mode

Catches bugs at compile time. Enables autocomplete everywhere.

4. MJML for Emails

HTML emails are a nightmare. MJML makes them tolerable:

<mj-section>
  <mj-column>
    <mj-button href="{{ url }}">
      Verify Your Email
    </mj-button>
  </mj-column>
</mj-section>
Enter fullscreen mode Exit fullscreen mode

Compiles to bulletproof HTML that works in Outlook 2007. (Yes, people still use that.)

What I Learned Building This

1. "Simple" Features Aren't Simple

Magic link auth sounds easy: generate token, send email, verify token.

Reality: Token expiration, rate limiting, preventing token reuse, handling expired tokens gracefully, invalidating old tokens when new ones are requested, email deliverability...

2. Testing Saves More Time Than It Costs

I have 195+ tests. Every time I refactor something, they catch regressions I would've shipped to production.

test('user can accept team invitation', async ({ client }) => {
  const invitation = await InvitationFactory.create()

  const response = await client
    .post(`/invitations/${invitation.token}/accept`)
    .loginAs(invitedUser)

  response.assertStatus(200)
  await invitation.refresh()
  assert.equal(invitation.status, 'accepted')
})
Enter fullscreen mode Exit fullscreen mode

3. Design Systems > Custom Designs

I spent years fighting CSS. Now I use shadcn components and customize the CSS variables. Consistent, accessible, fast.

4. Premature Abstraction Is Real

My first version had a "notification service" that abstracted emails, SMS, and push notifications. I only needed email.

Now I build the simplest thing that works and abstract when there's a real need—not a hypothetical one.

5. Ship Boring Tech

I could've used the hot new framework. Instead: PostgreSQL (40 years of reliability), Redis (battle-tested), AdonisJS (stable, well-documented).

When something breaks at 3 AM, I want boring. Boring has Stack Overflow answers.

The Result

I finally stopped rebuilding and started shipping.

I packaged everything into Nuda Kit — the SaaS starter kit I wish existed when I started.

It's not free (I spent months building this), but if you value your time, it might save you weeks:

  • ✅ Full authentication — Email/password, magic links, social OAuth, verification, password reset
  • ✅ Team management — Create teams, invite members, manage roles
  • ✅ Stripe billing — Subscriptions, checkout, portal, invoices, webhooks
  • ✅ AI integration — OpenAI, Anthropic, Google with streaming chat UI
  • ✅ Beautiful UI — 40+ shadcn components, dark mode, full landing page
  • ✅ Production-ready — TypeScript everywhere, 35+ tests, background jobs, Docker setup

For Those Who'll Build Their Own

If you're going to build from scratch anyway (I respect that), here's my advice:

  1. Timebox auth. If you're still building auth after 2 weeks, step back and reassess.
  2. Use background jobs from day one. Retrofitting async processing into a sync codebase is painful.
  3. Pick boring, stable technologies. AdonisJS, Nuxt, Postgres. They'll still be here in 5 years.
  4. Write tests for critical paths only. Auth, billing, team permissions. Skip tests for UI tweaks.
  5. Ship the ugly version first. You can make it pretty after you know people want it.
  6. Don't abstract too early. You don't need a "notification service" when you only send emails.

What's Next?

I'm genuinely curious: What features do you keep rebuilding?

For me it was auth and billing. For you it might be file uploads, real-time notifications, or analytics dashboards.

Drop a comment—I want to know what slows people down.

Building something? I'd love to hear about it. Find me on Twitter or check out Nuda Kit if you want to skip the boilerplate and start shipping.

Top comments (0)