Loading...
Loading...
Build and monetize a full-stack SaaS for $0/month using Next.js on Vercel, Supabase for auth + database, and Stripe for payments. Complete guide with free tier limits and real setup steps.
I run three SaaS products. My total infrastructure cost for the first 6 months of each? $0.00. Zero. Zilch. Nada. 🎉
Not because I'm cheap (okay, maybe a little). Because the free tiers in 2026 are genuinely insane. You can build a production SaaS, get your first 1,000 users, and not pay a single cent until you're actually making money.
Let me show you the exact stack, the free tier limits, and how to set it all up.
| Service | Role | Free Tier |
|---|---|---|
| Next.js | Frontend + API | Open source (free forever) |
| Vercel | Hosting + Edge | 100GB bandwidth, serverless functions |
| Supabase | Database + Auth + Storage | 500MB DB, 50K auth users, 1GB storage |
| Stripe | Payments | No monthly fee, 2.9% + 30c per transaction |
| Resend | Transactional email | 3,000 emails/month |
| Upstash | Redis (rate limiting) | 10K commands/day |
Total monthly cost: $0 until you need to scale. And by then, you should have revenue. 💰
Before we build, let's be honest about what "free" actually gets you:
| Service | What You Get Free | When You'll Outgrow It |
|---|---|---|
| Vercel Hobby | 100GB bandwidth, 100 deployments/day, Edge + Serverless | ~10K daily active users |
| Supabase Free | 500MB Postgres, 2 projects, 50K MAU auth, 1GB file storage, 500K Edge Function invocations | ~5K active users with moderate data |
| Stripe | $0 base cost | Never (you pay per transaction only) |
| Resend | 3,000 emails/month, 1 custom domain | ~1K users (onboarding + notifications) |
| Upstash | 10K commands/day, 256MB | ~2K users with rate limiting |
The honest answer: this stack comfortably handles 1,000-5,000 monthly active users before you need to upgrade anything. For a new SaaS, that's more than enough to validate your idea and start generating revenue.
npx create-next-app@latest my-saas --typescript --tailwind --app --src-dir
cd my-saasInstall the essentials:
npm install @supabase/supabase-js @supabase/ssr stripe @stripe/stripe-js resendYour project structure:
src/
app/
(auth)/
login/page.tsx
signup/page.tsx
callback/route.ts
(dashboard)/
dashboard/page.tsx
settings/page.tsx
billing/page.tsx
api/
stripe/webhook/route.ts
contact/route.ts
layout.tsx
page.tsx
lib/
supabase/
client.ts # Browser client
server.ts # Server client
middleware.ts # Auth middleware
stripe.ts
resend.ts
components/
middleware.tsCreate a project at supabase.com. Grab your project URL and anon key.
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIs...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIs...Create the Supabase client:
// src/lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr';
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}// src/lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
},
},
}
);
}Set up the database schema in Supabase SQL editor:
-- Users profile (extends Supabase auth.users)
create table public.profiles (
id uuid references auth.users on delete cascade primary key,
full_name text,
avatar_url text,
stripe_customer_id text unique,
subscription_status text default 'free',
subscription_plan text,
created_at timestamptz default now()
);
-- Enable Row Level Security
alter table public.profiles enable row level security;
-- Users can read/update their own profile
create policy "Users can view own profile"
on profiles for select using (auth.uid() = id);
create policy "Users can update own profile"
on profiles for update using (auth.uid() = id);
-- Auto-create profile on signup
create function public.handle_new_user()
returns trigger as $$
begin
insert into public.profiles (id, full_name, avatar_url)
values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();// src/lib/stripe.ts
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2025-12-18.acacia',
typescript: true,
});
// Create a checkout session
export async function createCheckoutSession(
customerId: string,
priceId: string,
userId: string
) {
return stripe.checkout.sessions.create({
customer: customerId,
line_items: [{ price: priceId, quantity: 1 }],
mode: 'subscription',
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing?canceled=true`,
metadata: { userId },
});
}The webhook handler (this is the critical piece):
// src/app/api/stripe/webhook/route.ts
import { stripe } from '@/lib/stripe';
import { createClient } from '@supabase/supabase-js';
import { headers } from 'next/headers';
const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
export async function POST(req: Request) {
const body = await req.text();
const headersList = await headers();
const signature = headersList.get('stripe-signature')!;
const event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object;
await supabaseAdmin
.from('profiles')
.update({
stripe_customer_id: session.customer as string,
subscription_status: 'active',
subscription_plan: 'pro',
})
.eq('id', session.metadata?.userId);
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object;
await supabaseAdmin
.from('profiles')
.update({ subscription_status: 'free', subscription_plan: null })
.eq('stripe_customer_id', subscription.customer as string);
break;
}
}
return new Response('OK', { status: 200 });
}// src/middleware.ts
import { createServerClient } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
let response = NextResponse.next({ request });
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: () => request.cookies.getAll(),
setAll: (cookiesToSet) => {
cookiesToSet.forEach(({ name, value, options }) => {
response.cookies.set(name, value, options);
});
},
},
}
);
const { data: { user } } = await supabase.auth.getUser();
// Protect dashboard routes
if (request.nextUrl.pathname.startsWith('/dashboard') && !user) {
return NextResponse.redirect(new URL('/login', request.url));
}
return response;
}
export const config = {
matcher: ['/dashboard/:path*', '/settings/:path*', '/billing/:path*'],
};# Push to GitHub, then:
vercel --prodAdd your environment variables in Vercel's dashboard. Set up the Stripe webhook URL to https://your-domain.com/api/stripe/webhook. Done. 🚀
Here's the pricing strategy that works for indie SaaS:
| Tier | Price | What They Get |
|---|---|---|
| Free | $0/month | Core features, limited usage |
| Pro | $19/month | Full features, higher limits |
| Team | $49/month | Collaboration, priority support |
With 100 paying users at $19/month, you're making $1,900/month with infrastructure costs of approximately $25/month (Supabase Pro + Vercel Pro). That's a 98.7% margin. 📈
When you outgrow free tiers:
| Service | Free → Paid | Monthly Cost |
|---|---|---|
| Vercel Pro | At ~10K users | $20/month |
| Supabase Pro | At ~5K users | $25/month |
| Resend Pro | At 3K+ emails | $20/month |
| Upstash Pro | At 10K commands | $10/month |
| Total | ~$75/month |
$75/month to serve thousands of users. Compare that to the old days of managing your own VPS, database server, email service, and Redis instance. We're living in the golden age of indie development. ✨
STRIPE_WEBHOOK_SECRET, not NEXT_PUBLIC_STRIPE_WEBHOOK_SECRETThe 2026 indie hacker stack is:
Total cost: $0 until you have revenue. Then ~$75/month when you're making thousands.
Stop planning. Start building. Your SaaS idea won't validate itself. 💪
What's your go-to indie stack? I've heard great things about the Remix + Planetscale + Lemon Squeezy combo too. Share your setup!
How I built a dark-theme glassmorphism developer portfolio with 3D liquid metal visuals, canvas particle effects, and smooth scroll animations — all with Next.js 16, React 19, and zero compromises on performance.
The dominant stack costs $0/month to host. Realistic timelines are 2-6 months to first revenue. Here's the complete, no-fluff playbook for going from zero to $10K MRR as a solo developer.
Every year someone declares WordPress dead. Every year it still powers 43% of the web. Let's settle this with data, not hot takes.