DEV Community

吴美良
吴美良

Posted on

# I Built a $0/month SaaS — Full Stack Breakdown

3,700+ images compressed. 178 users. 1 Pro sale. Total monthly hosting bill: $0.00.

I built CompressFast — a privacy-first image compressor that runs entirely in the browser. No server-side processing. No uploads. No database costs.

Here's exactly how the stack works, why it costs nothing, and what I'd change.


The $0 Stack

Layer Service Cost
Hosting Vercel (Hobby) $0
DNS Cloudflare $0
Database Upstash Redis (free tier) $0
Email Resend (100/day free) $0
Payments Creem (5% per sale) $0 fixed
Analytics Vercel Analytics (free tier) $0
Domain compressfast.site ~$3/yr
Total monthly burn $0.00

The only real cost is the domain renewal. Everything else rides on generous free tiers.


Architecture: Why There's No Server Bill

The key insight: image compression doesn't need a server.

User's browser
    │
    ├─ Page loads (static HTML from Vercel CDN)
    ├─ User drops 30 images
    ├─ Web Workers spawn → Canvas API compresses locally
    ├─ Blob URLs generated → ZIP created client-side
    └─ Download starts instantly
Enter fullscreen mode Exit fullscreen mode

No upload. No backend processing queue. No temporary storage. The browser does everything.

The only backend code is licensing

/api/create-license  → Generate activation code → Store in Redis
/api/verify-license  → Check code validity → Track device fingerprint
/api/creem           → Webhook: payment → issue license → email via Resend
/api/resend-license  → "Forgot your code?" → email it back
Enter fullscreen mode Exit fullscreen mode

Three tiny API routes. Redis stores license keys + device IDs + stats. That's it.


Multi-Worker Pool (0 Cost, 4x Speed)

Instead of spawning/destroying workers per task, I keep a pool alive:

const POOL_SIZE = Math.min(4, navigator.hardwareConcurrency || 2)
const workers: Worker[] = []

for (let i = 0; i < POOL_SIZE; i++) {
  const w = new Worker(new URL('./worker.ts', import.meta.url))
  workers.push(w)
}

// Round-robin dispatch — no load balancer needed
function dispatch(task: CompressTask): Promise<CompressResult> {
  const worker = workers[taskIndex++ % POOL_SIZE]
  return new Promise(resolve => {
    worker.onmessage = (e) => resolve(e.data)
    worker.postMessage(task.buffer, [task.buffer]) // transferable = zero-copy
  })
}
Enter fullscreen mode Exit fullscreen mode

postMessage(buffer, [buffer]) with transferable objects means zero-copy handoff. The buffer is literally moved, not cloned. 2x faster for large images.


The Compression Pipeline (All Client-Side)

┌─────────────────────────────────────┐
│         Web Worker Pipeline          │
├─────────────────────────────────────┤
│ 1. decodeImage()                    │
│    createImageBitmap → OffscreenCanvas
│                                     │
│ 2. resize?                          │
│    calcResizeDims + multi-step scale │
│                                     │
│ 3. transform? (rotation + flip)     │
│    Canvas matrix transform           │
│                                     │
│ 4. watermark? (text or image)       │
│    Overlay render                    │
│                                     │
│ 5. encode()                         │
│    Canvas API: JPEG/WebP/PNG        │
│    WASM: AVIF (@jsquash), PNG (oxipng) │
│                                     │
│ 6. postMessage back to main thread  │
└─────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Full pipeline runs in 50-200ms per image depending on size and format.


License Keys Instead of User Accounts

I did NOT want to build auth. So the Pro model is dead simple:

User pays Creem ($24.99 lifetime)
    → Webhook fires
    → Server: generate "XXXX-XXXX-XXXX" code
    → Store in Redis: code → {email, devices[], created_at}
    → Resend: email with activation code
    → User enters code in app
    → Verified, device fingerprint registered
    → Max 5 devices per code
Enter fullscreen mode Exit fullscreen mode

No passwords. No sessions. No user table. No GDPR headaches. The activation code IS the account.


SEO Landing Pages (11 Pages, $0 Hosting)

11 SEO tool pages pre-rendered at build time. Each page targets a specific keyword:

Page Target
/compress-png "compress PNG online"
/compress-jpeg "compress JPEG online"
/compress-webp "WebP compressor"
/compress-svg "SVG optimizer"
/remove-metadata "remove EXIF data"
/vs-tinypng "TinyPNG alternative"
... (6 more)

All static HTML. Vercel CDN handles them globally for free. Google is slowly indexing them.


Real Numbers (After 1 Month)

Metric Value
Total PV 850
Total UV 178
Images compressed 3,716
Pro sales 1 ($24.99)
Monthly cost $0.00

Not retiring on it. But it proves the model works: $0 burn rate means every sale is pure profit.


What I'd Do Differently

  1. SEO pages earlier. They're low effort, high leverage. Should have been day 1.
  2. Start Twitter sooner. Build-in-public compounds. I started late.
  3. Don't spend 3 hours debugging Worker window access. It's always the same bug.

The Stack in One Line

Next.js + Web Workers + Canvas API + Upstash Redis + Creem + Vercel = $0/month SaaS.

No servers. No uploads. No excuses.

compressfast.site · GitHub


Tags: #webdev #javascript #nextjs #saas #buildinpublic

Top comments (0)