We built a full-stack SaaS product - auth, database, payments, integrations, leaderboard, i18n, mobile UI, SEO - in a single sprint using AI as a build partner. Here's every step, decision, and dead end. By the end of this post, you'll think "wait, I could actually build that."
The Idea That Wouldn't Shut Up
Every indie hacker has the same problem: you want to build in public, but the tools suck.
You've got your MRR on a Google Sheet. Your equity is in a Notion doc nobody reads. Your expenses live in three different apps. And when someone asks "how's the startup going?" you have to piece together a story from six different tabs.
That's when the idea hit: what if Spotify, but for founders?
Not Spotify the product - Spotify the vibe. The dark design. The stats everywhere. The "Wrapped" that makes you feel like your year actually meant something. A profile page at indiefy.xyz/yourname that tells your whole founder story in one scroll - real MRR, real profit, real equity, real goals.
We called it Indiefy.
Step 0: Write the Spec Before Writing a Single Line of Code
The first thing we did wasn't open a terminal. We wrote an indiefy.md file - a proper product spec. Not a Notion doc, not a tweet thread. A real, structured document with:
- The concept in one sentence: "Spotify, but for indie hackers"
- The target audience (5 different profiles: solo founders, early employees with stock options, angels, advisors, minority partners)
- Every single feature, grouped into sections
- The monetization model (Free vs Pro)
- The tech stack
- A 5-day build timeline
This step is the one most builders skip. Don't skip it. When you have a spec, you have prompts. When you have prompts, AI can actually build your product instead of guessing.
Here's an excerpt from ours:
## Features
### Charges & P&L public
- Real Profit = MRR - project costs - pro-rata fixed costs
- Runway = savings / total monthly costs
- Break-even: at what MRR you cover everything
> First tool that shows REAL project profitability, not just gross MRR.
That last line became our north star for every design decision.
Step 1: The Design System First (Seriously, Do This First)
We started with design tokens, not code. The whole aesthetic was clear from day one: Spotify dark theme. Exactly this palette:
| Token | Value |
|---|---|
| Background | #0a0a0a |
| Cards |
#111111 and #1a1a1a
|
| Accent green | #1DB954 |
| Primary text | #FFFFFF |
| Secondary | #A3A3A3 |
| Borders | #2a2a2a |
Two fonts. That's it. DM Sans for body copy. Syne for numbers and titles - that Syne font on a big MRR number hits different.
The prompt we gave the AI was surgical:
Create the complete design system for Indiefy.
Stack: Next.js 16 App Router, Tailwind CSS v4, TypeScript strict.
Design: Spotify-inspired, dark mode only.
Colors: [exact hex values]
Typography: DM Sans (body) + Syne (stats/titles)
Generate: tailwind.config.ts, globals.css, Layout.tsx with left sidebar,
Card.tsx (variants: default/highlight/glass), Typography.tsx
Rules: NO generic Bootstrap-like styles. Server Components by default.
The AI spit out a complete design system in one shot. Because the prompt was specific. Garbage in, garbage out - and vice versa.
Step 2: The Database Schema - Think Hard Before You Click Enter
The spec: 8 models. User, Project, Expense, Revenue, Goal, Milestone, Badge, Follow.
We wrote out the entire Prisma schema by hand in the spec file before generating it. Every relation, every index, every enum. This took 45 minutes and saved us from three migrations later.
model Project {
id String @id @default(cuid())
userId String
name String
slug String @unique
status ProjectStatus // ACTIVE | ACQUIRED | DEAD | STEALTH
equityPercent Float?
estimatedValuation Float?
mrr Float @default(0)
isPublic Boolean @default(true)
showFinancials Boolean @default(false)
// ...
}
The showFinancials boolean was a key product decision: people can share their profile publicly without showing the money. This unlocks a whole segment of users who want visibility but not full transparency.
We also added the Integration model from day one - Stripe, RevenueCat, LemonSqueezy, Paddle - even before building the integration pages. Schema-first means you don't paint yourself into a corner later.
Step 3: Auth - The Part Everyone Dreads
Supabase Auth with Google + GitHub OAuth. Two provider buttons. That's it.
The interesting part wasn't the OAuth flow - it was the sync between Supabase Auth (which handles the JWT and session) and Prisma (which owns the business data). We wrote a route app/api/auth/sync/route.ts that fires after OAuth callback and ensures a User row exists in Postgres.
The middleware was three lines of logic:
// middleware.ts
const isProtectedRoute = matcher.some(path => req.nextUrl.pathname.startsWith(path))
const session = await getSession(req)
if (isProtectedRoute && !session) {
return NextResponse.redirect(new URL('/login', req.url))
}
Username validation deserves a mention: lowercase, alphanumeric + hyphens, 3-20 chars, unique. We validated this with Zod server-side and debounced API check client-side. Tiny detail, important UX.
Step 4: The Dashboard - Making Numbers Feel Alive
This is where the product really started to look like something.
Four KPI cards at the top:
- Total MRR
- Net Profit
- Total Users
- Paper Wealth (equity x valuations)
That last one is what makes Indiefy different from every other dashboard tool. Paper wealth - the sum of all your equity stakes multiplied by estimated valuations - is a number no other tool shows you. It might be zero. It might be fictional. But it's yours, and it's there, and it feels good.
The MRRChart component used Recharts AreaChart with a gradient fill. One snippet made the whole chart feel premium:
<defs>
<linearGradient id="mrrGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#1DB954" stopOpacity={0.3} />
<stop offset="95%" stopColor="#1DB954" stopOpacity={0} />
</linearGradient>
</defs>
Step 5: Projects - The Core Loop
The project form was the most complex UI we built. A slider for equity percentage (0-100) with a live calculation beneath it: "Your estimated stake: 47,000". A tech stack tag input. Status badges that actually looked like badges, not dropdowns.
The killer feature was the acquisition simulator. You type a valuation. The number updates instantly:
const yourShare = (estimatedSale * equityPercent) / 100
// Displayed in real-time as you type
Status colors:
| Status | Color | Vibe |
|---|---|---|
ACTIVE |
#1DB954 green |
Go time |
ACQUIRED |
Blue | Congrats |
DEAD |
Red | RIP |
STEALTH |
Gray | We see you |
After creating a project, we auto-generated a milestone: "Launch de projet". Small touch. Makes the product feel alive immediately.
Step 6: Expenses & Real P&L
This is the feature that makes Indiefy honest.
Most "MRR dashboards" just show you gross revenue. Indiefy shows you the real number:
Gross Revenue (MRR): 2,400
Project costs (Vercel, etc): - 89
Fixed costs (pro-rata): - 340
--------
Net Profit: 1,971
Margin rate: 82%
Runway: 14 months
Break-even MRR: 429
The getPnLSummary() server action ran a single Prisma query that joined revenues, project expenses, and fixed costs pro-rated across all active projects. The math isn't complex - it's just honest math nobody else was doing.
Step 7: Integrations - The Part That Made It Real
Instead of just supporting Stripe, we ended up integrating five payment providers:
- Stripe - OAuth Connect, read-only access
- RevenueCat - API key, for mobile apps
- LemonSqueezy - API key
- Paddle - API key
- Polar - the new open-source one
- Dodo Payments - emerging market focus
The git log tells the story:
e98928d Polar
2f675fd leaderboard
7457233 dodo payments
67366a0 paddle
829add8 coming soon integrations
Each integration followed the same pattern:
// lib/integrations/syncEngine.ts
async function syncProjectRevenue(projectId: string): Promise<SyncResult> {
const integration = await getActiveIntegration(projectId)
const data = await fetchFromProvider(integration)
await prisma.project.update({
where: { id: projectId },
data: { mrr: data.mrr, arr: data.arr, totalRevenue: data.totalRevenue }
})
await createRevenueEntry(projectId, data)
await checkAndGenerateMilestones(projectId)
await updateLastSynced(integration.id)
return { success: true, mrr: data.mrr, lastSyncedAt: new Date() }
}
API keys were stored encrypted with AES-256-GCM. Not optional. You're handling people's financial data.
The Vercel cron job synced everything at 6am daily:
// vercel.json
{
"crons": [{
"path": "/api/cron/sync-revenues",
"schedule": "0 6 * * *"
}]
}
Step 8: Leaderboard - The Viral Loop
The leaderboard was the feature we almost cut. "Do we really need a ranking system?"
Yes. We really did.
2f675fd leaderboard
a2e22b5 explore algo
a17f087 trending algo
Three commits across leaderboard features. The ranking algorithm wasn't just "sort by MRR" - we built a momentum score: a composite of MRR velocity (month-over-month growth %), user growth, and update frequency.
A 200 MRR project growing 30% monthly ranks higher than a 5k MRR project that hasn't been updated in three months.
The Explore page had its own trending algorithm. Same logic: recent activity + growth velocity > absolute numbers. This made the platform feel fair for people who are just getting started.
Step 9: Mobile - The Feature You Can't Skip Anymore
b8ff2e8 mobile ui
b4fc4a6 bottom nav
471fcce bubbles: mobile nav icons, verified-only badges
3a8410b loading pages
Mobile was a late sprint but not an afterthought. We replaced the left sidebar with a bottom navigation bar on mobile. Standard pattern, but the implementation matters: same server components, just conditional layout based on viewport. The Tailwind breakpoints did the heavy lifting.
The "bubble" nav icons made the mobile UI feel native rather than "responsive web." Five tabs: Dashboard, Explore, Profile, Goals, Settings.
Loading states were added systematically across all pages - skeleton screens, not spinners. The difference between a product that feels fast and one that feels slow is often just skeleton screens.
Step 10: i18n - Because Half Your Users Aren't English
d2ca272 i18n
ba6d47f i18n
40a060b Goal i18n
We used next-intl for internationalization. The routing structure:
app/
[locale]/
dashboard/
profile/
...
Every user-facing string became a key:
const t = useTranslations('dashboard')
// t('kpi.totalMrr') -> "Total MRR" or "MRR Total"
Three commits for i18n because the first one was "we need i18n", the second was "actually doing i18n", and the third was "ok goals weren't translated yet."
Step 11: Security, Legal, Pricing
b4fc4a6 legals
a78c03e SECU
5023241 pricing page
Security was a dedicated commit because integrations meant we had real sensitive data: OAuth tokens, API keys, payment provider access. We audited every route for:
- Auth checks on every server action
- User ownership verification before any data mutation
- Encrypted storage for all third-party credentials
- Rate limiting on public API endpoints
Legal pages (Privacy Policy, Terms of Service) were generated but reviewed. You need them. Especially when you're storing financial data and connecting to payment processors.
The pricing page was the last non-feature piece. Simple: Free (1 project, basic stats) vs Pro (~7/month, everything).
Step 12: SEO - The Long Game Starts at Launch
7928c7b SEO
eb6e47b SEO
2312193 Analytics
6e88960 Remove from sitemap the not public accounts
Dynamic OG images for every public profile (using @vercel/og). Proper <meta> tags. Sitemap generation that excluded private accounts - you don't want to index profiles people marked private.
Vercel Analytics + Speed Insights went in at the same time. You can't improve what you don't measure.
The Final Stack
After 115 commits, here's what Indiefy runs on:
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| Styling | Tailwind CSS v4 |
| Database | Supabase (PostgreSQL) |
| ORM | Prisma 7 |
| Auth | Supabase Auth (Google + GitHub OAuth) |
| Payments | Stripe (subscriptions) |
| Integrations | Stripe, RevenueCat, LemonSqueezy, Paddle, Polar, Dodo |
| Charts | Recharts |
| i18n | next-intl |
| Validation | Zod v4 |
| Toasts | Sonner |
| Deployment | Vercel |
| OG Images | @vercel/og |
| Analytics | Vercel Analytics + Speed Insights |
What Made This Possible
The honest answer: structured prompts written by a human who knew exactly what to build.
Every build step was a complete, specific prompt:
- "Generate X with these exact fields"
- "Use Zod for validation"
- "No
anytypes" - "Server Components by default, use client only when necessary"
- "Match this exact design system"
The AI didn't make product decisions. It executed them. The product thinking - what to build, why, in what order - that was 100% human.
Three things made the AI useful here:
- Specificity - vague prompts get vague code
- Existing design system - the AI respected our tokens because we defined them first
- Step-by-step - small, focused tasks beat "build me an app"
The Numbers
| Metric | Count |
|---|---|
| Git commits | 115 |
| Major features | ~15 |
| Payment integrations | 6 |
| Languages at launch | 2 |
| Bootstrap CSS used | 0 |
Could You Build This?
Yes. Genuinely, yes.
The technical complexity here isn't exceptional. Next.js App Router is mature. Supabase handles auth beautifully. Prisma removes the pain from database work. Stripe's documentation is excellent.
What's rare isn't the ability to build it - it's the discipline to spec it first, prompt it right, and ship before perfecting.
The features we shipped aren't perfect. Some UI could be cleaner. The mobile experience could be smoother. The analytics could be deeper. But the product exists, it works, and it's live.
Perfect is the enemy of shipped.
What's Next for Indiefy
- Indiefy Wrapped (December): your year as a founder, Spotify-style, shareable card
- Enjoy life
If you're an indie hacker who wants to display your real journey - not just a landing page, but actual MRR, real profit, equity you might never cash out but still own - create your profile at indiefy.xyz.
Top comments (0)