Aurora AITell us your case

Offering

ServicesProductsCase studies

For whom

Private EquityEnterpriseSMB
ServicesProductsCase studiesAboutBlogContact

Knowledge base

Start hereWikiGlossaryGuides

Courses Knowledge base

Build an AI SaaS product from zero: from idea to first payment

A full build of an AI SaaS product with coding agents: validating the pain, wiring auth and payments, and proving it works before anyone pays.

A single continuous filament of light passes through a series of shapes on a graphite background, from scattered points on the left to one solid crystallised form on the right.
A single continuous filament of light passes through a series of shapes on a graphite background, from scattered points on the left to one solid crystallised form on the right.
Courses#ai-saas #coding-agents #product-build #supabase #stripe #verification

An agent can hand you a working application in an afternoon and still hand you something no paying customer should touch. Every screen renders, every button responds, and then you point your own domain at it and the login form stops working for reasons that have nothing to do with the code the agent wrote.

I will walk the whole build so that moment does not catch you out. Six stages, in this order: pain, promise, product, plumbing, packaging, proof. Six words starting with the same letter is a slightly silly way to organise an engineering project, and it is exactly why the sequence sticks.

The worked example throughout is ClientPack, a tool that turns discovery-call transcripts into a branded proposal deck an agency owner can send to a client. It is illustrative only. Aurora did not build it, does not sell it, and has no customers for it. I use it because a vague example teaches nothing.

Pain: validate the problem before you write product code

Sell before you build. Before any product code exists you want evidence that somebody will pay, and that evidence has to come from outside your own head: a conversation where you ask the price question directly, a rough demo somebody reacts to, an email address typed into a waiting list. Your own confidence about the idea is the one input that proves nothing.

Most people open by asking a model for a business idea. That is the one prompt almost guaranteed to give you a mediocre answer, because the model has nothing from you to work with and returns the same shape of idea to everyone else who asks. Nothing in the output is yours.

Mine data you already own instead. If you run a channel, a community, a mailing list, or a support inbox, you are sitting on thousands of unfiltered complaints written by exactly the people you want as customers. Point research agents at that first, then at the public places where the same person complains: forums, subreddits, replies. You want the raw sentences, not a summary. The phrasing people use when they are annoyed is the phrasing that later belongs on your landing page.

If you have no audience yet, the substitute is your own subject-matter knowledge. Pick the industry you actually understand and research the pain inside that niche. What makes the research yours is either data nobody else can reach or judgment nobody else has. Without one of those, you get the generic idea everyone else got.

What comes out of this phase is a short list of candidate products, and you pick from it yourself. Three constraints kept the choice honest here. The thing had to be buildable in a day. It had to be visual, because a person who can see the output understands the value without training. And a new user had to reach something worth having within five or ten minutes of signing up, not after a week of onboarding.

ClientPack won on all three. Agency owners finish discovery calls and then rebuild the same proposal document from scratch every time, badly, at eleven at night. The pain is real, boring, and repeated weekly, which is the profile you want.

One honest note before moving on. When you look at an idea like this and see no technical barrier, that is not a reason to drop it, but it is a reason to be clear-eyed: a competent person with the same tools can rebuild the surface of your app in a day. What they cannot rebuild is covered at the end of this piece.

Now build the waiting list. Not as marketing, as an instrument. Put up one page with the promise on it and an email field, send it to the places where you found the pain, and read the result as data. If nobody signs up, either you aimed at the wrong person or the promise is not worth the click. Both answers are cheaper to learn now than after two weeks of building.

Promise: one sentence, one person

The promise is a single sentence on the landing page that says what the tool does and how that answers the pain you just found. If a visitor needs the second paragraph to understand the first, you do not have a promise yet, you have a description.

For the worked example the headline landed as "Turn discovery calls into client-ready proposal packs." Underneath it, one longer line does the work of connecting to the pain: a polished, branded proposal with scope, an ROI breakdown and a project plan, without rebuilding every document from scratch. The short brand line is "Discovery in, client-ready out."

Notice how narrow that is. It does not say "AI for agencies." It names one input, one output, one moment in one person's week. A broad tool sounds more valuable and sells worse, because the visitor has to do the work of imagining themselves in it. Narrow it until one specific person reads the headline and thinks: that is my Tuesday.

