<?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: Vlad Zoff</title>
    <description>The latest articles on DEV Community by Vlad Zoff (@vladzoff).</description>
    <link>https://dev.to/vladzoff</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3992934%2F61af1d3c-0ba9-465b-9e29-befc3796b413.jpeg</url>
      <title>DEV Community: Vlad Zoff</title>
      <link>https://dev.to/vladzoff</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vladzoff"/>
    <language>en</language>
    <item>
      <title>The Next.js SaaS Boilerplate Checklist for 2026</title>
      <dc:creator>Vlad Zoff</dc:creator>
      <pubDate>Wed, 12 Aug 2026 19:00:47 +0000</pubDate>
      <link>https://dev.to/vladzoff/the-nextjs-saas-boilerplate-checklist-for-2026-2g7m</link>
      <guid>https://dev.to/vladzoff/the-nextjs-saas-boilerplate-checklist-for-2026-2g7m</guid>
      <description>&lt;p&gt;Most SaaS boilerplates look similar on the homepage: auth, payments, dashboard, emails, blog, and a fast launch promise. The differences appear later, when a customer asks for team roles, billing changes, audit logs, SSO, data export, custom domains, or a support workflow.&lt;/p&gt;

&lt;p&gt;Use this checklist before you build on any starter.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Authentication Is More Than Sign-In
&lt;/h2&gt;

&lt;p&gt;A serious SaaS foundation should answer these questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does it support email/password and OAuth?&lt;/li&gt;
&lt;li&gt;Does it support magic links or passkeys?&lt;/li&gt;
&lt;li&gt;Can you enable 2FA?&lt;/li&gt;
&lt;li&gt;Are protected routes handled consistently?&lt;/li&gt;
&lt;li&gt;Are auth API routes rate-limited?&lt;/li&gt;
&lt;li&gt;Can admins impersonate users for support?&lt;/li&gt;
&lt;li&gt;Can you switch auth providers without rewriting the app?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If auth is only a login form and a session hook, the real work is still ahead.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Teams Should Be First-Class
&lt;/h2&gt;

&lt;p&gt;For B2B SaaS, the customer is often an organization, not a single user. The starter should model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Organizations or workspaces.&lt;/li&gt;
&lt;li&gt;Memberships.&lt;/li&gt;
&lt;li&gt;Roles.&lt;/li&gt;
&lt;li&gt;Invitations.&lt;/li&gt;
&lt;li&gt;Active organization context.&lt;/li&gt;
&lt;li&gt;Organization-scoped billing.&lt;/li&gt;
&lt;li&gt;Organization-scoped analytics.&lt;/li&gt;
&lt;li&gt;Organization-scoped webhooks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Skipping this early usually creates painful migrations later.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4aahfp8w2gtqvy17sbno.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4aahfp8w2gtqvy17sbno.png" alt="Organization switcher" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Billing Needs Product Flexibility
&lt;/h2&gt;

&lt;p&gt;Stripe checkout is useful, but it is not a complete billing model. Check whether the starter handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Subscriptions.&lt;/li&gt;
&lt;li&gt;One-time purchases.&lt;/li&gt;
&lt;li&gt;Trials.&lt;/li&gt;
&lt;li&gt;Seat-based billing.&lt;/li&gt;
&lt;li&gt;Usage credits.&lt;/li&gt;
&lt;li&gt;Add-ons.&lt;/li&gt;
&lt;li&gt;Webhook verification.&lt;/li&gt;
&lt;li&gt;Customer portal links.&lt;/li&gt;
&lt;li&gt;Subscription state in the database.&lt;/li&gt;
&lt;li&gt;Admin subscription support.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many products change packaging after launch. Your boilerplate should make that survivable.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Admin Is a Product Surface
&lt;/h2&gt;

&lt;p&gt;Admin panels are often treated as internal afterthoughts. That is a mistake. Customer support, billing support, abuse response, feature flags, and rollout decisions all happen there.&lt;/p&gt;

&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User management.&lt;/li&gt;
&lt;li&gt;Subscription list.&lt;/li&gt;
&lt;li&gt;Waitlist or leads.&lt;/li&gt;
&lt;li&gt;Feature flags.&lt;/li&gt;
&lt;li&gt;Webhook logs.&lt;/li&gt;
&lt;li&gt;Activity logs.&lt;/li&gt;
&lt;li&gt;Performance and error visibility.&lt;/li&gt;
&lt;li&gt;Export flows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A usable admin panel reduces support cost and launch risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The Database Layer Should Be Understandable
&lt;/h2&gt;

&lt;p&gt;Before choosing a starter, inspect the schema:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are table names clear?&lt;/li&gt;
&lt;li&gt;Are relationships explicit?&lt;/li&gt;
&lt;li&gt;Are indexes present where lists and lookups need them?&lt;/li&gt;
&lt;li&gt;Are auth tables compatible with the auth library?&lt;/li&gt;
&lt;li&gt;Is the code tied to one database vendor?&lt;/li&gt;
&lt;li&gt;Are migrations documented?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The database is where boilerplate shortcuts become expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. API Boundaries Should Match the Product
&lt;/h2&gt;

&lt;p&gt;A good SaaS starter should not force one API style everywhere. Type-safe internal product flows can use tRPC, public integrations can use REST or OpenAPI, and flexible clients may need GraphQL.&lt;/p&gt;

&lt;p&gt;Check for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input validation.&lt;/li&gt;
&lt;li&gt;Rate limits.&lt;/li&gt;
&lt;li&gt;Authenticated procedures.&lt;/li&gt;
&lt;li&gt;Admin-only procedures.&lt;/li&gt;
&lt;li&gt;Versioning for public APIs.&lt;/li&gt;
&lt;li&gt;Webhook signature verification.&lt;/li&gt;
&lt;li&gt;Clear error responses.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Email Is Not Just Transactional Templates
&lt;/h2&gt;

&lt;p&gt;You need more than one welcome email:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verification.&lt;/li&gt;
&lt;li&gt;Password or magic link flows.&lt;/li&gt;
&lt;li&gt;Invitations.&lt;/li&gt;
&lt;li&gt;Billing events.&lt;/li&gt;
&lt;li&gt;Reports.&lt;/li&gt;
&lt;li&gt;Drip campaigns.&lt;/li&gt;
&lt;li&gt;Custom sender domains.&lt;/li&gt;
&lt;li&gt;Bounce or suppression handling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If email is only a demo template, plan extra time.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Observability Should Be Wired Early
&lt;/h2&gt;

&lt;p&gt;At launch, you need to know what broke and who was affected. Evaluate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structured logs.&lt;/li&gt;
&lt;li&gt;Error reporting.&lt;/li&gt;
&lt;li&gt;Web vitals.&lt;/li&gt;
&lt;li&gt;Analytics events.&lt;/li&gt;
&lt;li&gt;Audit logs.&lt;/li&gt;
&lt;li&gt;Webhook delivery records.&lt;/li&gt;
&lt;li&gt;Background job visibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These systems are much easier to wire before production traffic arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. SEO and Content Matter for SaaS Starters
&lt;/h2&gt;

&lt;p&gt;If the starter includes marketing pages, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Metadata per page.&lt;/li&gt;
&lt;li&gt;Open Graph images.&lt;/li&gt;
&lt;li&gt;Sitemap.&lt;/li&gt;
&lt;li&gt;Robots rules.&lt;/li&gt;
&lt;li&gt;Blog RSS.&lt;/li&gt;
&lt;li&gt;Canonical URLs.&lt;/li&gt;
&lt;li&gt;FAQ schema where appropriate.&lt;/li&gt;
&lt;li&gt;Product schema on pricing pages.&lt;/li&gt;
&lt;li&gt;Noindex for auth, dashboard, and admin pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Marketing infrastructure is part of the product when your acquisition depends on search.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Deployment Should Not Be a Single Happy Path
&lt;/h2&gt;

