Pin photos to places on a map. A multi-tenant SaaS with Stripe subscriptions, content moderation, collaboration, and a privacy model. My first product with a “buy” button. Live at geoscrapbook.com.
This happened by chance. I was at work when I saw that Google's Antigravity editor dropped. After work, I downloaded it and stared at the screen. What the hell was I going to build?
Well, my girlfriend and I were getting ready to take a trip to Europe. We had a bunch of specific places on our itinerary, and I thought about how when we're over there, our families are definitely going to want to see photos. But it's hard to share 100 photos from 15 different places and have the viewer really appreciate the nature of the trip. So I thought — how great would it be if you could pin the photos on a map? Apple has a feature like this in Photos, but you can't share it. It would be great if you could.
So I built a web app with an auth paywall where we pinned all our Europe photos. We shared the link with our relatives and they loved being able to see not only what we did but where we did it. It was easy to stand up — I had experience at home and in a professional setting developing map-based web applications. When someone said “this is so cool, I would use this” — I decided to make GeoScrapbook.
It's funny — this was the first thing I've ever deployed with a “buy” button, and it's true what they say: every software engineer that makes an app and realizes they've created a marketing job for themselves has a moment of clarity. Check it out, we have a free tier ;). After all, every journey has a thousand stories.

Photos are pinned to coordinates on a Mapbox map. Clusters expand as you zoom in. Tap a pin to see the photo, its caption, and who added it. The map is the primary interface, not a gallery with a map tacked on. You browse by place, not by date.
Each scrapbook is an isolated container with its own photos, collaborators, and privacy settings. Users create multiple scrapbooks — one per trip, one per city, one for a shared group. The data model is fully multi-tenant: every query is scoped, every permission check is per-scrapbook. There's no leakage between tenants.
Scrapbooks can be shared via invite links with per-role permissions. Owners control who can view, who can add photos, and who can manage the scrapbook. The invite system generates unique links per role, so you can share a view-only link publicly while keeping edit access restricted.

Every uploaded image passes through Google Cloud Vision SafeSearch before it's visible to other users. The moderation pipeline runs asynchronously — the upload completes instantly, the image enters a pending state, and SafeSearch classification happens in the background. Flagged content is quarantined automatically. This isn't optional when you're running a platform where users upload photos to shared spaces.
Free accounts get limited scrapbooks and storage. Paid plans unlock more capacity. The subscription system is fully integrated with Stripe — checkout, customer portal, webhook handling for subscription lifecycle events (upgrades, downgrades, cancellations, failed payments). Entitlements are checked server-side on every relevant operation.
┌──────────────┐ ┌──────────────────────────┐
│ React SPA │ │ Firebase Backend │
│ (Mapbox GL) │───────►│ │
└──────────────┘ │ ┌────────────────────┐ │
│ │ Firestore │ │
│ │ (scrapbooks, │ │
│ │ photos, users) │ │
│ └────────────────────┘ │
│ │
│ ┌────────────────────┐ │
│ │ Cloud Storage │ │
│ │ (signed URLs) │ │
│ └────────────────────┘ │
│ │
│ ┌────────────────────┐ │
│ │ Cloud Functions │ │
│ │ ┌──────────────┐ │ │
│ │ │ SafeSearch │ │ │
│ │ │ moderation │ │ │
│ │ └──────────────┘ │ │
│ │ ┌──────────────┐ │ │
│ │ │ Stripe │ │ │
│ │ │ webhooks │ │ │
│ │ └──────────────┘ │ │
│ └────────────────────┘ │
└──────────────────────────┘The backend is entirely Firebase — Firestore for structured data, Cloud Storage for images, Cloud Functions for server-side logic. Photos are served via signed URLs with expiration, so there's no public bucket access. The Firestore security rules enforce the multi-tenant model: every read and write is validated against the user's role in the target scrapbook.
Cloud Functions handle the async operations that don't belong in the client: SafeSearch classification, Stripe webhook processing, invite link generation, and storage quota enforcement. The functions are triggered by Firestore writes and HTTP endpoints, keeping the React frontend thin.
User photos are private by default. Every image access goes through a signed URL with a time-limited token. This means even if someone intercepts a URL, it expires. The privacy model is enforced at the storage layer, not just the application layer. This was a non-negotiable requirement for a photo platform.
For a SaaS with auth, file storage, real-time data, and serverless functions, Firebase provides all the infrastructure. The security rules language is expressive enough for the multi-tenant access model. The tradeoff is vendor lock-in, but the time saved on infrastructure means more time on the product.
SafeSearch classification takes 1-3 seconds. Making users wait for that on every upload would destroy the experience. Instead, photos upload instantly into a pending state, moderation runs in a Cloud Function trigger, and the photo becomes visible only after it passes. The user barely notices the delay, and the platform stays clean.

I'd add automatic location extraction from photo EXIF data. Right now you place pins manually. Most phone photos have GPS coordinates embedded — reading that metadata on upload would auto-place 90% of photos without any user input.
I'd also launch a waitlist first. I read a post on X years ago from a guy who said “I don't launch products, I launch waitlists” — at the time I thought it was kind of funny. Now I get it.