Your App LogoYOUR APP EXPERTYAE
    • Services
    • About
    • Portfolio
    • Blog
    • FAQ
    • Build Your App
    1. Home
    2. Blog
    3. How to build a SaaS MVP in 6 weeks (without a rewrite later)
    SaaS

    How to build a SaaS MVP in 6 weeks (without a rewrite later)

    A six-week SaaS MVP plan that doesn't trade speed for technical debt — auth, billing, multi-tenancy, and a real operator dashboard from day one.

    YAEL Engineering·01 May 2026·10 min read·1,917 words
    On this page
    • Week 0 — the boring decisions that save you in week 5
    • Week 1 — the multi-tenant foundation
    • Week 2 — billing wired up before the first feature
    • Week 3 — the product surface
    • Week 4 — the operator surface
    • Week 5 — the rough edges
    • Week 6 — production hardening
    • What's *not* in the six weeks
    • FAQ
    • Can I really ship a SaaS in 6 weeks?
    • What if I'm not technical and I'm hiring this out?
    • Do I need Postgres? Why not MongoDB or DynamoDB?
    • When do I switch from Vercel to my own infra?
    • What about Cloudflare Workers / Fly / Railway?
    • Should I use Supabase or roll my own Postgres?
    • What's the typical six-week budget?
    • What if I miss the six-week mark?

    Six weeks is enough time to ship a SaaS MVP that doesn't need a rewrite when the tenth customer signs up. The catch is that you have to build it like a SaaS from the first commit. The teams that fail at the six-week mark are the ones who shipped "a web app with a login" and then spent six months bolting on org switching, billing, role checks, audit logs, and tenant isolation after the fact. Build the foundation first and the features go on top of it. Build the features first and the foundation gets bolted on with a wrench at 3am.

    This is the plan we use when a founder hires us to take a SaaS from blank repo to first paying customer in six weeks. Same plan that took CloudChat from idea to embedded widget at production scale.

    Week 0 — the boring decisions that save you in week 5

    Before week one starts, lock these. They are not interesting and they will define how painful the next five weeks are.

    Framework. Next.js App Router. Server actions for mutations, RSC for reads. You will not be writing a separate API layer.

    Database. Postgres. Use Neon or Supabase if you don't want to run it. Use row-level security from day one.

    Auth. Pick one of Clerk, Auth.js, or WorkOS. Do not roll your own. The team that rolls their own auth at week 1 ships at week 12.

    Billing. Stripe. Specifically Stripe Billing with prices defined in the dashboard, not in code. We have a longer take on Stripe vs Paddle vs LemonSqueezy — for most B2B SaaS the answer is Stripe.

    Hosting. Vercel. You can move later. You won't.

    Pick boring tools

    Every novel tool you pick adds a week. A new ORM, a new auth provider, a new state library — each one looks small in isolation. They are not small in aggregate. Use the tools you've shipped before.

    Week 1 — the multi-tenant foundation

    The single most important week. Get the data model right and everything else is incremental. Get it wrong and you will rebuild it under load when customer #20 asks why they can see customer #19's invoices.

    The data model:

    sql
    -- orgs are the tenant boundary
    create table orgs (
      id          text primary key default 'org_' || nanoid(),
      name        text not null,
      slug        text not null unique,
      plan        text not null default 'free',
      created_at  timestamptz not null default now()
    );
    
    -- users belong to many orgs via membership
    create table users (
      id          text primary key default 'usr_' || nanoid(),
      email       text not null unique,
      created_at  timestamptz not null default now()
    );
    
    create table memberships (
      org_id      text not null references orgs(id) on delete cascade,
      user_id     text not null references users(id) on delete cascade,
      role        text not null check (role in ('owner','admin','member')),
      created_at  timestamptz not null default now(),
      primary key (org_id, user_id)
    );
    
    -- every domain table carries org_id
    create table widgets (
      id          text primary key default 'wid_' || nanoid(),
      org_id      text not null references orgs(id) on delete cascade,
      name        text not null,
      created_at  timestamptz not null default now()
    );
    create index widgets_org_idx on widgets(org_id);

    Now turn on row-level security. This is the part most teams skip, and it's the part that protects you from "we accidentally returned every customer's data in a single query" — which is the single most common failure mode of a SaaS at scale. Read Multi-tenant Postgres row-level security explained for the full take.

    sql
    alter table widgets enable row level security;
    create policy widgets_tenant_isolation on widgets
      using (org_id = current_setting('app.current_org_id', true)::text);

    Every request sets app.current_org_id before issuing a query. Now even a buggy select * from widgets returns only the current tenant's rows.

    Week 2 — billing wired up before the first feature

    Counter-intuitive. Most teams build features first and bolt on billing in the last week. We do it inverted: billing first, features second. Reason — billing surfaces every cross-cutting decision (plan limits, trial state, payment failure recovery, churn) and forces you to get them right while you build features, not after.

    Wire up Stripe Customer Portal, three plans, and one webhook handler. That's it. Here's what the webhook needs to handle on day one:

    ts
    // src/app/api/stripe/webhook/route.ts
    import { headers } from "next/headers";
    import Stripe from "stripe";
    
    const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
    
    export async function POST(req: Request) {
      const sig = (await headers()).get("stripe-signature");
      const body = await req.text();
    
      let event: Stripe.Event;
      try {
        event = stripe.webhooks.constructEvent(
          body,
          sig!,
          process.env.STRIPE_WEBHOOK_SECRET!,
        );
      } catch {
        return new Response("invalid signature", { status: 400 });
      }
    
      // Idempotency: skip if we've already processed this event id.
      if (await alreadyProcessed(event.id)) return new Response("ok");
    
      switch (event.type) {
        case "customer.subscription.created":
        case "customer.subscription.updated":
          await syncSubscription(event.data.object);
          break;
        case "customer.subscription.deleted":
          await downgradeToFree(event.data.object);
          break;
        case "invoice.payment_failed":
          await flagPaymentFailure(event.data.object);
          break;
      }
    
      await markProcessed(event.id);
      return new Response("ok");
    }

    The single biggest webhook bug we see in the wild — teams forgetting idempotency. Stripe will retry. If your handler isn't idempotent, retries will double-charge, double-grant entitlements, or double-cancel. We wrote a whole post on this: webhook idempotency is the bug most teams ship.

    Week 3 — the product surface

    Now you build the actual product. Three days for the main authenticated surface, two days for the unauthenticated marketing page. The authenticated surface lives at /app/<org-slug>/.... Every page is a React Server Component that reads the org context from the request, sets the RLS variable, and queries Postgres directly.

    tsx
    // src/app/app/[orgSlug]/widgets/page.tsx
    import { db, withOrg } from "@/lib/db";
    import { requireMembership } from "@/lib/auth";
    
    export default async function WidgetsPage({
      params,
    }: {
      params: Promise<{ orgSlug: string }>;
    }) {
      const { orgSlug } = await params;
      const { org } = await requireMembership(orgSlug);
    
      const widgets = await withOrg(org.id, () =>
        db.query.widgets.findMany({ orderBy: (w, { desc }) => desc(w.createdAt) }),
      );
    
      return <WidgetList widgets={widgets} />;
    }

    No useEffect. No client-side data fetching. No useState for server data. The page renders on the server, hits the database directly, and ships HTML. The client bundle for this page is whatever interactive components you sprinkle in — that's it.

    Week 4 — the operator surface

    This is the week most MVPs skip and regret. You need an internal dashboard for you (the founder) to do the things your product doesn't expose yet: impersonate a user to debug, force-cancel a subscription, manually grant a plan, view audit logs.

    It does not need to be pretty. It needs to be functional. We typically build it under /admin behind an env-var-gated auth check. Two days of work in week 4 saves you from "can you log into the database and update the plan column for this customer" Slack messages every weekend for the next year. We've written about this trade-off in building internal tools instead of buying Retool.

    Audit logs from day one

    Add an audit_events table and write to it from every mutation. You'll need it for SOC2 in 18 months. You'll need it for "who deleted this widget" in 6 weeks.

    Week 5 — the rough edges

    Onboarding flow. Empty states. Error states. Email notifications (Resend or Postmark). Loading skeletons that aren't ugly. Form validation that doesn't ship with the default Zod error messages. The header that doesn't shift the page on auth state changes.

    This week is what separates an MVP that converts from one that doesn't. The product can be small. It cannot feel half-finished.

    A short checklist of the things that always get missed:

    • Empty states for every list ("you have no widgets yet — create one →")
    • Skeleton loaders that don't flash for 50ms then disappear
    • 404 and error pages that match the brand
    • Email verification flow
    • Password reset (if you're not using magic links)
    • Org invite flow
    • Org deletion flow (yes, even at MVP — GDPR)

    Week 6 — production hardening

    Sentry. Logflare or Axiom. Uptime monitoring (BetterUptime). A backup strategy that you've actually restored once. The dev/staging/prod split. Secrets that aren't in the repo. A /health endpoint. Read replicas if your bill-pay queries are slow.

    Friday of week six, you put the marketing site at the apex, the app at /app, point a payment link at the marketing CTA, and tell the first customer the door is open.

    What's not in the six weeks

    This matters. The plan above explicitly excludes:

    • A custom design system. Use shadcn/ui and ship.
    • A mobile app. Web first. We have a framework for deciding mobile vs web — most B2B SaaS doesn't need a mobile app for 18 months.
    • A full SOC2 program. Audit logs and encryption-at-rest, yes. The whole compliance dance, no.
    • Internationalization. English-only is fine. Add next-intl later if you need it.
    • A native iOS app, a webhooks-out integration platform, single sign-on. All later.

    Trying to ship all of these in six weeks is how you ship none of them.

    Want us to build it with you?

    We've taken five SaaS products to production using this playbook. The next one could be yours.

    Scope your MVP

    FAQ

    Can I really ship a SaaS in 6 weeks?

    A real, paying-customer-ready SaaS in a focused vertical: yes. A horizontal "platform" that does five things: no. The plan above assumes a tight scope — one core feature done well, with the SaaS scaffolding around it solid enough to last.

    What if I'm not technical and I'm hiring this out?

    The six-week plan still applies. The questions you need to ask your engineering partner: how are you handling multi-tenancy, what's your billing webhook architecture, when are you wiring up the admin surface, and what's the audit-log strategy. If the answers are vague, that's the warning sign.

    Do I need Postgres? Why not MongoDB or DynamoDB?

    For a SaaS where you'll need analytics, billing reports, and arbitrary queries against tenant data, relational with row-level security is by far the lowest-risk choice. If you're sure your access patterns are key-value only, DynamoDB is fine — but most SaaS founders aren't sure, and Postgres covers both cases.

    When do I switch from Vercel to my own infra?

    When your Vercel bill exceeds the cost of an engineer-month, or when you have a latency budget Vercel can't meet. Usually 18-24 months in. Until then, Vercel pays for itself in deployment speed.

    What about Cloudflare Workers / Fly / Railway?

    All fine. The point is to pick one hosting platform and stick with it through the six weeks. Switching mid-build is a week of lost work.

    Should I use Supabase or roll my own Postgres?

    Supabase if you want the auth + storage + realtime bundled. Neon (or RDS) if you want plain Postgres. Both are good. Pick by which auth provider you prefer, not by which one has the better landing page.

    What's the typical six-week budget?

    For a YAEL build, six weeks of senior engineering is roughly £24-40k depending on scope. A solo founder doing it themselves is six weeks of focused work. A junior engineer doing it: 12+ weeks. The cost difference between senior and junior on an MVP is not the day rate — it's the rewrite.

    What if I miss the six-week mark?

    The plan absorbs a week of slip — that's why week 5 and week 6 are buffer-heavy. If you miss by more than two weeks, the scope was wrong. Cut features, not foundations.

    TagsSaaSMVPArchitectureStripeMulti-tenancy
    ServiceSaaS DevelopmentMVP DevelopmentStripe Integration
    Case studyCloudChat
    Next Stripe Billing vs Paddle vs LemonSqueezy for SaaS in 2026

    Keep reading

    PaymentsStripe Billing vs Paddle vs LemonSqueezy for SaaS in 2026An opinionated comparison of the three default billing platforms for B2B SaaS — pricing model coverage, MoR vs not, dev DX, and where each one breaks at scale.8 min readArchitectureMulti-tenant Postgres: row-level security explained (with real code)How RLS actually works in production multi-tenant SaaS — set policies, set the session variable, handle bypass, and avoid the three failure modes that bite teams at scale.9 min readArchitectureWebhook idempotency: the bug most teams shipWhy webhook handlers double-charge, double-grant, and double-cancel — and the three-line database pattern that fixes all of it.8 min read
    On this page
    • Week 0 — the boring decisions that save you in week 5
    • Week 1 — the multi-tenant foundation
    • Week 2 — billing wired up before the first feature
    • Week 3 — the product surface
    • Week 4 — the operator surface
    • Week 5 — the rough edges
    • Week 6 — production hardening
    • What's *not* in the six weeks
    • FAQ
    • Can I really ship a SaaS in 6 weeks?
    • What if I'm not technical and I'm hiring this out?
    • Do I need Postgres? Why not MongoDB or DynamoDB?
    • When do I switch from Vercel to my own infra?
    • What about Cloudflare Workers / Fly / Railway?
    • Should I use Supabase or roll my own Postgres?
    • What's the typical six-week budget?
    • What if I miss the six-week mark?

    YOUR APP EXPERT LTD

    71-75 Shelton Street, LONDON WC2H 9JQ, UK

    +44 20 1234 5678

    [email protected]

    Quick Links

    • Services
    • About Us
    • Portfolio
    • Blog
    • Contact

    Stay Connected

    Newsletter

    Stay updated with our latest innovations and insights.

    © 2026 YOUR APP EXPERT LTD. All rights reserved.

    Engineering the Future of Technology