&lt;p&gt;Vercel is a great default, but your customers or internal constraints may push you elsewhere. Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker support.&lt;/li&gt;
&lt;li&gt;CI/CD examples.&lt;/li&gt;
&lt;li&gt;Environment variable reference.&lt;/li&gt;
&lt;li&gt;Infrastructure-as-code examples.&lt;/li&gt;
&lt;li&gt;Background job guidance.&lt;/li&gt;
&lt;li&gt;Storage configuration.&lt;/li&gt;
&lt;li&gt;Production checklist.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Decision Rule
&lt;/h2&gt;

&lt;p&gt;The right question is not "how complex is my product today?" but "how expensive will it be to add what I need next?"&lt;/p&gt;

&lt;p&gt;A minimal starter is fast to start and painful to grow. A modular starter lets you begin with only auth, billing,&lt;br&gt;
and a dashboard — and add teams, admin, AI, or SSO when the product actually needs them.&lt;br&gt;
With Codapult, you remove what you don't need via CLI. You don't add missing modules from scratch six months after launch.&lt;/p&gt;

&lt;p&gt;The right boilerplate should help you launch faster without forcing a rewrite after the first serious customer.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://codapult.dev" rel="noopener noreferrer"&gt;Codapult&lt;/a&gt; was built against this checklist. If you want to verify it item by item, the &lt;a href="https://codapult.dev/compare" rel="noopener noreferrer"&gt;compare page&lt;/a&gt; maps each capability to what ships in the box, including modules, adapters, infrastructure, and plugins.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>nextjs</category>
      <category>checklist</category>
      <category>webdev</category>
    </item>
    <item>
      <title>ShipFast vs Supastarter vs MakerKit vs Codapult: Choosing a SaaS Starter in 2026</title>
      <dc:creator>Vlad Zoff</dc:creator>
      <pubDate>Mon, 10 Aug 2026 18:33:44 +0000</pubDate>
      <link>https://dev.to/vladzoff/shipfast-vs-supastarter-vs-makerkit-vs-codapult-choosing-a-saas-starter-in-2026-i5e</link>
      <guid>https://dev.to/vladzoff/shipfast-vs-supastarter-vs-makerkit-vs-codapult-choosing-a-saas-starter-in-2026-i5e</guid>
      <description>&lt;p&gt;SaaS starters are easy to compare badly.&lt;/p&gt;

&lt;p&gt;The cheapest way is to count pages, components, or screenshots. That misses the part that decides whether the starter still helps after the first launch: how much of the product surface is already wired together, how much can be removed, and how painful it is to change providers later.&lt;/p&gt;

&lt;p&gt;This comparison focuses on fit, not winner-takes-all scoring. ShipFast, Supastarter, MakerKit, and Codapult are built for different buyers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8anjz81rpomhzpiqdqu0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8anjz81rpomhzpiqdqu0.png" alt="Compare" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Positioning
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ShipFast&lt;/strong&gt; is optimized for solo founders who want to launch a simple product quickly. It is direct, lightweight, and intentionally close to the marketing-site-plus-checkout shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Supastarter&lt;/strong&gt; is a polished modern starter with strong monorepo conventions and broad SaaS coverage. It is a good fit when you like its chosen stack and want a well-packaged application foundation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MakerKit&lt;/strong&gt; is strong when you want a mature starter ecosystem and multiple stack choices, especially if Supabase or Firebase is central to your plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Codapult&lt;/strong&gt; is for builders who want to start lean and grow without a rewrite. The setup wizard removes the modules your MVP does not need — permanently, from the codebase. What you keep is fully integrated and can be toggled via environment variables as the product evolves. You decide the scope at setup time instead of bolting missing pieces on later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Buying Question
&lt;/h2&gt;

&lt;p&gt;The question is not "which starter has the most features?"&lt;/p&gt;

&lt;p&gt;The better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which starter removes the work you are least likely to differentiate on, without boxing in the work you do need to differentiate on?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If your product is a narrow one-person launch, a smaller starter can be better. Less code means fewer decisions.&lt;/p&gt;

&lt;p&gt;If your product will need teams, support operations, billing changes, admin tools, and integrations, the smallest starter can become expensive after launch. You may spend the saved setup time rebuilding the operational layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;Look at what happens when the product changes.&lt;/p&gt;

&lt;p&gt;Can you remove a module? Can you switch providers? Can you add a plugin? Can you keep the public marketing site, dashboard, admin panel, API routes, and database schema understandable?&lt;/p&gt;

&lt;p&gt;Codapult is built around a few architecture choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App Router route groups for public, auth, dashboard, and admin surfaces.&lt;/li&gt;
&lt;li&gt;Provider adapters for auth, payments, storage, jobs, notifications, embeddings, and vector search.&lt;/li&gt;
&lt;li&gt;CLI setup wizard for permanently removing unused modules before you start building.&lt;/li&gt;
&lt;li&gt;Environment variable flags for toggling kept modules on or off after setup.&lt;/li&gt;
&lt;li&gt;MDX docs and blog on disk.&lt;/li&gt;
&lt;li&gt;A plugin registry for larger optional capabilities.&lt;/li&gt;
&lt;li&gt;Infrastructure paths for Vercel, Docker, Terraform, Pulumi, and Helm.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is heavier than a minimal starter. The upside is that modules you keep are already integrated — no wiring needed when you turn them on.&lt;br&gt;
Modules you remove are gone cleanly, with no dead code left behind.&lt;/p&gt;
&lt;h2&gt;
  
  
  Starting Simple with Codapult
&lt;/h2&gt;

&lt;p&gt;Codapult ships with 70+ modules. That does not mean your project has to use all of them.&lt;/p&gt;

&lt;p&gt;The CLI setup wizard lets you keep only what your MVP needs. A typical minimal start looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auth (email/password or OAuth)&lt;/li&gt;
&lt;li&gt;Billing (one provider, one plan)&lt;/li&gt;
&lt;li&gt;Dashboard (one route group for the logged-in user)&lt;/li&gt;
&lt;li&gt;Admin (user list and subscription view)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcodapult.dev%2Fscreenshots%2Fterminal_setup.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcodapult.dev%2Fscreenshots%2Fterminal_setup.gif" alt="Codapult CLI setup wizard" width="720" height="540"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything else — teams, AI, SSO, SCIM, audit logs, white-labeling — you either remove at setup via the CLI wizard (permanent, clean), or keep in the codebase and hide via an environment variable until the product needs it.&lt;/p&gt;

&lt;p&gt;This is the difference between Codapult and a minimal starter: you are not adding missing modules after launch. You are removing modules you do not need yet.&lt;/p&gt;
&lt;h2&gt;
  
  
  Billing
&lt;/h2&gt;

&lt;p&gt;Billing is where a lot of starters look similar until you need a second pricing model.&lt;/p&gt;

&lt;p&gt;Basic subscription checkout is useful, but SaaS pricing often evolves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One-time licenses.&lt;/li&gt;
&lt;li&gt;Monthly and annual subscriptions.&lt;/li&gt;
&lt;li&gt;Seats.&lt;/li&gt;
&lt;li&gt;Add-ons.&lt;/li&gt;
&lt;li&gt;Usage credits.&lt;/li&gt;
&lt;li&gt;Trials.&lt;/li&gt;
&lt;li&gt;Customer portal.&lt;/li&gt;
&lt;li&gt;Webhooks.&lt;/li&gt;
&lt;li&gt;Admin support workflows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Codapult includes Stripe, LemonSqueezy, and Polar adapters. That matters less because of the provider names and more because billing code is not scattered across the app. The product can talk to a payment interface while the provider implementation stays behind it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Unified payment interface used across the entire application&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PaymentAdapter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;createCheckoutSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CheckoutParams&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="nf"&gt;createCustomerPortal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="nf"&gt;handleWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WebhookResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;refundPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RefundParams&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RefundResult&lt;/span&gt;&lt;span class="o"&gt;&amp;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;h2&gt;
  
  
  Database