A cheap technique worth stealing when you lock the messaging: before committing, have a model generate several buyer personas and react to the name, the logo and the three lines of copy in character. An agency owner, a small-business owner, a CEO. Ask only for first impressions, not analysis. The useful output is the persona who misreads the headline, because that misreading is the sentence you have to fix. The version above went through exactly that pass and got tighter for it.

Write the promise before you build. Everything downstream, the deck structure, the onboarding, the pricing page, gets easier when there is one sentence to check decisions against.

Product: plan it before you build it

The temptation is to open a coding agent and say "build the app." What comes back looks impressive and falls apart the moment you push on it, because nobody decided what it was supposed to be.

Split the work in two. One session plans and never writes code. Another executes the plan. Say that out loud in the prompt, because a capable model will happily start implementing if you leave the door open: you are the project manager for this build, you are not the executor, and your deliverable is a written build plan that sub-agents can work from.

Then give the planner the actual vision, at length. This is the part of the process where your time is worth the most and where nobody can substitute for you. Dictating by voice helps here, because you will say more in three minutes of talking than in three minutes of typing, and the planner benefits from the extra detail. Describe the user's journey concretely: the user uploads one or more call transcripts, the system extracts the pain points with the client's own quotes, reframes the real constraint behind the stated request, proposes a plan, projects the numbers that will move, and outputs a branded deck carrying the user's own logo, colours and case studies.

The plan document that comes back should be specific enough to argue with. In this build it fixed the output structure first, a ten-slide deck: cover, where you are today, the real constraint, the plan, what moves, the math, how it works, why us, scope, and investment plus next steps. Fixing the artefact before the architecture is the right order, because the deck is the product and everything else is delivery.

Then the architecture. Next.js with the App Router for the application itself, Supabase for authentication and the database, Stripe for checkout. Two AI calls per project, one to analyse the transcripts and one to generate the deck, which matters later when you count cost per user. This is an unremarkable stack, and that is the point. Unremarkable stacks have documentation, and coding agents have read all of it.

Finally, the phasing. A foundation phase, then two build phases, split into lettered workstreams so several can run at once. Ask for the parallelism explicitly. An agent left to itself will build like an assembly line, one thing after another, and you will watch a progress bar for hours. Ask which parts have no dependency on each other and can be built simultaneously, and require each worker to write its progress back into the plan document so you can inspect who is doing what without interrupting anyone.

Give every sub-agent one narrow job. One agent that does everything degrades as its context fills up, and the failure is subtle: it does not stop, it just gets worse. When a session starts feeling vague, take a structured handoff summary, clear the context, and paste the summary into a fresh session.

One structural risk to plan around before you hit it. If you run two coding tools in the same project folder, they will eventually overwrite each other's work, and you will not notice until something that worked stops working. The fix is lanes: give each tool a different job (one builds features, the other tests and audits) and keep parallel sub-agents inside isolated working copies of the repository rather than all editing the same files.

A luminous plan lattice in the upper left fans out into four parallel light channels running in separate walled grooves that never touch or cross.
A luminous plan lattice in the upper left fans out into four parallel light channels running in separate walled grooves that never touch or cross.

Plumbing: accounts, database, payments

This is the part non-technical builders dread, and it is the part that has genuinely become easy. You are not expected to know how any of it works. You are expected to know what to ask for and where to click.

Four words, in plain language. Authentication is the system that checks who a user is and keeps them logged in. A database is where the app stores everything it needs to remember: users, uploads, generated documents. An API key is a long secret string that proves your app is allowed to call somebody else's service, and anyone holding it can spend your money. An environment variable is a named slot where you keep such a value so it lives in configuration rather than in your code.

Start with Supabase, which covers both authentication and the database. Create a free project, then open Project Settings and the API section and collect three values: the project URL, the anon public key, and the service-role secret. The difference between the last two matters. The anon key is designed to be visible in the browser and is limited by the rules you set on your tables. The service-role secret bypasses those rules entirely. It belongs on the server only, and every hosting panel that offers to mark a value as sensitive should be told to mark that one.

Those three values go into a local .env file, which is a plain text file of key-and-value pairs that your project is configured never to commit. Ask the agent to create it if it does not exist, paste the values in, and ask the agent to verify they work rather than assuming. This is the one part of the build where a typo produces a confusing error two hours later.

