What You'll Build
By the end of this guide you will have a deployed, production-ready SaaS application with:
- User authentication (email/password + magic link)
- A protected dashboard
- AI-powered features using Groq
- A database with row-level security
- Rate limiting
- A working payment flow (optional)
This is not a toy. This is a real architecture used by companies generating thousands of dollars per month.

The Stack
Free Tier (Takes You to $10k MRR)
| Tool | What It Does | Free Limit | Cost After |
|---|---|---|---|
| Next.js 14 | Full-stack framework | Free forever | Free forever |
| Cloudflare Pages | Hosting + CDN | Unlimited bandwidth | Free forever |
| Supabase | Database + Auth + Storage | 500MB DB, 50k users | $25/month |
| Groq | AI inference | 14,400 req/day | ~$0.27/million tokens |
| GitHub Actions | Cron + CI/CD | 2,000 min/month | $4/month |
| Resend | Transactional email | 3,000 emails/month | $20/month |
| Upstash Redis | Rate limiting + cache | 10,000 req/day | $10/month |
Total free tier cost: $0/month
Paid Tier (When You're Scaling Past $10k MRR)
| Tool | Replaces | Cost | Why Upgrade |
|---|---|---|---|
| PlanetScale | Supabase DB | $39/month | Better performance at scale |
| Vercel | Cloudflare Pages | $20/month | Better Next.js support, analytics |
| OpenAI GPT-4o | Groq | Pay-per-token | More capable for complex AI tasks |
| Postmark | Resend | $15/month | Better deliverability |
| Railway | GitHub Actions jobs | $5/month | Always-on background workers |
Step 1: Scaffold the Project
Open your terminal and run:
npx create-next-app@latest my-saas \
--javascript \
--tailwind \
--app \
--no-src-dir \
--no-import-alias
cd my-saas
npm install @supabase/supabase-js @supabase/auth-helpers-nextjs groq-sdk
Your folder structure will look like this:
my-saas/
├── app/
│ ├── layout.jsx
│ ├── page.jsx
│ └── api/
├── components/
├── lib/
└── public/
Step 2: Set Up Supabase
Create the Project
- Go to supabase.com and create a free account
- Click New Project
- Choose a name, database password, and region closest to your users
- Wait 2 minutes for the project to spin up
Get Your Keys
Go to Settings → API and copy:
- Project URL
anonpublic keyservice_rolesecret key (never expose this in the browser)
Create Your .env.local File
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_KEY=your-service-role-key
GROQ_API_KEY=your-groq-key
NEXT_PUBLIC_SITE_URL=http://localhost:3000
Create the Database Schema
Go to SQL Editor in Supabase and run:
-- User profiles (extends auth.users)
create table profiles (
id uuid primary key references auth.users(id) on delete cascade,
email text,
plan text default 'free',
created_at timestamptz default now()
);
-- Auto-create profile when user signs up
create or replace function handle_new_user()
returns trigger as $$
begin
insert into profiles (id, email)
values (new.id, new.email);
return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure handle_new_user();
-- Enable RLS
alter table profiles enable row level security;
-- Users can only see and edit their own profile
create policy "users own profile"
on profiles for all
using (auth.uid() = id);
Step 3: Wire Up Authentication
Create the Supabase Client
// lib/supabase.js
import { createClient } from '@supabase/supabase-js'
// Server-side — full database access
export const supabaseAdmin = createClient(
process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.SUPABASE_SERVICE_KEY
)
// Client-side — limited by RLS
export const supabasePublic = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
)
Create the Auth Page
// app/auth/page.jsx
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { supabasePublic } from '@/lib/supabase'
export default function AuthPage() {
const router = useRouter()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [mode, setMode] = useState('signin')
const [error, setError] = useState('')
async function submit() {
setError('')
const { error } =
mode === 'signup'
? await supabasePublic.auth.signUp({ email, password })
: await supabasePublic.auth.signInWithPassword({ email, password })
if (error) { setError(error.message); return }
router.push('/dashboard')
}
return (
<div className="min-h-screen flex items-center justify-center">
<div className="w-full max-w-sm space-y-4">
<h1 className="text-2xl font-bold">
{mode === 'signin' ? 'Sign In' : 'Create Account'}
</h1>
<input
type="email"
placeholder="Email"
value={email}
onChange={e => setEmail(e.target.value)}
className="w-full border rounded-lg px-4 py-2"
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={e => setPassword(e.target.value)}
className="w-full border rounded-lg px-4 py-2"
/>
{error && <p className="text-red-500 text-sm">{error}</p>}
<button onClick={submit} className="w-full bg-black text-white py-2 rounded-lg">
{mode === 'signin' ? 'Sign In' : 'Sign Up'}
</button>
<button onClick={() => setMode(m => m === 'signin' ? 'signup' : 'signin')}
className="text-sm text-gray-500 w-full text-center">
{mode === 'signin' ? 'Need an account?' : 'Already have one?'}
</button>
</div>
</div>
)
},
Protect Routes With Middleware
// middleware.js
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
export async function middleware(req) {
const res = NextResponse.next()
const supabase = createMiddlewareClient({ req, res })
const { data: { session } } = await supabase.auth.getSession()
if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/auth', req.url))
}
return res
},
export const config = { matcher: ['/dashboard/:path*'] }
Step 4: Add AI Features With Groq
Get your free API key at console.groq.com. The free tier gives you 14,400 requests per day — enough to power a real product.
// app/api/generate/route.js
import Groq from 'groq-sdk'
import { supabaseAdmin } from '@/lib/supabase'
const groq = new Groq({ apiKey: process.env.GROQ_API_KEY })
export async function POST(req) {
// Verify the user is logged in
const authHeader = req.headers.get('authorization')
const token = authHeader?.split(' ')[1]
if (!token) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
const { data: { user } } = await supabaseAdmin.auth.getUser(token)
if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 })
const { prompt } = await req.json()
const completion = await groq.chat.completions.create({
model: 'llama-3.3-70b-versatile',
messages: [
{
role: 'system',
content: 'You are a helpful AI assistant.'
},
{ role: 'user', content: prompt }
]
})
return Response.json({
result: completion.choices[0].message.content
})
},
Step 5: Deploy to Cloudflare Pages
Connect to GitHub
- Push your code to a GitHub repository
- Go to pages.cloudflare.com
- Click Create a Project → Connect to Git
- Select your repository
Configure Build Settings
| Setting | Value |
|---|---|
| Framework preset | Next.js |
| Build command | `npx next build` |
| Output directory | `.next` |
| Node.js version | `20` |
Add Environment Variables
In Settings → Environment Variables, add all variables from your .env.local file. Make sure to add both the NEXT_PUBLIC_ variables AND the server-side secrets.
Update Your Site URL
Once deployed, update NEXT_PUBLIC_SITE_URL from http://localhost:3000 to your actual Cloudflare Pages URL.
Step 6: Add Rate Limiting
Without rate limiting, a single bad actor can exhaust your Groq free tier in minutes. Add this to every API route:
// lib/ratelimit.js
const requests = new Map()
export function rateLimit(identifier, maxPerMinute = 10) {
const now = Date.now()
const window = now - 60_000
const history = (requests.get(identifier) || [])
.filter(ts => ts > window)
if (history.length >= maxPerMinute) {
return { success: false, remaining: 0 }
}
requests.set(identifier, [...history, now])
return { success: true, remaining: maxPerMinute - history.length - 1 }
},
Use it in any route:
const ip = req.headers.get('x-forwarded-for') || 'unknown'
const result = rateLimit(ip)
if (!result.success) {
return Response.json(
{ error: 'Too many requests. Slow down.' },
{ status: 429 }
)
},
Common Mistakes to Avoid
| Mistake | What Goes Wrong | Fix |
|---|---|---|
| Exposing `SUPABASE_SERVICE_KEY` in the browser | Anyone can bypass RLS and read all user data | Only use service key in server-side API routes |
| No rate limiting | Free API limits exhausted by bots | Add rate limiting to every AI endpoint |
| Skipping RLS | Any user can read other users' data | Enable RLS on every table from day one |
| No error boundaries | One crashed component breaks the whole page | Wrap dynamic sections in ` |
| Large bundle sizes | Slow loading on mobile networks | Use dynamic imports for heavy components |
What to Build Next
Once the foundation is working, these are the next features that turn a demo into a business:
- Onboarding flow — ask users their goal in 3 questions, personalise their experience
- Usage tracking — track which features users actually use
- Email drip sequence — automated 7-email onboarding sequence via Resend
- Stripe or Paystack integration — monetise your active users
- Admin dashboard — see all users, usage, revenue in one place
This stack — Next.js + Supabase + Groq + Cloudflare — is how Kivora itself is built. It costs $0 per month until you're making real money, and it scales comfortably past $10,000 MRR before you need to think about upgrading anything.

