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 |
| 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
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
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
})
}
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 │
└─────────────────────────────────────┘
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
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
- SEO pages earlier. They're low effort, high leverage. Should have been day 1.
- Start Twitter sooner. Build-in-public compounds. I started late.
-
Don't spend 3 hours debugging Worker
windowaccess. 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.
Tags: #webdev #javascript #nextjs #saas #buildinpublic
Top comments (0)