The tables come next, and you do not design them. Ask the agent for the SQL, which is the language databases take instructions in, then paste it into the Supabase SQL editor and run it. In this build that single query created eight tables in one shot: subscriptions, transcripts, usage events, decks, case studies, brand profiles and a couple of others. Check the table editor afterwards to confirm they exist. That is the whole verification: does the list look like the thing you were promised.

Then Stripe, and here is the rule that saves you from an expensive mistake. Do all of it in sandbox first. Stripe's sandbox is a complete parallel copy of your account where nothing is real: you get separate keys (they are prefixed pk_test and sk_test rather than pk_live and sk_live), a list of fake card numbers, and transactions that behave exactly like real ones without moving money. Set up the subscription product there, at 39 dollars a month or 390 a year in this example, and run the full loop: a user signs up, hits the paywall, checks out with a test card, and comes back to an account that reflects what they bought. Stripe's customer portal handles cancellation and card updates, so you do not build a billing screen.

One more term you will meet here. A webhook is Stripe calling your app to say something happened, such as a payment succeeding, so your database can update without polling. It has its own secret, and it is one of the values that cannot be set until the app has a public address, because it depends on the URL.

Only when the whole loop is proven in sandbox do you swap the test keys for live ones. The code does not change. That is the entire benefit.

Cost is worth checking now rather than after launch, and it depends on how much text your users push through. In the measured run here, a complete new-user journey cost about 13 cents in API calls, roughly 4 cents to analyse the transcript and 9 cents to generate the deck. A separate estimate on the same operation put a single pack nearer 16 to 20 cents, so treat the per-pack cost as a range of roughly 13 to 20 cents rather than a fixed number, and expect longer transcripts to sit at the top of it. The paid plan here caps a subscriber at 25 decks a month, and a subscriber who uses every one of them costs around 3 dollars of inference against the 39 dollars they pay. Infrastructure at around fifty users is similarly boring: Supabase Pro at 25 dollars a month for backups, Vercel Pro at 20. Every one of these figures is an estimate tied to a usage pattern, and the useful habit is having the agent log tokens and dollars per call so the real number is visible in your own admin panel.

Three modules linked by glowing conduits — a narrow gateway, a layered slab and a graduated valve — with one green glowing key floating outside their outline in its own pocket of darkness, held by a single thin thread.
Three modules linked by glowing conduits — a narrow gateway, a layered slab and a graduated valve — with one green glowing key floating outside their outline in its own pocket of darkness, held by a single thin thread.

Packaging: the name, the page, the address

Packaging can run in parallel with the build, because it depends on the promise rather than the code. While one tool builds, the other can name the thing.

Brief the naming properly and it takes one pass. Say what the product does, who it is for, and how it should feel. Here the brief was: literal rather than clever, because there is one clear promise and the name should say it; premium consultancy rather than startup playful; no red; modern and clean without looking like generic AI output; and no connection to any personal brand. Then add the requirement people forget: run a collision check and reject names already in use. That check killed two otherwise good candidates before anyone got attached to them, which is a much cheaper way to lose a name than a trademark letter.

The landing page carries the headline, the promise, and an email capture. Here is the prompt discipline that decides whether it works: do not stop until you have validated it, open it, take screenshots, and make sure the email actually gets written somewhere I can read. Without that instruction you get a page with a form that swallows addresses into nothing. It looks finished. It captures no one. When the emails did land in a place with no interface, the fix was one more request: a password-protected admin page listing signups with a CSV download. Then test it yourself by submitting an address and watching the row appear.

Deployment is two accounts and about ten minutes. Push the code to a GitHub repository, then import that repository into Vercel, which builds and hosts it and redeploys automatically on every push. From then on your release process is telling the agent to push.

The first deploy returned a 404, the page that means the server has nothing at that address. This is worth knowing not because of the specific cause but because of the fix: screenshot the error page, paste it back to the agent with a sentence of context about what you connected to what, and let it diagnose. It knows where deployment configuration lives, and a screenshot carries information a paraphrase loses.

Buy the domain wherever you like. Doing it inside Vercel is one click and attaches the domain to the project automatically, which is how this build got its address for 11.25 dollars. An existing domain from any registrar works too by pointing its DNS records at the host, and the agent will walk through the specific records.

