<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sergi Ullastre</title>
    <description>The latest articles on DEV Community by Sergi Ullastre (@seergiue).</description>
    <link>https://dev.to/seergiue</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2905958%2Fbc3f2b60-7878-40f7-8418-e02604a1cd72.jpeg</url>
      <title>DEV Community: Sergi Ullastre</title>
      <link>https://dev.to/seergiue</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/seergiue"/>
    <language>en</language>
    <item>
      <title>I Wasted 6 Weeks Building SaaS Boilerplate (Again). Here's What Finally Made Me Stop.</title>
      <dc:creator>Sergi Ullastre</dc:creator>
      <pubDate>Mon, 15 Dec 2025 12:38:14 +0000</pubDate>
      <link>https://dev.to/seergiue/i-wasted-6-weeks-building-saas-boilerplate-again-heres-what-finally-made-me-stop-3cmg</link>
      <guid>https://dev.to/seergiue/i-wasted-6-weeks-building-saas-boilerplate-again-heres-what-finally-made-me-stop-3cmg</guid>
      <description>&lt;p&gt;Every side project started the same way.&lt;br&gt;
"This time I'll finally ship fast."&lt;/p&gt;

&lt;p&gt;Then I'd spend the first month building:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication with email verification&lt;/li&gt;
&lt;li&gt;"Forgot password" flows&lt;/li&gt;
&lt;li&gt;Team invitations&lt;/li&gt;
&lt;li&gt;Stripe subscriptions&lt;/li&gt;
&lt;li&gt;Email templates that don't look terrible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By week 6, I still hadn't written a single line of my actual product. Sound familiar?&lt;/p&gt;
&lt;h2&gt;
  
  
  The Graveyard of Almost-Shipped Ideas
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Existing Solutions Didn't Work For Me
&lt;/h2&gt;

&lt;p&gt;I tried the alternatives:&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Building "just the auth part" myself&lt;/strong&gt; — LOL. "Just auth" becomes auth + email verification + password reset + magic links + rate limiting + session management. There's no "just."&lt;/p&gt;
&lt;h2&gt;
  
  
  What I Actually Needed
&lt;/h2&gt;

&lt;p&gt;After yet another abandoned project, I wrote down what I kept rebuilding:&lt;/p&gt;
&lt;h3&gt;
  
  
  Authentication (The Iceberg)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Team Management
&lt;/h3&gt;

&lt;p&gt;Real SaaS products have teams. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating/deleting teams&lt;/li&gt;
&lt;li&gt;Inviting members via email&lt;/li&gt;
&lt;li&gt;Role-based permissions (owner, admin, member)&lt;/li&gt;
&lt;li&gt;Handling invitation acceptance/rejection&lt;/li&gt;
&lt;li&gt;Team switching in the UI&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Billing
&lt;/h3&gt;

&lt;p&gt;Stripe is powerful but complex. Every project needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checkout sessions&lt;/li&gt;
&lt;li&gt;Subscription management&lt;/li&gt;
&lt;li&gt;Customer portal integration&lt;/li&gt;
&lt;li&gt;Webhook handling&lt;/li&gt;
&lt;li&gt;Invoice display&lt;/li&gt;
&lt;li&gt;Plan upgrades/downgrades&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  The "Boring" Stuff That Takes Forever
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Beautiful, responsive email templates&lt;/li&gt;
&lt;li&gt;Dark mode that actually works&lt;/li&gt;
&lt;li&gt;Form validation everywhere&lt;/li&gt;
&lt;li&gt;Loading states and error handling&lt;/li&gt;
&lt;li&gt;A landing page that doesn't look like a developer made it&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The Stack I Landed On
&lt;/h2&gt;

&lt;p&gt;After experimenting with many combinations, here's what clicked for me:&lt;/p&gt;
&lt;h3&gt;
  
  
  Backend: AdonisJS 6
&lt;/h3&gt;

&lt;p&gt;I wanted Laravel's developer experience but in TypeScript. AdonisJS delivers exactly that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Clean, expressive syntax&lt;/span&gt;
&lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/teams&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CreateTeamController&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/teams/:id/members&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;GetTeamMembersController&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Batteries included: ORM, validation, auth, mail, queues—all first-party, all TypeScript.&lt;/p&gt;

&lt;h3&gt;
  
  
  Frontend: Nuxt 4 + Vue 3
&lt;/h3&gt;

&lt;p&gt;Vue's reactivity model just makes sense to my brain. Nuxt adds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File-based routing&lt;/li&gt;
&lt;li&gt;SSR when I need it&lt;/li&gt;
&lt;li&gt;Auto-imports that don't feel magical&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  UI: Tailwind CSS 4 + shadcn-vue
&lt;/h3&gt;

