>It's been over half a year since I last wrote anything here — not because work or life got especially busy, just negligence. Noting it down as a reminder to myself: keep writing.
<ImageGallery columns={2} images={[ { src: "/posts/pixblur-recap/stripe-console.png", alt: "" }, { src: "/posts/pixblur-recap/google-console.png", alt: "" } ]} />
A few days ago I opened Stripe and noticed that PixBlur, the first SaaS product I launched back in December 2025, had finally gotten its first paying customer. I didn't feel excited. It's been almost a year since launch, daily active users hover around twenty or so, and they're all on the free plan. By any measure, this product has failed.
Why I Built PixBlur
When I was writing blog posts, I kept running into the need to redact things in images. On mobile there are already plenty of apps for this — most of them use iOS's built-in Vision framework or CoreML to do text (OCR) and face detection entirely on-device, no network, no server uploads. But I write on my laptop, which meant redacting on my phone and then transferring the image back to the computer every single time. Annoying. I also looked at some web-based redaction tools and found them all bloated — complex interfaces, feature-collection sites where redaction is just one item in a long menu, the page cluttered with noise. So I decided to build something focused on exactly one thing. It was also my first step into indie development, something I wanted to do for the experience and learning.
Problems I Ran Into During Development • Tech Stack Decisions
Controlling costs is critical for indie development — you can't be spending real money before the product makes any. After research, a fair amount of searching, and some help from AI, I landed on:
| Layer | Technology | |-------|------------| | Frontend | Next.js 15 + React 19 + TypeScript 5.8 + Tailwind CSS 4 | | UI | shadcn/ui (Radix UI) — 27+ components | | State | Zustand 5 (global) + TanStack Query v5 (server) | | Auth | BetterAuth 1.3 (Google/Microsoft/Apple OAuth) | | Database | Supabase PostgreSQL + Cloudflare Hyperdrive | | Email | Resend + React Email | | Image | Konva.js 10 + Canvas API | | Payments | Stripe (Checkout + Webhook + Customer Portal) | | Deploy | Cloudflare Workers (@opennextjs/cloudflare) | | CI/CD | GitHub Actions |
Early on, all of this costs almost nothing — every provider here offers a generous free tier. One specific call worth explaining: I didn't use Supabase's built-in auth and implemented it myself with BetterAuth instead. Supabase auth is billed separately, and even though it has a free tier, I figured the cost would grow with users and migrating auth later would be a real headache. Better to own it from the start. • Comparing Web OCR Approaches
iOS's Vision framework wraps text recognition and face detection in a single local API. On the web, these are two completely separate problems that need to be solved independently.
Client-side approaches (local, no network)
| Option | Privacy | Multi-language | Bundle size | Notes | |--------|---------|----------------|-------------|-------| | Tesseract.js | ✅ | ⭐⭐⭐ | 30 MB+ (Chinese pack) | Supports 100+ languages, but each language needs its own separate download | | Transformers.js | ✅ | ⭐⭐⭐⭐ | Hundreds of MB | Models too large for Web use | | Shape Detection API | ✅ | ❌ | Zero dependency | Chrome experimental only, language support unpredictable, not production-ready |
Face detection is a separate story entirely — OCR doesn't touch faces at all. You'd need to pull in something like face-api.js or MediaPipe, both neural-network-based libraries that add another few MB to tens of MB of dependencies on top.
Then there's the deeper problem: OCR just gives you raw characters. It has no idea which ones need to be redacted. Phone numbers can be caught with a regex, but names, addresses, and unstructured sensitive content require a ded