Then a bug that only side-by-side comparison catches. The landing page built by one tool used the real logo. The application pages built by the other tool did not, because that tool had never been told the logo existed. Nothing errored. Both surfaces looked fine alone, and the mismatch only appeared with the two windows open next to each other. When two tools work on one product, brand consistency is not something either of them will notice for you. Open the pages side by side and look.

Last packaging step, and the one that breaks deploys: your .env file is deliberately not pushed to GitHub, so the host does not have your keys. Every value you collected has to be entered again in the hosting project's environment variables, with the service-role secret marked sensitive. Two of them cannot be set until after the first deploy, because they depend on the address the host assigns: the site URL, and the webhook secret. Remember that pair. One of them is about to cause the most instructive failure in the whole build.

Proof: the part almost everyone skips

Verification does not happen unless you demand it, and it does not stick unless you demand it in a loop. An agent told to build X will build X, report success, and be sincerely wrong, because "done" for a model means the code it intended to write now exists. Whether the thing works end to end for a stranger is a different question, and one it will not ask unless you ask it.

So ask it. Do not stop until you have tested it yourself. Prove it works and show me what you tested. Keep going until you are confident, and tell me why you are confident. That phrasing costs nothing and changes the output, because it turns "write the code" into "reach a verified state."

Then bring in a second opinion that is not the builder. Use a different model or a different tool as QA, with browser automation so it clicks through the real application: uploading files in every format the app claims to accept, submitting empty forms, entering a colour name where a hex code is expected, generating documents and reading them. Give it read-only scope and one job, to find what breaks, so it can run while the first tool keeps building. A builder reviewing its own work shares its own blind spots. A different tool has different ones, and the overlap is where the bugs live.

What that pass found in this build is the most useful bug of the day, because it is a category rather than an incident. The application would generate a full deck from empty or nonsensical input. Regenerate stayed enabled with zero pain points, zero solutions, negative interest and a negative investment. Worse, when the return on investment could not be computed, the deck did not fail, it rendered a client-facing slide reading "0x". Somebody would have sent that to a client.

The rule underneath it: a validation rule that is not enforced in code does not exist. The plan said users would review and approve the extracted findings before generating. The interface even showed a review screen. But nothing stopped a user from skipping it, so the review step was a suggestion, and suggestions get skipped. Every time you write "the user will confirm" in a plan, check that the code refuses to proceed when they have not.

Your own hands still find things no automation reports. Clicking around this build turned up a sign-out button that did nothing and a confirmation link in the signup email that dropped new users on the waiting-list page instead of the application. Neither is a blocker, both were deliberately deferred, and both are the kind of small wrongness that quietly tells a first-time user the product is unfinished. Keep a list, decide what ships broken on purpose, and know the difference between deferred and forgotten.

Run a security pass separately, and give it a standard to work from rather than "check for vulnerabilities." Pointing the tool at an OWASP-style checklist turns a vague request into a list it can work through. The single most valuable test for any application where each customer's data sits in a shared database is the two-tenant test: create two accounts and try, from one, to read or modify anything belonging to the other. Cross-tenant leakage is the failure class that ends products, and it is invisible from the outside because the app looks correct to each user in isolation. Here that audit returned four high-severity release blockers plus one critical configuration issue, all of them fixed and re-verified before launch, none of which would have been found by clicking around.

Expect a handful of items the tool cannot close because they are yours: rotating the API key that has been sitting in a project folder all day, switching the demo flag off, and leaving the Stripe sandbox for live keys. Make that list explicit and work through it by hand.

A dense grid of identical cells with one cell out of rhythm — pushed from its slot and flaring green; a thin line links it to a blue measuring plane standing alongside.
A dense grid of identical cells with one cell out of rhythm — pushed from its slot and flaring green; a thin line links it to a blue measuring plane standing alongside.

Which brings me to the failure I would most want you to have read about before it happens, because the obvious diagnosis is the wrong one.

The application worked on the staging address the host had assigned. The domain got attached, and on the real domain login stopped working. Same code, same database, same user, different address. The tempting suspicion was the database: two tables had been added during the security pass a short while before, so of course that was the culprit. It was not. Chasing it would have cost an hour and broken something that was fine.