&lt;/h2&gt;

&lt;p&gt;Database preference is personal and team-specific.&lt;/p&gt;

&lt;p&gt;Turso and Drizzle are lightweight and fast to start with. PostgreSQL is familiar, widely hosted, and often preferred by teams selling to enterprise buyers. Prisma has a huge ecosystem. Supabase gives you a bundled platform.&lt;/p&gt;

&lt;p&gt;The practical advice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pick Turso/Drizzle if you want a small TypeScript-first setup and like SQLite/libSQL ergonomics.&lt;/li&gt;
&lt;li&gt;Pick PostgreSQL if your team or customers expect it.&lt;/li&gt;
&lt;li&gt;Pick Supabase if managed auth/database/storage in one platform is the main value.&lt;/li&gt;
&lt;li&gt;Pick Prisma if your team strongly prefers its schema/client workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Codapult defaults to Turso/Drizzle but also documents PostgreSQL support through &lt;code&gt;DB_PROVIDER=postgres&lt;/code&gt;. That makes the default opinionated without making the buyer feel locked in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugins and Product Surface
&lt;/h2&gt;

&lt;p&gt;Most SaaS products do not need every module on day one.&lt;/p&gt;

&lt;p&gt;But many products eventually need one or more of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI tooling.&lt;/li&gt;
&lt;li&gt;CRM workflows.&lt;/li&gt;
&lt;li&gt;Helpdesk workflows.&lt;/li&gt;
&lt;li&gt;Email marketing.&lt;/li&gt;
&lt;li&gt;Support analytics.&lt;/li&gt;
&lt;li&gt;Admin operations.&lt;/li&gt;
&lt;li&gt;Webhooks.&lt;/li&gt;
&lt;li&gt;Feature requests.&lt;/li&gt;
&lt;li&gt;Referrals.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Codapult treats larger optional capabilities as plugins where that boundary makes sense. The goal is not to turn every helper into a package.&lt;br&gt;
The goal is to keep the core application usable while letting serious capabilities be added as plugins when the product needs them.&lt;br&gt;
Plugins are separate from the core modules you select at setup — they can be installed independently at any point after launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  When ShipFast Is the Better Choice
&lt;/h2&gt;

&lt;p&gt;Choose ShipFast-style simplicity if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are a solo founder shipping one small app.&lt;/li&gt;
&lt;li&gt;You do not expect enterprise workflows soon.&lt;/li&gt;
&lt;li&gt;You value minimal code more than operational breadth.&lt;/li&gt;
&lt;li&gt;You are comfortable adding missing modules yourself.&lt;/li&gt;
&lt;li&gt;You want a fast marketing and checkout path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a valid product strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Supastarter or MakerKit Is the Better Choice
&lt;/h2&gt;

&lt;p&gt;Choose Supastarter or MakerKit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You like their stack choices.&lt;/li&gt;
&lt;li&gt;You want a mature starter with strong conventions.&lt;/li&gt;
&lt;li&gt;Supabase, Prisma, Firebase, or a specific monorepo shape is important.&lt;/li&gt;
&lt;li&gt;You want a broad starter without Codapult's plugin-oriented packaging.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best starter is often the one that matches your team's taste and habits.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Codapult Is the Better Choice
&lt;/h2&gt;

&lt;p&gt;Choose Codapult when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want to start with auth, billing, and a dashboard — and add teams, admin, AI, or SSO only when the product needs them.&lt;/li&gt;
&lt;li&gt;You want AI, docs, blog, billing, webhooks, and infrastructure already represented.&lt;/li&gt;
&lt;li&gt;You want to remove modules via CLI rather than add missing ones from scratch after launch.&lt;/li&gt;
&lt;li&gt;You expect teams and organization-scoped data at some point.&lt;/li&gt;
&lt;li&gt;You need admin operations for support, feature flags, or billing workflows.&lt;/li&gt;
&lt;li&gt;You want swappable providers — auth, payments, storage — without rewriting app logic.&lt;/li&gt;
&lt;li&gt;You may add CRM, helpdesk, email marketing, or AI platform features later (via plugins).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Codapult is less about the shortest path to a landing page and more about avoiding the second rebuild.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision Rule
&lt;/h2&gt;

&lt;p&gt;Use the smallest starter that will survive the next version of your product.&lt;/p&gt;

&lt;p&gt;If the next version is still one user, one plan, one dashboard, and one checkout, choose a smaller starter.&lt;/p&gt;

&lt;p&gt;If the next version includes teams, operational support, provider changes, AI features, and customer-facing documentation, choose a starter that already has those boundaries.&lt;/p&gt;




&lt;p&gt;If you are building a SaaS intended to evolve beyond a simple landing page without needing a complete rewrite down the road, explore the full architectural feature matrix and documentation on the &lt;a href="https://codapult.dev/compare" rel="noopener noreferrer"&gt;Codapult comparison page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>saas</category>
      <category>webdev</category>
      <category>startup</category>
    </item>
    <item>
      <title>I Spent $200 in API Credits Prompting an Agent to Build a Starter. Here’s Why I Still Clone Boilerplates.</title>
      <dc:creator>Vlad Zoff</dc:creator>
      <pubDate>Thu, 30 Jul 2026 17:56:30 +0000</pubDate>
      <link>https://dev.to/vladzoff/i-spent-200-in-api-credits-prompting-an-agent-to-build-a-starter-heres-why-i-still-clone-2lek</link>
      <guid>https://dev.to/vladzoff/i-spent-200-in-api-credits-prompting-an-agent-to-build-a-starter-heres-why-i-still-clone-2lek</guid>
      <description>&lt;p&gt;Last month I decided to test a simple hypothesis: with top-tier models like &lt;strong&gt;Claude Opus 4.8&lt;/strong&gt; and &lt;strong&gt;GPT-5.6&lt;/strong&gt; running inside CLI agents, is the SaaS boilerplate officially dead?&lt;/p&gt;

&lt;p&gt;I opened a fresh directory, booted up an agentic loop, and asked it to scaffold a production-ready Next.js 16 app with Better-Auth, Stripe subscriptions, Tailwind v4, and Drizzle ORM.&lt;/p&gt;

&lt;p&gt;Three hours and a $180 Anthropic invoice later, I had my answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Brutal Math of Multi-File Context
&lt;/h2&gt;

&lt;p&gt;When people say "AI coding is basically free," they are usually talking about generating a single UI component or a standalone utility function. That costs pennies.&lt;/p&gt;

&lt;p&gt;That math completely falls apart the second you ask an agent to build a multi-page architecture across 30+ interconnected files.&lt;/p&gt;

&lt;p&gt;Agentic workflows don't just write code—they inspect directory trees, run terminal checks, read type definitions, and edit files across multiple passes. Every time the agent makes a tool call, runs &lt;code&gt;npm run build&lt;/code&gt;, or checks why a Server Component failed to re-render, it re-sends the entire repository context back into the model.&lt;/p&gt;

&lt;p&gt;Add thousands of reasoning tokens per pass, and a single multi-turn session to wire up auth, session cookies, and Stripe webhooks easily burns through &lt;strong&gt;$150 to $300+ in raw API credits&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You are effectively paying AI providers hundreds of dollars to have an agent repeatedly re-invent standard authentication flows from scratch.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Cohesion Problem: 70+ Modules Don't Just "Prompt" Together
&lt;/h2&gt;

&lt;p&gt;Models like Opus 4.8 and GPT-5.6 are brilliant at generating localized logic. But a production SaaS isn't a collection of isolated files—it is a tightly coupled web of dependencies.&lt;/p&gt;

