A practical plan for using an AI coding agent to scaffold a subscription SaaS, plus the auth, database and billing details you still need to check yourself.
Quick answer: Write a one-page spec first: users, the one core workflow, the data tables and the paid plan. Have an AI agent scaffold the Next.js app with Supabase auth and a Stripe subscription flow, then review four areas yourself. (1) Server code checks the user with
supabase.auth.getClaims()(orgetUser()), nevergetSession(). (2) Row Level Security is enabled on every table in an exposed schema. (3) The Stripe webhook verifies signatures against the raw request body and handles duplicate and out-of-order events. (4) Secret keys never reach the browser. Then deploy to Vercel with separate test and live environment variables.
Step 1: Scope the MVP before you prompt
AI agents are fast, so scope creep is cheap to start and expensive to maintain. Write down:
- Who signs up: individuals, or teams with multiple members?
- The one workflow worth paying for: for example, "upload a CSV, get a cleaned report, download it".
-
Data: three to six tables at most, such as
profiles,projects,reports,subscriptions. - Plans: one free tier and one paid tier is enough to test willingness to pay.
- Out of scope: admin panels, teams, usage-based billing, i18n. Add them after real users ask.
This spec becomes your first prompt.
Step 2: Write a prompt the agent can execute
Name the stack, the pages and the rules. Leave visual polish for later. For example:
Build a Next.js App Router app in TypeScript with Tailwind. Use Supabase for email/password and magic-link auth and for the database. Tables:
profiles(id = auth user id),projects(owner_id, name, created_at),subscriptions(user_id, stripe_customer_id, stripe_subscription_id, status, price_id, current_period_end). Enable RLS on all tables so users can only read and write their own rows. Pages: landing, pricing, login, dashboard (list and create projects), settings (manage billing). Use Stripe Checkout in subscription mode for one "Pro" monthly price, and the Stripe customer portal for plan changes and cancellation. Add a webhook route that verifies the Stripe signature and updatessubscriptions. Gate project creation beyond 3 projects behind an active subscription, checked on the server.
Ask the agent to plan first and show you the file structure and SQL migration before writing everything. Fixing a plan is much cheaper than fixing code.
Step 3: Review authentication
Supabase's Next.js server-side auth guide uses the @supabase/ssr package with cookie-based sessions. Two things to check in what the agent generates:
-
Verify the user on the server properly. Supabase's docs say to use
supabase.auth.getClaims()to protect pages and user data. It verifies the access token's signature. You can also callgetUser()for a fresh, server-confirmed user record. The docs also say never to trustsupabase.auth.getSession()in server code, because it reads the session from a cookie without revalidating it, and cookies can be forged. -
Session refresh runs in middleware. The Supabase guide refreshes the auth token in middleware (Next.js 16 renamed middleware to "Proxy"). In Next.js 15,
cookies()is async, so the server client is created withawait.
Also check that protected routes redirect anonymous users on the server, not only in client components.
Step 4: Review the database and Row Level Security
This is the most important review step. Supabase's RLS guide is blunt: a table in an exposed schema without RLS is readable and writable by any role with a grant on it. Enable RLS on every table in an exposed schema.
Check the generated migration for:
-
alter table ... enable row level security;on every table, including tables added later. - Policies scoped to
auth.uid(): for example,projectsrows whereowner_id = auth.uid(). -
Views: Supabase notes that views bypass RLS by default because they are usually created by the
postgresuser. Don't expose a view over a protected table unless it's made safe. - Secret key usage: the secret (service role) key bypasses RLS. Supabase says never to use it in the browser. Use it only in server code such as the webhook handler.
Then test as a real user: create two accounts and confirm neither can see the other's data through the API.
Step 5: Review Stripe billing
The standard flow is: Checkout (subscription mode) → webhook updates your subscriptions table → your app reads that table to grant access → the customer portal handles upgrades and cancellation.
Stripe's webhook documentation covers what agents most often get wrong:
-
Verify every event. Use the
Stripe-Signatureheader and yourwhsec_signing secret. Without verification, Stripe warns, attackers could send fake events to grant access. -
Use the raw body. Stripe requires the unmodified raw request body for signature verification. In a Next.js route handler, read it with
await request.text()and don't parse JSON first. - Return 2xx quickly, before slow work, to avoid timeouts.
- Handle duplicates. Endpoints can receive the same event more than once, so log processed event IDs and skip repeats.
- Don't assume order. Stripe doesn't guarantee event order. Fetch the current subscription from the API when in doubt, rather than trusting the sequence.
For subscriptions, Stripe's docs describe customer.subscription.created, .updated (renewals, plan changes, discounts) and .deleted (subscription ends). On invoice.paid, Stripe recommends confirming the subscription status is active before extending access. Handle invoice.payment_failed too, so access lapses cleanly.
To test locally, run stripe listen --forward-to localhost:3000/api/webhooks/stripe (adjust to your route). The CLI prints a signing secret for local use.
Step 6: Deploy
- Push the code to GitHub.
- Import the repo into Vercel and set environment variables: Supabase URL and publishable key (public), Supabase secret key and Stripe secret and webhook secret (server-only, no
NEXT_PUBLIC_prefix). - Create a live-mode webhook endpoint in Stripe pointing at your production URL, and use its own signing secret.
- Add your production URL to Supabase Auth's redirect settings so magic links and OAuth return to the right domain.
- Run one real subscription with a live card, then cancel it through the portal and confirm access is removed.
Where an AI agent helps, and where it doesn't
An agent removes most of the boilerplate: routing, forms, dashboard UI, the Supabase client setup, the migration skeleton and the Checkout and portal routes. It doesn't remove your responsibility for security and money. RLS policies, webhook verification and server-side access checks are where a plausible-looking bug costs real data or revenue. Read those files line by line.
This is the workflow Massvai is built around. Its AI coding agent turns a prompt into a full-stack Next.js 15 + TypeScript app with Supabase and Stripe in the stack, shows the build in a live preview, syncs the repo to GitHub, and deploys to your own Vercel project through a guided flow. You can export every file, so you can run the review checklist below in your own editor.
Pre-launch checklist
- [ ] Server code uses
getClaims()/getUser(), nevergetSession(), for access decisions - [ ] RLS enabled on every exposed table; policies tested with two accounts
- [ ] No views exposing protected tables
- [ ] Supabase secret key and Stripe secrets are server-only
- [ ] Webhook verifies signatures against the raw body
- [ ] Duplicate events skipped by event ID; no dependence on event order
- [ ] Access granted only for
activesubscriptions; failed payments handled - [ ] Customer portal linked from settings
- [ ] Separate test and live keys, webhooks and redirect URLs
Sources: Supabase docs ("Setting up Server-Side Auth for Next.js", "Row Level Security"); Stripe docs ("Receive Stripe events in your webhook endpoint", "Using webhooks with subscriptions"); Next.js docs (Proxy file convention, version history); Massvai homepage. Checked October 2026.
Top comments (1)
One forward-looking addition, because this tutorial's prompt template is exactly the kind that goes stale on a date: after Supabase's October 30 change, new tables in existing projects stop getting default grants — so the migration the agent writes from this prompt needs explicit
GRANTstatements, or the first query against any table created after that date fails with42501: permission deniedwhile everything "looks" correctly configured.The fix is one more line in the prompt itself: "include
grant select, insert, update, delete on <table> to authenticated(andanononly where logged-out reads are intended) in the same migration as the RLS enable + policy" — grant and policy in one migration, same as the auth rule of the post. New projects created since late May already behave this way, so the template works both directions.The 60-second pre-flight for anyone who already shipped from an earlier version of this pattern:
select * from information_schema.role_table_grants where table_schema = 'public' and grantee in ('anon', 'authenticated');— zero rows means October 30 is a non-event; rows mean the grants are already explicit. (Full write-up with the error-message fixes: dev.to/cekuu35/supabase-permission...)