The actual cause sat in configuration. Web applications defend against cross-site request forgery, which is the attack where a page on another domain silently submits a form to your app using a logged-in user's session. The defence is a middleware check, code that inspects every request before it reaches the application, comparing where the request claims to come from against the host the app is configured to serve. That configured host is an environment variable, the site URL, and it still contained the old staging address. So a perfectly valid login submitted from the new domain looked, to the middleware, exactly like an attack, and got rejected before it ever reached the login logic. No error in the application code, because the request never arrived there.

The fix is one line: update the site URL variable to the real domain, redeploy, and verify end to end on the real address rather than the staging one. Verifying means signing up as a brand new user, confirming the email, running the onboarding, generating something, and completing a checkout, all on the domain customers will use.

Be precise about what that last step proves. The payment went through in Stripe's sandbox, on a card number copied out of Stripe's own documentation, so no money moved and nobody was charged. What is verified is the path: a stranger can arrive at the real domain, create an account, produce something worth having, and pay for it without anything breaking. Swapping the test keys for live ones is a separate action taken deliberately, and until you take it there is no first payment, only a first payment that would have worked.

The general lesson is worth more than the fix. Any environment variable that encodes a hostname is a landmine at cutover, and the blast goes off away from the code you changed. Before you switch domains, list every variable whose value contains a URL and treat that list as your checklist. Then run the whole flow again on the new address. Moving to a real domain changes what the application believes about itself, and every place that stored the old address has to be updated by hand.

What stays yours when the code stops being an advantage

The role you are playing throughout is project manager and judge. You are not writing the code and you do not need to read it, but every decision about what gets built, what counts as done, and what ships is yours. That cuts both ways: if the product fails, that is on the operator, never on the AI. Blaming the model is a category error, because the model did what it was told by someone who did not check.

There is an uncomfortable consequence to that. The application in this walkthrough could be rebuilt by a competent person in a day, and that is true of most software you can describe in a sentence. So the code is not the moat. Two things are. The first is the proprietary expertise encoded in your prompts and your logic: everything you know about how this job is actually done, the distinctions that separate a good output from a plausible one, the mistakes you have watched people make. That knowledge does not fall out of a general-purpose model. It has to be put in deliberately, and it is the reason your version produces something better than the copy someone builds after using yours for a month. The second is proof, and only real usage generates it. Results your customers got, in their words, with their numbers. Nobody can reverse-engineer that.

Neither exists on day one. What exists on day one is a tested V1 with the plumbing proven and the security audited, which is enough to put in front of the people whose complaints sent you looking for the pain in the first place. The next thing to build is not another feature. It is the loop that turns what those people tell you into the part nobody can copy.

Six stages: from pain to proofThe order a product gets built in: from evidence that the pain is real, through the plan and the plumbing, to verification before the first user gets in.Six stages: from pain to proofPainGet evidence somebody wants it before you spend weeks building it.PromiseOne sentence for one person: what it does and which pain it answers.ProductA planning session first; the coding agent executes from that plan.PlumbingAccounts, database and payments; keys in .env, never in the code.PackagingA name, brand rules, a waiting-list page and your own domain.ProofDemand verification, then hand the build to a second model to review.
Six stages: from pain to proofThe order a product gets built in: from evidence that the pain is real, through the plan and the plumbing, to verification before the first user gets in.Six stages: from pain to proofPainGet evidence somebody wants it before youspend weeks building it.PromiseOne sentence for one person: what it does andwhich pain it answers.ProductA planning session first; the coding agentexecutes from that plan.PlumbingAccounts, database and payments; keys in.env, never in the code.PackagingA name, brand rules, a waiting-list page andyour own domain.ProofDemand verification, then hand the build to asecond model to review.
The order a product gets built in: from evidence that the pain is real, through the plan and the plumbing, to verification before the first user gets in.

Test yourself

Five questions on the decisions that shaped the outcome of this build.

  1. What is the waiting-list page for, when it goes up before the product exists?

  2. The reflex is to open a coding agent and say "build the app." What is wrong with what comes back?

  3. The application pages shipped without the logo the other tool had put on the landing page, and neither tool reported an error. Why?

  4. The app could render a client-facing slide reading "0x" from empty input. Where was the actual fault?

  5. After the custom domain was attached, login worked on the staging address but failed on the new one. What turned out to be happening?