&lt;p&gt;I'm not a designer. shadcn gives me beautiful, accessible components I can actually customize:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Dialog&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DialogTrigger&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Button&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Invite&lt;/span&gt; &lt;span class="nx"&gt;Member&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/DialogTrigger&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DialogContent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!--&lt;/span&gt; &lt;span class="nx"&gt;Accessible&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;animated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;customizable&lt;/span&gt; &lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/DialogContent&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Dialog&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;40+ components, all with dark mode, all following the same design system.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI: Vercel AI SDK
&lt;/h3&gt;

&lt;p&gt;Every SaaS will need AI features soon. I integrated OpenAI, Anthropic, and Google from day one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Switch between providers seamlessly&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;aiService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;streamText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;modelName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-sonnet-4-20250514&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// or 'gpt-4o', 'gemini-2.5-pro'&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Technical Decisions That Mattered
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Background Jobs for Everything Email
&lt;/h3&gt;

&lt;p&gt;Never send emails synchronously. Ever.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Don't do this&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VerifyEmailNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// Do this&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SendVerificationEmailJob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your API responds instantly, emails go out reliably, and one slow SMTP server doesn't tank your request.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Separate API Services from Controllers
&lt;/h3&gt;

&lt;p&gt;Controllers handle HTTP. Services handle business logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Controller: thin&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validateUsing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;createTeamValidator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;team&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;teamService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;created&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Service: all the logic&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TeamService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateTeamData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;team&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;related&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;members&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; 
      &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;owner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; 
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;team&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Controllers stay readable. Services become testable and reusable.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Type Everything
&lt;/h3&gt;

&lt;p&gt;Full TypeScript means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// API responses are typed&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TeamMember&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;owner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;member&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Pick&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firstName&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lastName&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Frontend knows exactly what to expect&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getTeamMembers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;teamId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// data.role is 'owner' | 'admin' | 'member', not string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Catches bugs at compile time. Enables autocomplete everywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. MJML for Emails
&lt;/h3&gt;

&lt;p&gt;HTML emails are a nightmare. MJML makes them tolerable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;mj-section&amp;gt;
  &amp;lt;mj-column&amp;gt;
    &amp;lt;mj-button href="{{ url }}"&amp;gt;
      Verify Your Email
    &amp;lt;/mj-button&amp;gt;
  &amp;lt;/mj-column&amp;gt;
&amp;lt;/mj-section&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compiles to bulletproof HTML that works in Outlook 2007. (Yes, people still use that.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned Building This
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. "Simple" Features Aren't Simple
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Magic link&lt;/strong&gt; auth sounds easy: generate token, send email, verify token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reality&lt;/strong&gt;: Token expiration, rate limiting, preventing token reuse, handling expired tokens gracefully, invalidating old tokens when new ones are requested, email deliverability...&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Testing Saves More Time Than It Costs
&lt;/h3&gt;

&lt;p&gt;I have 195+ tests. Every time I refactor something, they catch regressions I would've shipped to production.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user can accept team invitation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invitation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;InvitationFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/invitations/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;invitation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/accept`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loginAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invitedUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;invitation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invitation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;accepted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Design Systems &amp;gt; Custom Designs
&lt;/h3&gt;

&lt;p&gt;I spent years fighting CSS. Now I use shadcn components and customize the CSS variables. Consistent, accessible, fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Premature Abstraction Is Real
&lt;/h3&gt;

&lt;p&gt;My first version had a "notification service" that abstracted emails, SMS, and push notifications. I only needed email.&lt;/p&gt;

&lt;p&gt;Now I build the simplest thing that works and abstract when there's a real need—not a hypothetical one.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Ship Boring Tech
&lt;/h3&gt;

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

&lt;p&gt;When something breaks at 3 AM, I want boring. Boring has Stack Overflow answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;I finally stopped rebuilding and started shipping.&lt;/p&gt;

&lt;p&gt;I packaged everything into &lt;a href="https://www.producthunt.com/products/nuda-kit?launch=nuda-kit" rel="noopener noreferrer"&gt;Nuda Kit&lt;/a&gt; — the SaaS starter kit I wish existed when I started.&lt;/p&gt;

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

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

&lt;h2&gt;
  
  
  For Those Who'll Build Their Own
&lt;/h2&gt;

&lt;p&gt;If you're going to build from scratch anyway (I respect that), here's my advice:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;I'm genuinely curious: &lt;strong&gt;What features do you keep rebuilding&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;For me it was auth and billing. For you it might be file uploads, real-time notifications, or analytics dashboards.&lt;/p&gt;

&lt;p&gt;Drop a comment—I want to know what slows people down.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Building something? I'd love to hear about it. Find me on &lt;a href="https://x.com/seergiue" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; or check out &lt;a href="https://www.producthunt.com/products/nuda-kit?launch=nuda-kit" rel="noopener noreferrer"&gt;Nuda Kit&lt;/a&gt; if you want to skip the boilerplate and start shipping.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>typescript</category>
      <category>saas</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
