[{"data":1,"prerenderedAt":508},["ShallowReactive",2],{"\u002Fblog\u002Fstripe-saas-step-by-step-guide-nuxt-builders-data":3},{"post":4,"surround":497},{"id":5,"title":6,"alternates":7,"authors":8,"badge":14,"body":16,"date":455,"dateModified":456,"description":26,"extension":457,"head":456,"hero_image_url":25,"json_ld":458,"meta":480,"navigation":481,"ogImage":456,"outbound_links":482,"path":483,"primary_keyword":484,"related_articles":485,"robots":456,"schemaOrg":456,"search_intent":486,"seo":487,"sitemap":488,"stem":489,"supporting_keywords":490,"tags":495,"__hash__":496},"blog_en\u002Fblog\u002Fstripe-saas-step-by-step-guide-nuxt-builders.md","Stripe SaaS: A Step-by-Step Guide for Nuxt Builders",[],[9],{"name":10,"to":11,"avatar":12},"Tom Han","https:\u002F\u002Fx.com\u002Ftomhan245",{"src":13},"https:\u002F\u002Fcdn.shipahe.ad\u002Ftomhan.webp",{"label":15},"How-to",{"type":17,"value":18,"toc":429},"minimark",[19,28,32,35,40,43,90,93,97,100,105,112,115,119,122,126,129,155,162,166,169,183,186,190,193,215,218,222,227,230,233,237,240,251,260,263,274,277,288,291,294,298,301,304,315,319,351,355,372,416],[20,21,22],"figure",{},[23,24],"img",{"src":25,"alt":26,"style":27},"https:\u002F\u002Fshipahe.ad\u002Fimages\u002Fblog\u002Fstripe-saas-step-by-step-guide-nuxt-builders\u002Fpost-736.webp","Build a Stripe SaaS with a Nuxt starter kit: auth, checkout, subscriptions, webhooks, and billing UX. Step-by-step guide to ship fast with confidence.","max-width:100%;border-radius:12px",[29,30,31],"p",{},"You have a working idea and a Nuxt app taking shape, but recurring payments, access control, and onboarding keep slipping the launch date. This is the practical Stripe + Nuxt path I use to get a paid product live without gluing together half a dozen billing widgets.",[29,33,34],{},"The same flow works whether you sell an AI feature set, a dashboard, or a narrow workflow tool. Stripe handles money and invoices. Your Nuxt app handles identity, entitlements, and UI. A good Nuxt SaaS boilerplate (like the one we ship at shipahe.ad) removes the busywork so you can focus on where your product is unique.",[36,37,39],"h2",{"id":38},"what-were-building","What we’re building",[29,41,42],{},"A production Stripe SaaS on top of a Vue\u002FNuxt starter kit that ships the unglamorous, critical pieces:",[44,45,46,54,60,66,72,78,84],"ul",{},[47,48,49,53],"li",{},[50,51,52],"strong",{},"Authentication"," with protected routes.",[47,55,56,59],{},[50,57,58],{},"Checkout"," for one-time and recurring plans.",[47,61,62,65],{},[50,63,64],{},"Webhooks"," as the source of truth for billing state.",[47,67,68,71],{},[50,69,70],{},"Entitlements"," that gate features, seats, and limits.",[47,73,74,77],{},[50,75,76],{},"Billing UX"," with a clear account page, receipts, and self-serve changes.",[47,79,80,83],{},[50,81,82],{},"Localization + SEO"," so your pricing and emails work in more than one market.",[47,85,86,89],{},[50,87,88],{},"Admin + analytics"," to operate after launch, not just demo.",[29,91,92],{},"If you buy a Nuxt boilerplate, you also get a prebuilt landing page, typed ORM models, cron scaffolds, and S3-compatible storage, all wired to run locally and deploy with one command. That is weeks saved.",[36,94,96],{"id":95},"implement-stripe-in-nuxt-the-essential-wiring","Implement Stripe in Nuxt: the essential wiring",[29,98,99],{},"Stripe gives you reliable payments. Your job is to line up three flows: create checkout, confirm via webhook, and gate features in the app. Here is the concrete version.",[101,102,104],"h3",{"id":103},"_1-configure-environment-keys","1) Configure environment keys",[106,107,108],"pre",{},[109,110,111],"code",{},"# .env\nSTRIPE_SECRET_KEY=sk_test_...\nSTRIPE_WEBHOOK_SECRET=whsec_...\nPUBLIC_PRICE_BASIC=price_123\nPUBLIC_PRICE_PRO=price_456\nBASE_URL=https:\u002F\u002Fyourapp.com\n",[29,113,114],{},"Keep provider keys in environment variables and load them via runtime config. Store the public price IDs for your plans so the client can start a session without exposing secrets.",[101,116,118],{"id":117},"_2-create-products-and-prices","2) Create Products and Prices",[29,120,121],{},"In Stripe Dashboard (test mode), create one Product per plan and add monthly and annual Prices. If you need add-ons, create one-time Prices. Copy the price IDs into your .env. Name objects clearly: “Pro Monthly” and “Pro Annual” are easier to audit than “Plan A.”",[101,123,125],{"id":124},"_3-start-a-checkout-session","3) Start a Checkout Session",[29,127,128],{},"Expose a server endpoint that takes a price ID and returns a Stripe Checkout Session URL. Keep it server-side so you can attach the authenticated user.",[106,130,131,150],{},[109,132,133,134],{},"\u002F\u002F server\u002Fapi\u002Fbilling\u002Fcreate-checkout.post.ts\nimport Stripe from 'stripe'\nexport default defineEventHandler(async (event) => {\n  const user = await requireUser(event) \u002F\u002F from your auth layer\n  const body = await readBody(event)\n  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2024-06-20' })\n",[29,135,136,137,141,142,145,146,149],{},"const session = await stripe.checkout.sessions.create({\nmode: 'subscription', \u002F\u002F or 'payment' for one-time\ncustomer_email: user.email,\nline_items: ",[138,139,140],"span",{},"{ price: body.priceId, quantity: 1 }",",\nsuccess_url: ",[109,143,144],{},"${process.env.BASE_URL}\u002Fbilling\u002Fsuccess?session_id={CHECKOUT_SESSION_ID}",",\ncancel_url: ",[109,147,148],{},"${process.env.BASE_URL}\u002Fbilling\u002Fcancelled",",\nmetadata: { userId: user.id }\n})",[29,151,152],{},[109,153,154],{},"return { url: session.url }\n})\n",[29,156,157,158,161],{},"On the client, call this endpoint and redirect the browser to ",[109,159,160],{},"session.url",". Do not update your database here beyond a temporary intent; the webhook is authoritative.",[101,163,165],{"id":164},"_4-verify-webhooks-and-upsert-subscriptions","4) Verify webhooks and upsert subscriptions",[29,167,168],{},"Webhooks are where your app learns the truth: a checkout completed, an invoice paid or failed, a subscription canceled or renewed.",[106,170,171,174],{},[109,172,173],{},"\u002F\u002F server\u002Fapi\u002Fstripe-webhook.post.ts\nimport Stripe from 'stripe'\nexport default defineEventHandler(async (event) => {\nconst sig = getHeader(event, 'stripe-signature')\nconst buf = await readRawBody(event)\nconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2024-06-20' })\nlet evt\ntry {\nevt = stripe.webhooks.constructEvent(buf!, sig!, process.env.STRIPE_WEBHOOK_SECRET!)\n} catch (err) {\nsetResponseStatus(event, 400)\nreturn { error: 'Invalid signature' }\n}",[29,175,176],{},[109,177,178,179,182],{},"switch (evt.type) {\ncase 'checkout.session.completed': {\nconst s = evt.data.object as Stripe.Checkout.Session\nawait upsertCustomerAndSub({\nprovider: 'stripe',\nuserId: s.metadata?.userId as string,\ncustomerId: s.customer as string,\nsubscriptionId: s.subscription as string,\nstatus: 'active'\n})\nbreak\n}\ncase 'invoice.paid':\ncase 'customer.subscription.updated':\ncase 'customer.subscription.deleted': {\nconst sub = evt.data.object as Stripe.Subscription\nawait upsertSubscription({\nprovider: 'stripe',\ncustomerId: sub.customer as string,\nsubscriptionId: sub.id,\npriceId: sub.items.data",[138,180,181],{},"0","?.price.id,\ncurrentPeriodEnd: new Date(sub.current_period_end * 1000),\nstatus: sub.status\n})\nbreak\n}\n}\nreturn { received: true }\n})\n",[29,184,185],{},"Store fields you will actually use in guards and UI: provider, customer ID, subscription ID, price ID, status, current period end, and plan attributes derived from the price.",[101,187,189],{"id":188},"_5-model-plans-as-entitlements","5) Model plans as entitlements",[29,191,192],{},"Plans change. Entitlements are stable. Keep a small map that turns a price ID into abilities and limits. This makes price experiments low risk.",[106,194,195,206],{},[109,196,197,198,201,202,205],{},"\u002F\u002F lib\u002Fentitlements.ts\nexport const ENTITLEMENTS = {\n",[138,199,200],{},"process.env.PUBLIC_PRICE_BASIC!",": { seats: 1, ai_chat: false, uploads: 1, rpm: 20 },\n",[138,203,204],{},"process.env.PUBLIC_PRICE_PRO!",":   { seats: 5, ai_chat: true,  uploads: 10, rpm: 120 }\n}",[29,207,208],{},[109,209,210,211,214],{},"export function getEntitlements(priceId?: string) {\nreturn ENTITLEMENTS",[138,212,213],{},"priceId || ''"," || { seats: 0, ai_chat: false, uploads: 0, rpm: 0 }\n}\n",[29,216,217],{},"On login or webhook update, attach the current plan flags to the user’s session or fetch them server-side when rendering protected pages.",[101,219,221],{"id":220},"_6-gate-features-in-route-middleware-and-components","6) Gate features in route middleware and components",[106,223,224],{},[109,225,226],{},"\u002F\u002F middleware\u002Frequire-paid.global.ts\nexport default defineNuxtRouteMiddleware(async (to) => {\nconst { user } = useAuth()\nif (!user.value) return navigateTo('\u002Flogin')\nconst plan = await $fetch('\u002Fapi\u002Fme\u002Fplan')\nif (!plan.ai_chat && to.path.startsWith('\u002Fchat')) return navigateTo('\u002Fupgrade')\n})\n",[29,228,229],{},"Prefer server checks for write actions. For example, validate usage limits in API handlers so users cannot bypass the UI.",[29,231,232],{},"If you are shipping an AI product, tie token spend or requests per minute to the same entitlements map. Log prompt\u002Fresponse counts per user and block when limits are hit. No separate stack required.",[36,234,236],{"id":235},"build-the-billing-and-operations-layer","Build the billing and operations layer",[29,238,239],{},"Users expect to self-serve. Keep the Billing page boring and accurate:",[44,241,242,245,248],{},[47,243,244],{},"Show plan name, renewal date, status, and payment method.",[47,246,247],{},"Link to Stripe’s Customer Portal for card updates, address, and invoice downloads.",[47,249,250],{},"Offer one-click upgrade paths that create a fresh checkout session.",[106,252,253],{},[109,254,255,256,259],{},"\u002F\u002F server\u002Fapi\u002Fbilling\u002Fportal.post.ts\nconst portal = await stripe.billingPortal.sessions.create({\ncustomer: customerId,\nreturn_url: ",[109,257,258],{},"${process.env.BASE_URL}\u002Fsettings\u002Fbilling","\n})\nreturn { url: portal.url }\n",[29,261,262],{},"Emails matter more than most founders think. Send:",[44,264,265,268,271],{},[47,266,267],{},"Welcome and receipt after first payment.",[47,269,270],{},"Trial ending 3 days and 1 day before expiry.",[47,272,273],{},"Payment failed, then dunning retries with a clear call to update the card.",[29,275,276],{},"Automate the chores. Use cron-compatible jobs for:",[44,278,279,282,285],{},[47,280,281],{},"Daily MRR and failed-payment summaries to your inbox or Slack.",[47,283,284],{},"Trial-to-paid conversion report.",[47,286,287],{},"Usage reset jobs for monthly limits.",[29,289,290],{},"Your boilerplate’s admin panel should let you search by email, view subscription history, comp a month, or ban obvious spammers. Add a manual “set plan” action for support, but keep the webhook as the source of truth to prevent drift.",[29,292,293],{},"Localization is not optional if your audience is global. Start with one extra language. Translate pricing copy, plan names, and the five transactional emails above. Ensure each locale has proper meta tags and a sitemap entry so search engines index the right pages.",[36,295,297],{"id":296},"launch-and-grow-without-guesswork","Launch and grow without guesswork",[29,299,300],{},"Ship with a real landing page, not a placeholder. Put your top three outcomes above the fold, a short demo GIF, and a clear pricing grid that mirrors your actual entitlements.",[29,302,303],{},"Turn on analytics on day one. Track: visits, signups, checkout starts, checkout completes, upgrade\u002Fdowngrade events, and churn reasons. A simple funnel shows where to fix copy versus code.",[29,305,306,307,314],{},"Post updates where your audience already hangs out. If you work in public on X, a ",[308,309,313],"a",{"href":310,"rel":311},"https:\u002F\u002Fx-post-copier.online",[312],"dofollow","tweet copier Chrome extension"," helps turn scattered posts and screenshots into a weekly changelog or a clean case study. It saves time you should spend on product, not copy-paste.",[36,316,318],{"id":317},"common-pitfalls","Common pitfalls",[44,320,321,327,333,339,345],{},[47,322,323,326],{},[50,324,325],{},"Treating redirects as proof."," Users close tabs. Network calls drop. Only webhooks determine who paid and what they get.",[47,328,329,332],{},[50,330,331],{},"Binding logic to pricing."," Put entitlements in code or config, not in Stripe object names. Prices can change without breaking access checks.",[47,334,335,338],{},[50,336,337],{},"Burying provider specifics in your app."," Wrap Stripe behind a small PaymentProvider interface. You will thank yourself when you add a second processor or a regional method.",[47,340,341,344],{},[50,342,343],{},"Skipping test paths."," In test mode, run: trial start, upgrade, downgrade, cancel, card update, and payment failure. Keep logs. Fix every 400\u002F500 until green.",[47,346,347,350],{},[50,348,349],{},"Forgetting localization and SPF\u002FDKIM."," Translate subject lines and verify your sender domain so invoices do not land in spam.",[36,352,354],{"id":353},"key-takeaways","Key takeaways",[44,356,357,360,363,366,369],{},[47,358,359],{},"Stripe handles money. Your Nuxt app owns identity, entitlements, and UI gates.",[47,361,362],{},"Model plans as flags tied to price IDs, and let webhooks be your billing source of truth.",[47,364,365],{},"Build a plain Billing page with self-serve portal links and clear emails.",[47,367,368],{},"Automate reports and limits so operations do not depend on your memory.",[47,370,371],{},"Launch with analytics and a real page. Use simple tools to repurpose social proof fast.",[373,374,377,381,385,388,392,395,399,402,406,409,413],"section",{"className":375},[376],"post-faq",[36,378,380],{"id":379},"faq","FAQ",[101,382,384],{"id":383},"do-i-need-stripe-billing-for-a-nuxt-saas-or-is-checkout-enough","Do I need Stripe Billing for a Nuxt SaaS, or is Checkout enough?",[29,386,387],{},"Hosted Checkout covers most subscriptions. Add the hosted customer portal if you want self-serve plan changes and payment method updates without building custom UI.",[101,389,391],{"id":390},"how-should-i-test-webhooks-during-development","How should I test webhooks during development?",[29,393,394],{},"Use your provider’s CLI or a tunneling tool to forward webhooks to your local server. Log every event and verify your signing secret in test mode first.",[101,396,398],{"id":397},"what-subscription-data-should-i-store-in-my-database","What subscription data should I store in my database?",[29,400,401],{},"Keep provider, customer ID, subscription ID, plan or price ID, status, current period end, and currency. Derive access from these fields and your plan flags.",[101,403,405],{"id":404},"can-i-switch-payment-providers-later","Can I switch payment providers later?",[29,407,408],{},"Yes. If your starter kit supports multiple, swappable providers, keep a PaymentProvider interface and map shared events so you can migrate with minimal code changes.",[101,410,412],{"id":411},"how-do-i-tie-ai-features-to-paid-plans","How do I tie AI features to paid plans?",[29,414,415],{},"Add plan flags like ai_chat or generation_limits. Check these flags in middleware and rate-limit generation endpoints based on the active subscription.",[373,417,418,422],{},[36,419,421],{"id":420},"recommended-resources","Recommended resources",[44,423,424,425],{},"\n  ",[47,426,427],{},[308,428,313],{"href":310},{"title":430,"searchDepth":431,"depth":431,"links":432},"",2,[433,434,443,444,445,446,447,454],{"id":38,"depth":431,"text":39},{"id":95,"depth":431,"text":96,"children":435},[436,438,439,440,441,442],{"id":103,"depth":437,"text":104},3,{"id":117,"depth":437,"text":118},{"id":124,"depth":437,"text":125},{"id":164,"depth":437,"text":165},{"id":188,"depth":437,"text":189},{"id":220,"depth":437,"text":221},{"id":235,"depth":431,"text":236},{"id":296,"depth":431,"text":297},{"id":317,"depth":431,"text":318},{"id":353,"depth":431,"text":354},{"id":379,"depth":431,"text":380,"children":448},[449,450,451,452,453],{"id":383,"depth":437,"text":384},{"id":390,"depth":437,"text":391},{"id":397,"depth":437,"text":398},{"id":404,"depth":437,"text":405},{"id":411,"depth":437,"text":412},{"id":420,"depth":431,"text":421},"2026-09-13",null,"md",{"@context":459,"@graph":460},"https:\u002F\u002Fschema.org",[461,465],{"@type":462,"headline":6,"description":26,"image":25,"inLanguage":463,"datePublished":464},"BlogPosting","en","2026-09-13 03:07:34",{"@type":466,"mainEntity":467},"FAQPage",[468,472,474,476,478],{"@type":469,"name":384,"acceptedAnswer":470},"Question",{"@type":471,"text":387},"Answer",{"@type":469,"name":391,"acceptedAnswer":473},{"@type":471,"text":394},{"@type":469,"name":398,"acceptedAnswer":475},{"@type":471,"text":401},{"@type":469,"name":405,"acceptedAnswer":477},{"@type":471,"text":408},{"@type":469,"name":412,"acceptedAnswer":479},{"@type":471,"text":415},{},true,[],"\u002Fblog\u002Fstripe-saas-step-by-step-guide-nuxt-builders","stripe saas",[],"Informational",{"title":6,"description":26},{"loc":483},"blog\u002Fstripe-saas-step-by-step-guide-nuxt-builders",[484,491,492,493,494],"nuxt","saas starter kit","payments","vue",[484,491,492,493,494],"rhGmoozLCHFHSjtLFQaUNRpWjf-g28xrbsKBKV7U6k8",[498,503],{"title":499,"path":500,"stem":501,"description":502,"children":-1},"SaaS Post-Launch Checklist – 7 Steps to a Secure Startup","\u002Fblog\u002Fsaas-post-launch-hardening-checklist","blog\u002Fsaas-post-launch-hardening-checklist","Just went live? Follow this SaaS launch checklist to harden your security and ensure your app is ready for scale. Simple steps for a professional foundation.",{"title":504,"path":505,"stem":506,"description":507,"children":-1},"Best Nuxt Starter Kits for SaaS Projects (2026)","\u002Fblog\u002Ftop-saas-starter-kits","blog\u002Ftop-saas-starter-kits","Looking for the best Nuxt starter kit? I've reviewed the top Nuxt boilerplates to help you find a stack that doesn't get in your way.",1789286496446]