&lt;p&gt;A real foundation requires 70+ modules and 30+ database tables all speaking the exact same language:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auth &amp;amp; Identity:&lt;/strong&gt; Better-Auth configured for Passkeys, 2FA, SSO, and multi-tenant Organizations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing Lifecycles:&lt;/strong&gt; Webhook handlers for both Stripe and LemonSqueezy, subscription seats, grace periods, and usage metering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System Layer:&lt;/strong&gt; Admin dashboards, granular RBAC, and i18n localization routing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you try to prompt an agent into building this graph from scratch, type drift is inevitable. The database schema slightly diverges from the session middleware; the RBAC checks miss an edge case in server actions; the payment webhooks break when a workspace ID is passed as &lt;code&gt;undefined&lt;/code&gt; instead of a string.&lt;/p&gt;

&lt;p&gt;Fixing these cross-module integration bugs requires constant back-and-forth prompts, burning through tokens just to get basic features to stop breaking each other.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Day-2 Infrastructure Trap
&lt;/h2&gt;

&lt;p&gt;Agents excel at building things that run on &lt;code&gt;localhost:3000&lt;/code&gt; via &lt;code&gt;npm run dev&lt;/code&gt;. They are notoriously unreliable when preparing code for production infrastructure.&lt;/p&gt;

