[{"data":1,"prerenderedAt":593},["ShallowReactive",2],{"blog-free-nuxt-supabase-saas-boilerplate":3,"related-free-nuxt-supabase-saas-boilerplate":289},{"id":4,"title":5,"author":6,"body":7,"category":6,"date":270,"description":271,"draft":272,"extension":273,"image":6,"meta":274,"navigation":276,"path":277,"seo":278,"stem":279,"tags":280,"__hash__":288},"blog\u002Fblog\u002Ffree-nuxt-supabase-saas-boilerplate.md","A free Nuxt + Supabase SaaS boilerplate (auth, Stripe, email, MIT)",null,{"type":8,"value":9,"toc":264},"minimark",[10,14,21,26,29,32,36,39,94,97,105,109,128,199,210,214,221,234,249,260],[11,12,13],"p",{},"Search \"free SaaS boilerplate\" and you get two kinds of result. Abandoned GitHub repos with a login form and a README that stops at \"TODO: payments\", or polished starter kits with a paywall in front of the thing you actually need. We wanted a third option, so we built one and gave it away.",[11,15,16,20],{},[17,18,19],"strong",{},"BoiledPlate Lite"," is a free, MIT-licensed SaaS starter kit. Same app as the paid version — Nuxt 4, Supabase, Stripe, Resend — minus the AI tooling that makes the paid one set itself up. You clone it, wire it up by hand, and ship.",[22,23,25],"h2",{"id":24},"what-free-actually-means-here","What \"free\" actually means here",[11,27,28],{},"MIT licensed. Use it for client work, commercial products, whatever — no attribution gymnastics, no \"free for non-commercial.\" The repo is private, but access is free: create an account, tell us your GitHub username, and the invite lands in seconds. The account is the entire price.",[11,30,31],{},"It is also not a toy. The free tier is the real plumbing, not a marketing demo with the hard parts stubbed out. That is the whole point: a SaaS starter that quietly skips webhooks and row-level security is not saving you the weeks that actually hurt.",[22,33,35],{"id":34},"whats-in-the-free-saas-starter","What's in the free SaaS starter",[11,37,38],{},"The stack is the one most people land on anyway — a typed Nuxt + Supabase + Stripe + Resend SaaS template:",[40,41,42,54,70,76,82,88],"ul",{},[43,44,45,48,49,53],"li",{},[17,46,47],{},"Authentication"," — email\u002Fpassword and Google OAuth via Supabase, password reset that does not leak whether an account exists, and route protection that lives in middleware instead of scattered ",[50,51,52],"code",{},"if (!user)"," redirects.",[43,55,56,59,60,63,64,69],{},[17,57,58],{},"Stripe billing"," — Checkout and the billing portal, with a signature-verified, idempotent ",[17,61,62],{},"webhook as the source of truth"," for subscription state. Plan limits are enforced in the database with row-level security, not just hidden in the UI. (This is the part most free boilerplates skip — ",[65,66,68],"a",{"href":67},"\u002Fblog\u002Fstripe-webhooks-source-of-truth","here is why the webhook is the product",".)",[43,71,72,75],{},[17,73,74],{},"Transactional email"," — Resend, with idempotent sends so a Stripe retry can't fire the receipt twice.",[43,77,78,81],{},[17,79,80],{},"A dashboard"," — a real app shell with a collapsible sidebar, a revenue-analytics view built on dependency-free SVG charts, an integrations directory, and a notes CRUD that demonstrates the typed-composable + RLS pattern end to end.",[43,83,84,87],{},[17,85,86],{},"A blog"," — Markdown via Nuxt Content, prerendered and in the sitemap (the one you're reading runs on the same setup).",[43,89,90,93],{},[17,91,92],{},"Internationalization"," — every user-facing string is a locale key, shipped in English, German, Spanish, and French with a language switcher and locale-aware currency and dates. Light\u002Fdark theming out of the box.",[11,95,96],{},"Everything is TypeScript strict with generated Supabase types shared across app and server, so you stop guessing at column names.",[11,98,99,100,104],{},"If you want the long version of why each of those pieces takes longer than it looks, we wrote about ",[65,101,103],{"href":102},"\u002Fblog\u002Fnuxt-supabase-stripe-saas-boilerplate","what it actually takes to wire up Nuxt, Supabase and Stripe",".",[22,106,108],{"id":107},"quick-start","Quick start",[110,111,112,119,125],"ol",{},[43,113,114,118],{},[65,115,117],{"href":116},"\u002Flogin","Create a free account"," — that's the whole \"checkout\".",[43,120,121,122,124],{},"On your dashboard, the ",[17,123,19],{}," card asks for your GitHub username — the invite to the repo arrives within seconds.",[43,126,127],{},"Accept it, then:",[129,130,135],"pre",{"className":131,"code":132,"language":133,"meta":134,"style":134},"language-bash shiki shiki-themes material-theme-lighter github-light github-dark","git clone https:\u002F\u002Fgithub.com\u002Firmscheremilio\u002Fboiledplate-lite-release\ncd boiledplate-lite-release\nnpm install\ncp .env.example .env   # add your Supabase keys (Stripe\u002FResend optional to start)\nnpm run dev\n","bash","",[50,136,137,153,163,172,188],{"__ignoreMap":134},[138,139,142,146,150],"span",{"class":140,"line":141},"line",1,[138,143,145],{"class":144},"sbgvK","git",[138,147,149],{"class":148},"s_sjI"," clone",[138,151,152],{"class":148}," https:\u002F\u002Fgithub.com\u002Firmscheremilio\u002Fboiledplate-lite-release\n",[138,154,156,160],{"class":140,"line":155},2,[138,157,159],{"class":158},"sptTA","cd",[138,161,162],{"class":148}," boiledplate-lite-release\n",[138,164,166,169],{"class":140,"line":165},3,[138,167,168],{"class":144},"npm",[138,170,171],{"class":148}," install\n",[138,173,175,178,181,184],{"class":140,"line":174},4,[138,176,177],{"class":144},"cp",[138,179,180],{"class":148}," .env.example",[138,182,183],{"class":148}," .env",[138,185,187],{"class":186},"sutJx","   # add your Supabase keys (Stripe\u002FResend optional to start)\n",[138,189,191,193,196],{"class":140,"line":190},5,[138,192,168],{"class":144},[138,194,195],{"class":148}," run",[138,197,198],{"class":148}," dev\n",[11,200,201,202,205,206,209],{},"Apply the migrations in ",[50,203,204],{},"supabase\u002Fmigrations\u002F",", generate types with ",[50,207,208],{},"npm run db:types",", and you have auth, a dashboard, and the data layer running locally. Add Stripe keys when you're ready to turn on billing.",[22,211,213],{"id":212},"where-the-free-version-stops-and-the-paid-one-starts","Where the free version stops, and the paid one starts",[11,215,216,217,220],{},"The free starter gives you the working product. ",[17,218,219],{},"You"," wire it up.",[11,222,223,224,228,229,233],{},"The paid ",[65,225,227],{"href":226},"\u002F","BoiledPlate"," is the same app built to be set up and extended ",[230,231,232],"em",{},"by AI coding agents",". The difference is the build experience, not the feature checklist:",[40,235,236,243,246],{},[43,237,238,239,242],{},"An ",[50,240,241],{},"AGENTS.md"," conventions contract plus vendored skills, so Claude Code, Cursor, or Codex build on-pattern instead of inventing a second way to do everything.",[43,244,245],{},"A scripted setup interview that brands and configures the whole codebase to your answers, and one-command provisioning of Supabase and Stripe.",[43,247,248],{},"Semantic updates you apply to your customized code with one sentence, instead of a scary upstream merge.",[11,250,251,252,255,256,104],{},"If you just want a free, honest, production-shaped SaaS starter kit, the Lite version is genuinely all you need. If you'd rather an agent did the wiring — and kept the starter updatable as your product grows — ",[65,253,254],{"href":226},"that's what the full version is for",". Either way, you start from the same plumbing we trust enough to run this site on. ",[65,257,259],{"href":258},"\u002Fblog\u002Fhow-to-ship-saas-fast","Here's how we think about shipping SaaS fast",[261,262,263],"style",{},"html pre.shiki code .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sptTA, html code.shiki .sptTA{--shiki-light:#6182B8;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":134,"searchDepth":165,"depth":165,"links":265},[266,267,268,269],{"id":24,"depth":155,"text":25},{"id":34,"depth":155,"text":35},{"id":107,"depth":155,"text":108},{"id":212,"depth":155,"text":213},"2026-06-10","BoiledPlate Lite is a free, MIT-licensed SaaS starter kit: Supabase auth, Stripe billing, transactional email, a blog, and i18n on Nuxt 4. Clone it and ship.",false,"md",{"slug":275},"free-nuxt-supabase-saas-boilerplate",true,"\u002Fblog\u002Ffree-nuxt-supabase-saas-boilerplate",{"title":5,"description":271},"blog\u002Ffree-nuxt-supabase-saas-boilerplate",[281,282,283,284,285,286,287],"free-saas-boilerplate","saas-starter-kit","nuxt","supabase","stripe","open-source","mit","BDvwkRrDE4x6wJ3Yb367mmxDGZ0VG2D3jrS-3-RR34U",[290,405,489],{"id":291,"title":292,"author":6,"body":293,"category":6,"date":396,"description":397,"draft":272,"extension":273,"image":6,"meta":398,"navigation":276,"path":258,"seo":400,"stem":401,"tags":402,"__hash__":404},"blog\u002Fblog\u002Fhow-to-ship-saas-fast.md","How to ship SaaS fast: the stack is easy, the plumbing is the project",{"type":8,"value":294,"toc":390},[295,298,301,305,312,315,319,322,336,346,352,358,364,368,371,374,378,381,387],[11,296,297],{},"Ask ten founders how to ship SaaS fast and you get ten stack arguments. Next vs Nuxt, Postgres vs a hosted DB, Stripe vs Paddle. Pick whatever you already know and the stack debate is over in an afternoon. The framework is not what slows you down.",[11,299,300],{},"What slows you down is the plumbing between the pieces. We know, because we built BoiledPlate by writing that plumbing and then keeping a running log of every edge case it took to make it trustworthy. This site runs on it. Here is the stack we landed on, and then the part nobody warns you about.",[22,302,304],{"id":303},"the-stack-settled-in-one-paragraph","The stack, settled in one paragraph",[11,306,307,308,311],{},"Nuxt 4 with TypeScript in strict mode for the app. Supabase for Postgres, auth, and row-level security. Stripe for payments, with webhooks as the source of truth. Resend for transactional email. Nuxt UI v4 on Tailwind for the interface, and ",[50,309,310],{},"@nuxtjs\u002Fi18n"," so the product speaks more than one language from day one. None of these choices are clever. They are the boring, well-documented options, and that is the point: every one of them has a paved path and an agent can stay on it.",[11,313,314],{},"You can assemble that list in a day. Then you discover the list was the easy 10 percent.",[22,316,318],{"id":317},"the-90-percent-plumbing-you-only-notice-when-it-breaks","The 90 percent: plumbing you only notice when it breaks",[11,320,321],{},"A checkout button that charges a card is a demo. A billing system you can trust at 3am is a project. The gap between them is a pile of edge cases that each look like a one-liner until you actually hit them. Here are the ones that shaped our own commit history.",[11,323,324,327,328,331,332,335],{},[17,325,326],{},"Webhook idempotency."," Stripe retries deliveries. If your handler is not idempotent, a retry double-records a purchase or double-sends a confirmation email. Our handler claims the event id in a ",[50,329,330],{},"stripe_events"," table before doing any work and releases it on failure, so a retried event is recognized and skipped, but a genuinely failed one still gets reprocessed. That single rule (writes are driven by signature-verified, idempotent webhooks and nothing else) is what makes the billing state believable. We wrote a ",[65,333,334],{"href":67},"whole post on why webhooks are the source of truth"," because it is the easiest thing to get subtly wrong.",[11,337,338,341,342,345],{},[17,339,340],{},"Refunds, not just charges."," Most tutorials stop at the happy path. A real product has to handle a chargeback. Our ",[50,343,344],{},"charge.refunded"," case revokes the site entitlement, and because we deliver the product as a private GitHub repo invite, it also removes the buyer from that repo and cancels any still-pending invitation. Full refunds only, partial refunds keep access, and the revocation re-runs safely on Stripe retries even if the refund timestamp was already stamped. That is one event type, and it is four behaviors that all have to agree.",[11,347,348,351],{},[17,349,350],{},"RLS in the same migration, every time."," It is tempting to ship a table now and add policies \"later.\" Later is how data leaks. Our rule is that every table turns on row-level security with explicit policies in the same migration that creates it. No table ever exists in a state where the wrong user can read it.",[11,353,354,357],{},[17,355,356],{},"The legal plumbing."," Selling digital goods to EU buyers means the 14-day right of withdrawal disappears the moment you grant immediate access, but only if the buyer expressly waives it. We collect that waiver as a required consent in Stripe Checkout, with the acceptance message localized, because the terms page depends on it. That is not a feature anyone demos. It is the difference between a sale you can keep and a refund you have to give back.",[11,359,360,363],{},[17,361,362],{},"Ordering."," Inside a paid checkout the steps run in a fixed order: record the purchase, deliver the product, then send the email. Get the order wrong and a delivery retry double-sends the receipt. None of this is hard once you know it. Knowing it is what costs the weeks.",[22,365,367],{"id":366},"why-this-is-the-real-ship-fast-lever","Why this is the real \"ship fast\" lever",[11,369,370],{},"Add those up. Webhook idempotency, refund revocation across two systems, RLS on every table, EU consent collection, delivery state machines, password reset, auth email over SMTP, a sitemap, JSON-LD that does not crash on hydration. Each is a few hours of reading docs and a few more hours of getting the edge case right. Together they are the reason a \"weekend SaaS\" takes two months.",[11,372,373],{},"So the honest answer to \"how do I ship SaaS fast\" is not a framework. It is: do not write the plumbing again. That is the entire thesis behind BoiledPlate. The stack above comes pre-wired, the webhook is already idempotent, the refund path already revokes access, the tables already have their policies, and the EU waiver is already in the checkout. You start from the part that is actually your product.",[22,375,377],{"id":376},"the-setup-is-plumbing-too","The setup is plumbing too",[11,379,380],{},"There is one more place the days go: wiring the accounts. Creating the Stripe product and price, registering the webhook endpoint with the right event types, linking the Supabase project, pushing migrations, configuring the Google provider and the Resend SMTP. We automated that. Point a coding agent at the repo and its first session interviews you (name, languages, theme, billing model), reshapes the codebase to your answers, and then provisions Stripe and Supabase by running the setup scripts instead of clicking through dashboards. The conventions are written down so the agent stays on-pattern while it does it.",[11,382,383,384,104],{},"The result is that the boring, error-prone, well-understood 90 percent is already done and verified, which is exactly the 90 percent that was standing between you and a launch. If you want the longer tour of how the three services fit together, we wrote that up in ",[65,385,386],{"href":102},"the Nuxt + Supabase + Stripe boilerplate post",[11,388,389],{},"Ship fast by spending your time on the 10 percent that is yours. Let the plumbing be plumbing.",{"title":134,"searchDepth":165,"depth":165,"links":391},[392,393,394,395],{"id":303,"depth":155,"text":304},{"id":317,"depth":155,"text":318},{"id":366,"depth":155,"text":367},{"id":376,"depth":155,"text":377},"2026-06-09","The framework picks itself. Webhook idempotency, RLS, refunds, EU consent: the plumbing is what eats your launch. The anti-plumbing SaaS stack.",{"slug":399},"how-to-ship-saas-fast",{"title":292,"description":397},"blog\u002Fhow-to-ship-saas-fast",[403,283,284,285],"saas","VdQKZEYdVWgs4M9SWVxC0T-_oVqEPor_ynaRVdnSTpY",{"id":406,"title":407,"author":6,"body":408,"category":6,"date":396,"description":480,"draft":272,"extension":273,"image":6,"meta":481,"navigation":276,"path":102,"seo":483,"stem":484,"tags":485,"__hash__":488},"blog\u002Fblog\u002Fnuxt-supabase-stripe-saas-boilerplate.md","What it actually takes to wire up a Nuxt, Supabase and Stripe SaaS",{"type":8,"value":409,"toc":473},[410,413,416,419,423,426,429,433,436,443,447,450,453,457,460,464,467,470],[11,411,412],{},"If you are about to build a SaaS, the stack writes itself these days. Nuxt for the app, Supabase for the database and auth, Stripe for payments, Resend for email. It is a good stack. I have shipped on it more than once and I would pick it again tomorrow.",[11,414,415],{},"Then you open an empty project and remember that picking the stack was the easy decision. Wiring four services into something that does not leak data or lose payments is the part that takes the weeks.",[11,417,418],{},"Here is what each piece actually asks of you, roughly in the order it tends to bite.",[22,420,422],{"id":421},"supabase-auth-is-the-fast-part-rls-is-the-part-you-forget","Supabase: auth is the fast part, RLS is the part you forget",[11,424,425],{},"Getting a login form working with Supabase takes an afternoon. Email and password, maybe Google OAuth, a session, a redirect. Feels done.",[11,427,428],{},"It is not done. The moment you have more than one user, every table needs row-level security, and you need it from the first migration, not bolted on after launch when you are already holding real data. RLS is not optional polish. It is the only thing standing between user A and user B's rows. Get a policy wrong and the bug does not throw an error, it quietly serves the wrong data to the wrong person. You also want password recovery that does not reveal whether an email exists, route protection that lives in middleware instead of scattered redirects, and generated types so you stop guessing at column names.",[22,430,432],{"id":431},"stripe-the-webhook-is-the-product-the-checkout-is-the-easy-bit","Stripe: the webhook is the product, the checkout is the easy bit",[11,434,435],{},"Everyone wires the checkout first because it is satisfying. Money goes in, you get redirected, you feel like a founder.",[11,437,438,439,442],{},"The redirect is the least trustworthy thing in your whole system. The real billing state has to come from Stripe's webhooks, signature-verified and idempotent, because the browser will close the tab and the network will deliver the same event twice. Then there is the part most starters skip: a refund has to revoke access on its own, and plan limits have to be enforced in the database, not hidden in the UI. None of this is hard once. It is a lot of small correct decisions, and any one of them wrong is a billing bug in production. I wrote about ",[65,440,441],{"href":67},"the webhook trap in more detail here"," if you want the longer version.",[22,444,446],{"id":445},"resend-simple-to-send-annoying-to-send-exactly-once","Resend: simple to send, annoying to send exactly once",[11,448,449],{},"Sending one email with Resend is three lines. The trouble starts when a Stripe retry fires a second payment-confirmation email, or a flaky request makes you send the welcome email twice. You want a single server-side utility, sends that are idempotent for anything a user can trigger, and every line of those emails living as a locale key so a German customer does not get an English receipt.",[11,451,452],{},"Which brings up the thing that quietly doubles the work on everything above.",[22,454,456],{"id":455},"nuxt-is-the-glue-and-i18n-is-the-tax-on-doing-it-right","Nuxt is the glue, and i18n is the tax on doing it right",[11,458,459],{},"Every string in all of this, the auth labels, the validation messages, the toasts, the Stripe error states, the email bodies, has to exist in every language you support. Tack i18n on at the end and you will spend a week hunting hardcoded strings. Build it in from the first component and it is just a habit. Shipping four languages on day one is a very different project from shipping one and promising the rest.",[22,461,463],{"id":462},"so-how-long-is-all-of-that","So how long is all of that",[11,465,466],{},"For me, the first time, weeks. Not because any single piece is clever, but because the correct version of each one hides about five non-obvious decisions, and you only learn which five by getting them wrong.",[11,468,469],{},"That is the entire reason BoiledPlate exists. It is a Nuxt 4 SaaS starter on exactly this stack, Supabase, Stripe and Resend, already wired the right way. RLS on every table from the first migration. The Stripe webhook as the source of truth, refund-aware, with limits in the policies. One idempotent email utility, localized. Four languages shipped, not promised. You point a coding agent at it, answer nine questions, and the thing reshapes itself to your product instead of you reshaping yourself to it.",[11,471,472],{},"You can absolutely wire this yourself. The stack is good and the docs are all there. I just think the second time you do it, you start wondering why you are paying the same tax twice.",{"title":134,"searchDepth":165,"depth":165,"links":474},[475,476,477,478,479],{"id":421,"depth":155,"text":422},{"id":431,"depth":155,"text":432},{"id":445,"depth":155,"text":446},{"id":455,"depth":155,"text":456},{"id":462,"depth":155,"text":463},"Picking a Nuxt + Supabase + Stripe + Resend stack is the easy decision. Here is the wiring each piece really demands, and where a SaaS starter saves you weeks.",{"slug":482},"nuxt-supabase-stripe-saas-boilerplate",{"title":407,"description":480},"blog\u002Fnuxt-supabase-stripe-saas-boilerplate",[283,284,285,486,403,487],"resend","boilerplate","O9aBZHsolIkxENJU_ebdFE9p0IW2dowSQQ-IXutuW_M",{"id":490,"title":491,"author":6,"body":492,"category":6,"date":396,"description":584,"draft":272,"extension":273,"image":6,"meta":585,"navigation":276,"path":587,"seo":588,"stem":589,"tags":590,"__hash__":592},"blog\u002Fblog\u002Fstripe-subscriptions-nuxt-supabase.md","How to set up Stripe subscriptions in a Nuxt + Supabase app",{"type":8,"value":493,"toc":576},[494,497,500,504,525,529,535,539,546,550,559,563,566,570,573],[11,495,496],{},"Subscriptions are the part of a SaaS where the demo and the reality diverge most. The happy path, where a user clicks subscribe, pays, and gets access, is maybe a fifth of the work. The other four fifths is everything that happens after, and Stripe plus Supabase have a clean division of labor for it if you set things up in the right order.",[11,498,499],{},"Here is the order that has worked for me on a Nuxt + Supabase app.",[22,501,503],{"id":502},"_1-define-your-plans-in-one-place","1. Define your plans in one place",[11,505,506,507,510,511,510,514,517,518,520,521,524],{},"Before any Stripe code, decide what your plans are and give each a slug your app understands, like ",[50,508,509],{},"free",", ",[50,512,513],{},"pro",[50,515,516],{},"team",". Keep the slugs in your codebase and keep the Stripe price IDs in an environment map next to them. The reason to separate the two: your app should reason about ",[50,519,513],{},", not about ",[50,522,523],{},"price_1Q2x...",", and you will have different price IDs in test and live mode anyway. One source of truth for the slugs, one env map for the IDs.",[22,526,528],{"id":527},"_2-checkout-is-a-redirect-nothing-more","2. Checkout is a redirect, nothing more",[11,530,531,532],{},"Create a Stripe Checkout session on the server, hand the URL to the browser, redirect. Do not record anything about the subscription at this point. The checkout session succeeding tells you the user started paying, not that they are entitled to anything yet. Resist the urge to flip a flag on the success page. ",[65,533,534],{"href":67},"That instinct is the single most common billing bug, and here is why.",[22,536,538],{"id":537},"_3-the-webhook-is-where-subscriptions-actually-live","3. The webhook is where subscriptions actually live",[11,540,541,542,545],{},"This is the heart of it. Stripe sends events for the whole lifecycle: subscription created, renewed, payment failed, cancelled, refunded. Your webhook handler, signature-verified and idempotent, is the only thing that writes subscription state to Supabase. When ",[50,543,544],{},"customer.subscription.updated"," says the plan changed, you update the row. When it says the payment is past due, you reflect that. The database is a mirror of what Stripe believes, never a guess your frontend made.",[22,547,549],{"id":548},"_4-turn-subscription-state-into-access","4. Turn subscription state into access",[11,551,552,553,555,556,558],{},"Now you have a row in Supabase that says this user is on ",[50,554,513],{}," and the subscription is active. Access control reads from that row. The important part, again, is where you enforce it: in your row-level security policies, so a ",[50,557,513],{},"-only table is genuinely unreachable for a free user, not just visually hidden. The UI can hide buttons for nice UX, but the policy is what makes the limit real.",[22,560,562],{"id":561},"_5-give-them-the-customer-portal","5. Give them the customer portal",[11,564,565],{},"You do not want to build cancellation, card updates, and invoice history yourself. Stripe's customer portal does all of it. Generate a portal session on the server, redirect the user, and let your webhook pick up whatever they change there. One less set of screens to build and keep correct.",[22,567,569],{"id":568},"the-shape-of-it","The shape of it",[11,571,572],{},"Plans in config. Checkout as a dumb redirect. Webhook as the only writer of state. RLS as the enforcer. Portal for self-service. Get those five right and subscriptions stop being scary, because every piece has exactly one job.",[11,574,575],{},"If you would rather not assemble all five by hand, this is the billing setup BoiledPlate ships by default on a Nuxt 4 + Supabase stack. Subscriptions, one-time, or none, chosen when the starter sets itself up, with the webhook, the RLS limits and the portal already wired. Think of it as the version of this post where someone already made the five decisions, so you just build your product on top.",{"title":134,"searchDepth":165,"depth":165,"links":577},[578,579,580,581,582,583],{"id":502,"depth":155,"text":503},{"id":527,"depth":155,"text":528},{"id":537,"depth":155,"text":538},{"id":548,"depth":155,"text":549},{"id":561,"depth":155,"text":562},{"id":568,"depth":155,"text":569},"The five decisions behind Stripe subscriptions on a Nuxt + Supabase stack: plans in config, checkout, webhooks, RLS-enforced access, and the customer portal.",{"slug":586},"stripe-subscriptions-nuxt-supabase","\u002Fblog\u002Fstripe-subscriptions-nuxt-supabase",{"title":491,"description":584},"blog\u002Fstripe-subscriptions-nuxt-supabase",[285,591,283,284,403],"subscriptions","WgV2n4beSVMVXw3OM_Wp6Q6RGGHUo73mCDuPwU0S7FM",1781125992164]