&lt;p&gt;Getting a project ready for production means setting up Infrastructure as Code (IaC):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Valid &lt;strong&gt;Docker Compose&lt;/strong&gt; files for local production mirrors.&lt;/li&gt;
&lt;li&gt;Clean &lt;strong&gt;Terraform&lt;/strong&gt; or &lt;strong&gt;Pulumi&lt;/strong&gt; manifests.&lt;/li&gt;
&lt;li&gt;Ready-to-deploy &lt;strong&gt;Kubernetes Helm charts&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Multi-database flexibility, like swapping between PostgreSQL and edge-ready Turso SQLite via an environment variable (&lt;code&gt;DB_PROVIDER&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you ask an agent to generate valid Helm charts or Terraform manifests alongside app code, it frequently hallucinates configuration keys or outputs outdated syntax. Debugging a failing deployment pipeline through an AI agent can cost $30 to $50 in API compute alone, purely spent fixing minor syntax errors in YAML and HCL files.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI Code is a Dead-End Fork
&lt;/h2&gt;

&lt;p&gt;Code generated by a prompt session is a static snapshot in time. It has no lineage and no upstream repository.&lt;/p&gt;

&lt;p&gt;Six months from now, when Next.js introduces breaking API changes, Tailwind v4 releases a major patch, or security vulnerabilities surface in your auth dependencies, your AI-generated starter becomes instant technical debt. Your only recourse is to feed the whole codebase back into an agent and hope it doesn't break your custom business logic while refactoring.&lt;/p&gt;

&lt;p&gt;A purpose-built boilerplate provides a &lt;strong&gt;Git upstream remote&lt;/strong&gt;. When core security patches, framework migrations, or new features drop, you pull them directly with &lt;code&gt;git pull upstream&lt;/code&gt;. You own a living codebase backed by an active maintainer, not an unmaintainable single-shot generation.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Agent Baby-Sitting Tax
&lt;/h2&gt;

&lt;p&gt;Even if API tokens were completely free, there is the time cost.&lt;/p&gt;

&lt;p&gt;Current agents are fast, but they aren't autonomous enough for you to write one prompt and walk away while a full application builds itself. You are forced to sit at your terminal baby-sitting the process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reviewing diffs to make sure the agent didn't mess up Next.js 16's async request APIs (&lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;headers()&lt;/code&gt;, or route &lt;code&gt;params&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Approving terminal executions and watching builds fail because a webhook route missed a signature check.&lt;/li&gt;
&lt;li&gt;Guiding the model through fixing subtle hydration mismatches between server and client states.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you ever decide to customize the setup mid-build—for instance, removing multi-tenancy while keeping organization billing—asking an agent to refactor its own freshly written code usually results in broken imports and orphaned schema tables.&lt;/p&gt;

&lt;p&gt;Compare that to running an interactive CLI installer like &lt;code&gt;npx create-codapult&lt;/code&gt;, which prompts you for feature flags upfront and instantly outputs a clean, trimmed codebase in seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Save Your Compute Budget for What Actually Matters
&lt;/h2&gt;

&lt;p&gt;Top-tier models are incredible at writing complex, unique business logic that sets your product apart. Using high-reasoning compute to re-generate standard Stripe subscription lifecycles, Tailwind v4 configs, and DB schemas is a waste of capital.&lt;/p&gt;

&lt;p&gt;You’re paying premium rates for the AI equivalent of laying a brick foundation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Skip the Scaffolding Prompts
&lt;/h2&gt;

&lt;p&gt;This is exactly why I built &lt;strong&gt;&lt;a href="https://codapult.dev" rel="noopener noreferrer"&gt;Codapult&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is a clean, modular Next.js 16 SaaS starter designed to save you from prompt fatigue and context drain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complete Foundation:&lt;/strong&gt; Better-Auth (Passkeys, SSO, 2FA), Stripe &amp;amp; LemonSqueezy billing, Tailwind v4, Drizzle ORM, and i18n out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production Infrastructure:&lt;/strong&gt; Pre-configured Docker, Terraform, Kubernetes Helm charts, and flexible DB routing (PostgreSQL / Turso SQLite).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extensible Architecture:&lt;/strong&gt; Need specialized capabilities later? Drop in official modules like the &lt;strong&gt;AI Kit&lt;/strong&gt;, &lt;strong&gt;CRM&lt;/strong&gt;, or &lt;strong&gt;Helpdesk&lt;/strong&gt; without touching the core framework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintainable Lifetime:&lt;/strong&gt; Stay updated effortlessly via the Git upstream CLI workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You run &lt;code&gt;git clone&lt;/code&gt; or use &lt;code&gt;npx create-codapult&lt;/code&gt;, drop in your &lt;code&gt;.env&lt;/code&gt; variables, and start building actual features immediately.&lt;/p&gt;

&lt;p&gt;Save your high-tier tokens for your core product.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://codapult.dev" rel="noopener noreferrer"&gt;Codapult&lt;/a&gt; includes a complete, production-ready Next.js 16 stack out of the box — Better-Auth, Stripe/LemonSqueezy billing lifecycles, Tailwind v4, Drizzle ORM, IaC configs, and modular plugins — saving you hundreds of dollars in API compute and hours of agent orchestration. Explore the architecture in the &lt;a href="https://codapult.dev/docs" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How are you balancing agentic workflows with pre-built boilerplate architecture in your projects? Are you fully prompting, or cloning first?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>nextjs</category>
      <category>saas</category>
    </item>
    <item>
      <title>SaaS Billing Models: Subscriptions, Seats, Credits, and Add-ons</title>
      <dc:creator>Vlad Zoff</dc:creator>
      <pubDate>Tue, 07 Jul 2026 12:12:24 +0000</pubDate>
      <link>https://dev.to/vladzoff/saas-billing-models-subscriptions-seats-credits-and-add-ons-39db</link>
      <guid>https://dev.to/vladzoff/saas-billing-models-subscriptions-seats-credits-and-add-ons-39db</guid>
      <description>&lt;p&gt;Billing is product strategy expressed in code. If your starter only handles a single checkout link, you will rebuild it as soon as pricing changes.&lt;/p&gt;

&lt;p&gt;This article breaks down the billing models a SaaS foundation should support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flat Subscription
&lt;/h2&gt;

&lt;p&gt;The simplest model is one plan, one recurring price.&lt;/p&gt;

&lt;p&gt;Use it when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The product has a clear single package.&lt;/li&gt;
&lt;li&gt;Usage does not vary much across customers.&lt;/li&gt;
&lt;li&gt;Support and infrastructure costs are predictable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Implementation needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checkout session.&lt;/li&gt;
&lt;li&gt;Subscription created webhook.&lt;/li&gt;
&lt;li&gt;Subscription updated webhook.&lt;/li&gt;
&lt;li&gt;Subscription canceled webhook.&lt;/li&gt;
&lt;li&gt;Customer portal.&lt;/li&gt;
&lt;li&gt;Plan state in the database.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tiered Plans
&lt;/h2&gt;

&lt;p&gt;Most SaaS products need at least two paid tiers.&lt;/p&gt;

&lt;p&gt;Typical differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Feature access.&lt;/li&gt;
&lt;li&gt;Usage limits.&lt;/li&gt;
&lt;li&gt;Support level.&lt;/li&gt;
&lt;li&gt;Number of seats.&lt;/li&gt;
&lt;li&gt;Data retention.&lt;/li&gt;
&lt;li&gt;Integrations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Store &lt;code&gt;planId&lt;/code&gt; in your subscription table and map it to a local plan catalog. Do not rely on provider price IDs as your product logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seat-Based Billing
&lt;/h2&gt;

&lt;p&gt;Seat billing is common for B2B SaaS. It aligns price with customer team size.&lt;/p&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current seat count.&lt;/li&gt;
&lt;li&gt;Optional max seat cap.&lt;/li&gt;
&lt;li&gt;Member invite logic.&lt;/li&gt;
&lt;li&gt;Checkout or subscription update flow.&lt;/li&gt;
&lt;li&gt;Billing UI that explains renewal impact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzt8d7l8b2micbaki8n4g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzt8d7l8b2micbaki8n4g.png" alt="Billing screen" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage Credits
&lt;/h2&gt;

&lt;p&gt;Usage credits work well for AI, API calls, file processing, or metered workflows.&lt;/p&gt;

&lt;p&gt;Track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total credits.&lt;/li&gt;
&lt;li&gt;Used credits.&lt;/li&gt;
&lt;li&gt;Reset date.&lt;/li&gt;
&lt;li&gt;Usage events.&lt;/li&gt;
&lt;li&gt;Budget alerts.&lt;/li&gt;
&lt;li&gt;Overage policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Credits should be visible to the customer before they become a billing surprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add-ons
&lt;/h2&gt;

&lt;p&gt;Add-ons let you expand revenue without forcing every customer into a higher tier.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extra storage.&lt;/li&gt;
&lt;li&gt;White-labeling.&lt;/li&gt;
&lt;li&gt;Priority support.&lt;/li&gt;
&lt;li&gt;Advanced analytics.&lt;/li&gt;
&lt;li&gt;Additional AI credits.&lt;/li&gt;
&lt;li&gt;Premium integrations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your checkout and subscription state need to support multiple line items, not just one plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trials
&lt;/h2&gt;

&lt;p&gt;Trials are useful when activation requires product usage. They are less useful when setup is complex and a sales conversation is needed.&lt;/p&gt;

&lt;p&gt;If you support trials, store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trial end date.&lt;/li&gt;
&lt;li&gt;Trial status.&lt;/li&gt;
&lt;li&gt;Conversion events.&lt;/li&gt;
&lt;li&gt;Reminder schedule.&lt;/li&gt;
&lt;li&gt;Cancellation behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  One-Time Purchases
&lt;/h2&gt;

&lt;p&gt;Some developer products sell lifetime access or source code licenses. That model needs different state than recurring SaaS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;License tier.&lt;/li&gt;
&lt;li&gt;Project count.&lt;/li&gt;
&lt;li&gt;Update period.&lt;/li&gt;
&lt;li&gt;Support period.&lt;/li&gt;
&lt;li&gt;Delivery workflow.&lt;/li&gt;
&lt;li&gt;Receipt and access email.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not mix one-time license logic into recurring subscription code without clear types.&lt;/p&gt;

&lt;h2&gt;
  
  
  Webhooks Are the Source of Truth
&lt;/h2&gt;

&lt;p&gt;Checkout success pages are not enough. Provider webhooks should update your database.&lt;/p&gt;

&lt;p&gt;At minimum, handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checkout completed.&lt;/li&gt;
&lt;li&gt;Subscription created.&lt;/li&gt;
&lt;li&gt;Subscription updated.&lt;/li&gt;
&lt;li&gt;Subscription deleted.&lt;/li&gt;
&lt;li&gt;Payment failed.&lt;/li&gt;
&lt;li&gt;Refund.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Log webhook delivery and errors. Silent billing failures are expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Admin Billing Support
&lt;/h2&gt;

&lt;p&gt;Your admin panel should show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User name and email.&lt;/li&gt;
&lt;li&gt;Provider.&lt;/li&gt;
&lt;li&gt;Plan ID.&lt;/li&gt;
&lt;li&gt;Status.&lt;/li&gt;
&lt;li&gt;Seat count.&lt;/li&gt;
&lt;li&gt;Renewal date.&lt;/li&gt;
&lt;li&gt;Cancellation state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Support should not need direct database access to answer a billing question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick the Model You Can Explain
&lt;/h2&gt;

&lt;p&gt;The best billing model is not the most sophisticated one. It is the one customers understand and your system can enforce.&lt;/p&gt;

&lt;p&gt;Start with the simplest model that matches value, but choose a codebase that can add seats, credits, add-ons, and trials when your pricing evolves.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://codapult.dev" rel="noopener noreferrer"&gt;Codapult&lt;/a&gt; ships billing adapters for Stripe, LemonSqueezy, and Polar with normalized webhook handling, subscription state in the database, seat tracking, usage credits, and a customer portal — switchable via &lt;code&gt;PAYMENT_PROVIDER&lt;/code&gt; without touching application code. The &lt;a href="https://codapult.dev/docs/payments/overview" rel="noopener noreferrer"&gt;payments docs&lt;/a&gt; show the integration points.&lt;/p&gt;

</description>
      <category>billing</category>
      <category>stripe</category>
      <category>lemonsqueezy</category>
      <category>saas</category>
    </item>
    <item>
      <title>Auth, Teams, and RBAC in a Next.js SaaS App</title>
      <dc:creator>Vlad Zoff</dc:creator>
      <pubDate>Tue, 07 Jul 2026 12:07:58 +0000</pubDate>
      <link>https://dev.to/vladzoff/auth-teams-and-rbac-in-a-nextjs-saas-app-2mla</link>
      <guid>https://dev.to/vladzoff/auth-teams-and-rbac-in-a-nextjs-saas-app-2mla</guid>
      <description>&lt;p&gt;Authentication gets users into the app. Authorization decides what they can do once they are there. In SaaS products, the second part is where many starters are too thin.&lt;/p&gt;

&lt;p&gt;This guide covers the practical model: platform roles, organization roles, protected routes, admin access, and future enterprise needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the User Record
&lt;/h2&gt;

&lt;p&gt;Keep the user record to identity only — id, name, email, verification state, avatar, platform role, 2FA state, timestamps. Nothing organization-specific belongs here, because the same person can be an owner in one workspace and a viewer in another. The moment you store a permission field directly on the user, you've hard-coded an assumption that breaks the first time someone joins a second organization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add Platform Roles
&lt;/h2&gt;

&lt;p&gt;Platform roles are global. Most apps only need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;user&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;admin&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The admin role controls platform screens such as &lt;code&gt;/admin/users&lt;/code&gt;, &lt;code&gt;/admin/subscriptions&lt;/code&gt;, and &lt;code&gt;/admin/feature-flags&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Do not use platform admin for customer workspace permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add Organization Memberships
&lt;/h2&gt;

&lt;p&gt;Organization roles should answer what a user can do inside one workspace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;owner&lt;/code&gt;: billing, members, settings, deletion.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;admin&lt;/code&gt;: most management actions except ownership transfer.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;member&lt;/code&gt;: normal product usage.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;viewer&lt;/code&gt;: read-only access.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives you enough granularity without creating a complex permission system too early.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxelz780kqmmayku9eg1a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxelz780kqmmayku9eg1a.png" alt="Organization Memberships" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Protect Routes at Multiple Layers
&lt;/h2&gt;

&lt;p&gt;Route protection should happen in more than one place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Middleware or proxy redirects unauthenticated users.&lt;/li&gt;
&lt;li&gt;Layouts block protected app areas.&lt;/li&gt;
&lt;li&gt;Server actions verify the session.&lt;/li&gt;
&lt;li&gt;API routes verify the session.&lt;/li&gt;
&lt;li&gt;Admin operations verify platform role.&lt;/li&gt;
&lt;li&gt;Organization operations verify membership and org role.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The UI can hide buttons, but the server must enforce permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Auth Pages Out of Search
&lt;/h2&gt;

&lt;p&gt;Sign-in, sign-up, dashboard, and admin pages should not be indexed. They are not acquisition pages, and search engines do not need them.&lt;/p&gt;

&lt;p&gt;Use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;robots&lt;/code&gt; metadata with &lt;code&gt;index: false&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;robots.txt&lt;/code&gt; disallow rules for protected paths.&lt;/li&gt;
&lt;li&gt;Sitemap exclusion for auth-only pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps SEO focus on marketing, docs, blog, pricing, and comparison pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plan for Enterprise Auth Without Building It Too Early
&lt;/h2&gt;

&lt;p&gt;Enterprise customers often ask for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SAML SSO.&lt;/li&gt;
&lt;li&gt;SCIM provisioning.&lt;/li&gt;
&lt;li&gt;Enforced 2FA.&lt;/li&gt;
&lt;li&gt;Audit logs.&lt;/li&gt;
&lt;li&gt;Domain verification.&lt;/li&gt;
&lt;li&gt;Custom session policies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not need to ship every enterprise feature on day one. You do need a data model that can add them without rewriting auth and teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Support Impersonation Carefully
&lt;/h2&gt;

&lt;p&gt;Impersonation is powerful for support, but it is sensitive.&lt;/p&gt;

&lt;p&gt;Rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only platform admins can impersonate.&lt;/li&gt;
&lt;li&gt;Show a visible impersonation banner.&lt;/li&gt;
&lt;li&gt;Log the impersonation start and end.&lt;/li&gt;
&lt;li&gt;Never let impersonation bypass destructive confirmation steps silently.&lt;/li&gt;
&lt;li&gt;Avoid using impersonation for billing provider actions when possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Test the Permission Matrix
&lt;/h2&gt;

&lt;p&gt;Write tests for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anonymous user access.&lt;/li&gt;
&lt;li&gt;Authenticated user access.&lt;/li&gt;
&lt;li&gt;Admin-only routes.&lt;/li&gt;
&lt;li&gt;Owner-only organization settings.&lt;/li&gt;
&lt;li&gt;Member actions.&lt;/li&gt;
&lt;li&gt;Viewer restrictions.&lt;/li&gt;
&lt;li&gt;Locale-prefixed protected routes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Auth regressions are high-risk because they often expose data, not just break UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Practical Baseline
&lt;/h2&gt;

&lt;p&gt;None of this is exotic. A real auth provider, protected routes at every layer, a platform admin role, an organization membership table with roles, server-side permission checks, audit logging, and noindex on anything that isn't marketing. That's the baseline that survives contact with a real customer — not an enterprise IAM project, just enough structure that adding SSO later doesn't mean rewriting how permissions work.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://codapult.dev" rel="noopener noreferrer"&gt;Codapult&lt;/a&gt; includes this full auth and teams layer out of the box — email/password, OAuth, magic links, TOTP 2FA, SAML SSO, organizations, RBAC, invitations, protected routes and audit logs — switchable between Better-Auth and Kinde via one environment variable. Explore the implementation map in the &lt;a href="https://codapult.dev/docs/authentication/overview" rel="noopener noreferrer"&gt;authentication docs&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>auth</category>
      <category>teams</category>
      <category>rbac</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Designing Removable Modules in a Next.js SaaS Starter</title>
      <dc:creator>Vlad Zoff</dc:creator>
      <pubDate>Tue, 07 Jul 2026 12:03:15 +0000</pubDate>
      <link>https://dev.to/vladzoff/designing-removable-modules-in-a-nextjs-saas-starter-1n87</link>
      <guid>https://dev.to/vladzoff/designing-removable-modules-in-a-nextjs-saas-starter-1n87</guid>
      <description>&lt;p&gt;Most SaaS starters say they are modular.&lt;/p&gt;

&lt;p&gt;The useful question is: what happens when you remove a module?&lt;/p&gt;

&lt;p&gt;If disabling a feature leaves dead navigation links, broken sitemap entries, missing imports, route handlers that still accept requests, or database tables nobody understands, the module was not really modular. It was just grouped in a folder.&lt;/p&gt;

&lt;p&gt;Removable modules need boundaries.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6rlgevh5sdpkejv5tae6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6rlgevh5sdpkejv5tae6.png" alt="Module architecture docs" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Start With User-Facing Surfaces
&lt;/h2&gt;

&lt;p&gt;A module usually shows up in more places than its main page suggests. Take a waitlist feature: there's the public signup route, sure, but also an admin view, an API route, a database table, an email template, a navbar link, a footer link, a sitemap entry. Delete the public page and the feature is still there — just invisible until someone hits the API route directly or wonders why waitlist_entry is sitting in the schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature Flags Are Not Enough
&lt;/h2&gt;

&lt;p&gt;Feature flags hide behavior at runtime.&lt;/p&gt;

&lt;p&gt;They do not remove code.&lt;/p&gt;

&lt;p&gt;That distinction matters. Runtime flags are useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hiding navigation.&lt;/li&gt;
&lt;li&gt;Returning 404 for disabled routes.&lt;/li&gt;
&lt;li&gt;Removing sitemap entries.&lt;/li&gt;
&lt;li&gt;Preventing API access.&lt;/li&gt;
&lt;li&gt;Running one codebase in multiple modes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But if a buyer wants a smaller codebase, the setup tool also needs to delete files and update references.&lt;/p&gt;

&lt;p&gt;A good starter supports both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Feature flags for runtime control.&lt;/li&gt;
&lt;li&gt;Pruning for permanent removal.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Route Groups Help
&lt;/h2&gt;

&lt;p&gt;Next.js App Router route groups are useful for keeping surfaces understandable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/app/[locale]/
  (marketing)/
  (auth)/
  (dashboard)/
  admin/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That structure makes it clear which pages are public, protected, authentication-only, or platform admin.&lt;/p&gt;

&lt;p&gt;It also lets you reason about SEO. Marketing pages are indexable. Auth, dashboard, and admin pages should be noindex and blocked where appropriate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Shared Code Honest
&lt;/h2&gt;

&lt;p&gt;The hardest modules to remove are the ones that leak into shared files.&lt;/p&gt;

&lt;p&gt;Common leak points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;navigation.ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sitemap.ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;robots.ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;proxy.ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Shared schema files.&lt;/li&gt;
&lt;li&gt;Shared validation schemas.&lt;/li&gt;
&lt;li&gt;Shared server actions.&lt;/li&gt;
&lt;li&gt;Shared dashboard layouts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a module needs entries in those files, make the entries obvious and grouped. If a setup wizard removes the module, it should know exactly what to prune.&lt;/p&gt;

&lt;h2&gt;
  
  
  API Routes Need Gates Too
&lt;/h2&gt;

&lt;p&gt;Disabled UI is not security.&lt;/p&gt;

&lt;p&gt;If a module has API routes, those routes need their own gates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auth check.&lt;/li&gt;
&lt;li&gt;Permission check.&lt;/li&gt;
&lt;li&gt;Rate limit.&lt;/li&gt;
&lt;li&gt;Feature flag check.&lt;/li&gt;
&lt;li&gt;Input validation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The proxy can return 404 for disabled feature routes, but route handlers should still follow the same protected pattern. That keeps modules safe when moved, copied, or extended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Database Tables Should Be Named by Domain
&lt;/h2&gt;

&lt;p&gt;Module data should be easy to identify.&lt;/p&gt;

&lt;p&gt;Good table names:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;waitlist_entry&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;feature_request&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;audit_log&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outgoing_webhook&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;workflow&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ambiguous table names make removal riskier. If the table name tells you the module, migration work is easier to review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests Should Prove Removal Boundaries
&lt;/h2&gt;

&lt;p&gt;Useful tests include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disabled feature returns 404.&lt;/li&gt;
&lt;li&gt;Disabled feature is absent from sitemap.&lt;/li&gt;
&lt;li&gt;Disabled feature is absent from navigation.&lt;/li&gt;
&lt;li&gt;API route rejects unauthenticated access.&lt;/li&gt;
&lt;li&gt;Module-specific queries still work when enabled.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not need massive coverage for every module. You need enough tests to prevent obvious leaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugins Are for Bigger Boundaries
&lt;/h2&gt;

&lt;p&gt;Not every removable feature should be a plugin.&lt;/p&gt;

&lt;p&gt;A simple waitlist page can be a module.&lt;/p&gt;

&lt;p&gt;A CRM with pages, routes, hooks, schema, components, AI helpers, and install commands is a better plugin candidate.&lt;/p&gt;

&lt;p&gt;Use plugins when the feature has its own lifecycle and customer value. Use modules when the feature is part of the core application shape.&lt;/p&gt;

&lt;p&gt;None of this needs to be exhaustive. It needs to be enough that "modular" describes the codebase instead of describing the marketing copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Payoff
&lt;/h2&gt;

&lt;p&gt;Removable modules give buyers confidence.&lt;/p&gt;

&lt;p&gt;They can start broad, then strip what they do not need. Or they can keep a larger operational surface without building it themselves.&lt;/p&gt;

&lt;p&gt;The codebase stays understandable because module boundaries are explicit.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://codapult.dev" rel="noopener noreferrer"&gt;Codapult&lt;/a&gt; documents removable features in the &lt;a href="https://codapult.dev/docs/modules/overview" rel="noopener noreferrer"&gt;modules overview&lt;/a&gt;, including routes, dependencies, and manual removal notes.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>architecture</category>
      <category>typescript</category>
      <category>saas</category>
    </item>
    <item>
      <title>Turso + Drizzle vs PostgreSQL + Prisma for a SaaS Boilerplate</title>
      <dc:creator>Vlad Zoff</dc:creator>
      <pubDate>Tue, 07 Jul 2026 12:01:23 +0000</pubDate>
      <link>https://dev.to/vladzoff/turso-drizzle-vs-postgresql-prisma-for-a-saas-boilerplate-4pmb</link>
      <guid>https://dev.to/vladzoff/turso-drizzle-vs-postgresql-prisma-for-a-saas-boilerplate-4pmb</guid>
      <description>&lt;p&gt;Database choices in SaaS starters are more emotional than they should be.&lt;/p&gt;

&lt;p&gt;Turso, PostgreSQL, Drizzle, Prisma, Supabase, Neon, and SQLite all have real strengths. The right choice depends on what the starter optimizes for: local setup, operational familiarity, edge reads, schema control, ecosystem, or enterprise expectations.&lt;/p&gt;

&lt;p&gt;This is not a purity argument. It is a trade-off map.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turso + Drizzle
&lt;/h2&gt;

&lt;p&gt;Turso is hosted libSQL, a SQLite-compatible database with cloud features. Drizzle is a lightweight TypeScript ORM with explicit schema definitions and SQL-like query builders.&lt;/p&gt;

&lt;p&gt;The appeal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple local development with &lt;code&gt;file:local.db&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Fast setup for new projects.&lt;/li&gt;
&lt;li&gt;Lightweight query layer.&lt;/li&gt;
&lt;li&gt;TypeScript-first schema and query types.&lt;/li&gt;
&lt;li&gt;Good fit for many SaaS workloads that do not need heavyweight relational operations on day one.&lt;/li&gt;
&lt;li&gt;Multi-region read replica story through Turso.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some teams are less familiar with SQLite/libSQL in production.&lt;/li&gt;
&lt;li&gt;PostgreSQL has a larger ecosystem.&lt;/li&gt;
&lt;li&gt;Enterprise buyers may ask for Postgres by default.&lt;/li&gt;
&lt;li&gt;Some third-party tools assume Postgres.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Turso + Drizzle is a strong default when you value small setup and explicit TypeScript code.&lt;/p&gt;

&lt;h2&gt;
  
  
  PostgreSQL + Prisma
&lt;/h2&gt;

&lt;p&gt;PostgreSQL is the familiar production default for many SaaS teams. Prisma is widely adopted and has a comfortable schema/client workflow.&lt;/p&gt;

&lt;p&gt;The appeal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Very familiar to engineering teams.&lt;/li&gt;
&lt;li&gt;Huge hosting ecosystem: Neon, Supabase, RDS, Railway, Render, Fly, and more.&lt;/li&gt;
&lt;li&gt;Strong relational feature set.&lt;/li&gt;
&lt;li&gt;Many analytics, BI, and integration tools support it directly.&lt;/li&gt;
&lt;li&gt;Prisma has excellent discoverability and a large community.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prisma can add more abstraction than some TypeScript teams want.&lt;/li&gt;
&lt;li&gt;Generated clients and migrations introduce their own workflow.&lt;/li&gt;
&lt;li&gt;Local setup can be heavier than an SQLite file.&lt;/li&gt;
&lt;li&gt;Switching away from Prisma later is rarely free.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PostgreSQL + Prisma is a strong default when team familiarity and ecosystem matter more than minimal setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  PostgreSQL + Drizzle
&lt;/h2&gt;

&lt;p&gt;This middle path is increasingly attractive precisely because it refuses to pick a side: you get PostgreSQL's familiar hosting story and Drizzle's thinner query layer. The trade-off is that you still need a real Postgres instance for local dev — no single-file convenience — and Drizzle's ecosystem, while growing, is smaller than Prisma's. For teams that think in SQL, this is often the most pragmatic option on the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a SaaS Starter Should Do
&lt;/h2&gt;

&lt;p&gt;A starter should make the default easy without pretending it is universal.&lt;/p&gt;

&lt;p&gt;The docs should answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What database is default?&lt;/li&gt;
&lt;li&gt;How do I run locally without a cloud account?&lt;/li&gt;
&lt;li&gt;How do migrations work before and after production data exists?&lt;/li&gt;
&lt;li&gt;Can I use PostgreSQL?&lt;/li&gt;
&lt;li&gt;If PostgreSQL is supported, which env vars change?&lt;/li&gt;
&lt;li&gt;Which schema file is the source of truth?&lt;/li&gt;
&lt;li&gt;What are the trade-offs?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvuq3oorpdgktulw6v4dq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvuq3oorpdgktulw6v4dq.png" alt="Database docs" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The worst answer is silence. Buyers interpret silence as lock-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Workflow Matters
&lt;/h2&gt;

&lt;p&gt;For a fresh database, &lt;code&gt;db:push&lt;/code&gt; is convenient.&lt;/p&gt;

&lt;p&gt;For production data, generated migrations are safer.&lt;/p&gt;

&lt;p&gt;That distinction should be explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Fresh local database&lt;/span&gt;
pnpm db:push

&lt;span class="c"&gt;# Production changes&lt;/span&gt;
pnpm db:generate
pnpm db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most SaaS teams can start fast and become stricter when real data appears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Query Code Should Stay Boring
&lt;/h2&gt;

&lt;p&gt;Regardless of provider, application code should import one database client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/db&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;The app should not know whether the active provider is Turso or PostgreSQL. Provider selection belongs in database initialization, not in every route handler.&lt;/p&gt;

&lt;p&gt;That boundary matters more than the default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Practical Recommendation
&lt;/h2&gt;

&lt;p&gt;If fast local setup and multi-region reads matter most, start with Turso + Drizzle. If your team already knows Postgres, or your customers expect it, use Postgres — with Prisma if you want the generated-client ergonomics, or Drizzle if you'd rather write something closer to SQL. None of these is objectively correct. The mature answer isn't "this database is best" — it's picking a sensible default and documenting the escape hatch.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://codapult.dev" rel="noopener noreferrer"&gt;Codapult&lt;/a&gt; defaults to Turso + Drizzle and documents PostgreSQL via &lt;code&gt;DB_PROVIDER=postgres&lt;/code&gt;; the &lt;a href="https://codapult.dev/docs/database/overview" rel="noopener noreferrer"&gt;database docs&lt;/a&gt; cover setup and migration paths.&lt;/p&gt;

</description>
      <category>database</category>
      <category>typescript</category>
      <category>postgres</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Payment Adapter Pattern in Next.js: Stripe, LemonSqueezy, and Polar Without App-Wide Rewrites</title>
      <dc:creator>Vlad Zoff</dc:creator>
      <pubDate>Tue, 07 Jul 2026 11:33:39 +0000</pubDate>
      <link>https://dev.to/vladzoff/the-payment-adapter-pattern-in-nextjs-stripe-lemonsqueezy-and-polar-without-app-wide-rewrites-1h1p</link>
      <guid>https://dev.to/vladzoff/the-payment-adapter-pattern-in-nextjs-stripe-lemonsqueezy-and-polar-without-app-wide-rewrites-1h1p</guid>
      <description>&lt;p&gt;Most SaaS apps start with one payment provider.&lt;/p&gt;

&lt;p&gt;That is fine. The mistake is letting that provider become the architecture.&lt;/p&gt;

&lt;p&gt;If Stripe-specific objects leak into pages, server actions, admin tools, and webhook handlers, switching to LemonSqueezy or Polar becomes a rewrite. Even adding one-time products next to subscriptions can become awkward.&lt;/p&gt;

&lt;p&gt;The payment adapter pattern keeps provider details behind a small interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shape of the Problem
&lt;/h2&gt;

&lt;p&gt;Payment code tends to sprawl fast: pricing CTAs, authenticated checkout, one-time marketing checkout, billing portal links, webhooks, refunds, seat changes, usage credits. Import a provider SDK directly in even half of these, and every feature becomes provider-aware — which means every provider migration becomes an app-wide grep.&lt;/p&gt;

&lt;p&gt;If each layer imports a provider SDK directly, every feature becomes provider-aware.&lt;/p&gt;

&lt;p&gt;That makes the code harder to test and harder to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Small Interface Is Enough
&lt;/h2&gt;

&lt;p&gt;You do not need a large abstraction. Start with the operations the app actually performs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PaymentAdapter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;createCheckout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CheckoutParams&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;createMarketingCheckout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MarketingCheckoutParams&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;createPortalSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;handleWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WebhookResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;refundPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RefundParams&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RefundResult&lt;/span&gt;&lt;span class="o"&gt;&amp;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;p&gt;The rest of the app imports &lt;code&gt;getPaymentAdapter()&lt;/code&gt; instead of &lt;code&gt;stripe&lt;/code&gt;, &lt;code&gt;lemonsqueezy&lt;/code&gt;, or &lt;code&gt;polar&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adapter&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;getPaymentAdapter&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;checkoutUrl&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;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCheckout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;planId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;monthly&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;seats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;customerId&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;That one boundary gives you room to change providers without changing every caller.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Plans Provider-Neutral
&lt;/h2&gt;

&lt;p&gt;Plan definitions should describe your product, not your provider.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plans&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;monthly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;yearly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;190&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unlimited&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;aiCredits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Provider-specific IDs belong in environment variables or provider mapping code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PAYMENT_PROVIDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"stripe"&lt;/span&gt;
&lt;span class="nv"&gt;STRIPE_PRICE_PRO_MONTHLY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"price_..."&lt;/span&gt;

&lt;span class="c"&gt;# or&lt;/span&gt;
&lt;span class="nv"&gt;PAYMENT_PROVIDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"polar"&lt;/span&gt;
&lt;span class="nv"&gt;CHECKOUT_VARIANT_PRO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets the app reason about &lt;code&gt;pro&lt;/code&gt; while the adapter handles provider IDs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Marketing Checkout Is Different
&lt;/h2&gt;

&lt;p&gt;Authenticated SaaS checkout usually attaches a customer, organization, subscription, and return URL.&lt;/p&gt;

&lt;p&gt;Marketing checkout may sell source-code licenses or plugins before the buyer has an account.&lt;/p&gt;

&lt;p&gt;That path needs its own method:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh12s78xtu0k3pvjdpb58.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh12s78xtu0k3pvjdpb58.png" alt="Pricing" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMarketingCheckout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;productKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;plugin-ai-kit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;successUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;cancelUrl&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;The pricing page can route to &lt;code&gt;/api/checkout?product=plugin-ai-kit&lt;/code&gt;, and the route can call the active adapter.&lt;/p&gt;

&lt;p&gt;If you prefer direct hosted checkout URLs, keep those as the simpler path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;CHECKOUT_URL_PLUGIN_AI_KIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://provider.example/buy/plugin-ai-kit"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;a href="https://codapult.dev" rel="noopener noreferrer"&gt;Codapult&lt;/a&gt;, &lt;code&gt;CHECKOUT_VARIANT_*&lt;/code&gt; wins over &lt;code&gt;CHECKOUT_URL_*&lt;/code&gt;, so API checkout is used when a provider variant exists and direct links remain available as fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Webhooks Need Normalized Events
&lt;/h2&gt;

&lt;p&gt;Provider webhooks are not interchangeable.&lt;/p&gt;

&lt;p&gt;Stripe, LemonSqueezy, and Polar use different event names, payload shapes, signature headers, and product identifiers. The adapter should normalize those into app-level events.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;WebhookResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subscription.updated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;subscriptionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subscription.deleted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;subscriptionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment.refunded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;paymentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ignored&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;The route validates rate limits and signature, then passes the normalized result to application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Not to Abstract
&lt;/h2&gt;

&lt;p&gt;Do not hide every provider feature.&lt;/p&gt;

&lt;p&gt;Stripe Connect, LemonSqueezy licensing, and Polar's Merchant of Record model are not identical. If you need a provider-specific feature, keep it explicit at the edge of the adapter.&lt;/p&gt;

&lt;p&gt;Good adapters remove duplication. Bad adapters pretend different products are the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Payoff
&lt;/h2&gt;

&lt;p&gt;With this boundary in place, pages don't import provider SDKs, webhook routes normalize events before they hit application code, and tests mock one interface instead of three. Switching providers becomes a config change and an adapter update — not a rewrite.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://codapult.dev" rel="noopener noreferrer"&gt;Codapult&lt;/a&gt; includes Stripe, LemonSqueezy, and Polar payment adapters; the &lt;a href="https://codapult.dev/docs/payments/overview" rel="noopener noreferrer"&gt;payments docs&lt;/a&gt; show the provider switch and checkout paths.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>typescript</category>
      <category>stripe</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
