Case Study
Bluumo: Two-Sided Wellness Marketplace
Scale
181,000+
Lines of TypeScript
482
Files
120+
Screens / pages
123
Reusable components
80
Edge functions (Deno)
112+
Service / utility modules
2,199+
Git commits
2
Active payment providers
3 / 7
Languages (live / infra)
3,937
Translation keys
13
i18n namespaces
The Problem
The founder was running a wellness services startup with a WordPress brochure site and zero operational capability. Every phone call, appointment, and invoice was handled manually. They needed a production-grade two-sided marketplace - customers finding and booking wellness providers, providers managing their calendars and earnings - across web and native mobile, fast.
What Was Built
Customer Interface (11+ screens)
- ·Service browsing with category, price, and duration filters
- ·Provider discovery with ratings and comparison tools
- ·Location-based search with interactive maps
- ·Real-time availability and instant booking confirmation
- ·Finnish banking transfers, card payments, MobilePay, Edenred wellness cards, KELA reimbursement support
- ·Recurring booking management
- ·Gift card purchasing with PDF generation
- ·Referral program with automated rewards
- ·Bluumo Plus subscription tier
- ·In-app voice calling via Agora
- ·"Helmi" - AI concierge powered by Claude for booking assistance
Provider Portal (13+ screens)
- ·Document-based verification onboarding
- ·Multilingual profile management
- ·Visual availability calendar with Google Calendar sync
- ·Dynamic pricing (peak hours, discounts, time-based adjustments)
- ·Real-time booking notifications
- ·Earnings tracking and payout history (Holvi payout infrastructure built, not yet active)
- ·Booking status progression through sub-steps with visual completion indicators
- ·'Mark Complete' action to finalize the provider-side booking workflow
- ·Review management tools and service area configuration
Corporate B2B Portal
- ·Corporate registration with approval workflows
- ·Employee management with bulk operations
- ·Wellbeing day booking (individual and bulk)
- ·Cost center tracking, budget management, manager approval queues
- ·Invoice analytics, usage reporting, automated invoice generation
- ·Quote request system
Admin Dashboard (12 screens)
- ·Platform-wide KPIs and analytics
- ·Provider verification management
- ·Booking oversight and support ticket system
- ·Blog publishing with SEO metadata
- ·Campaign and promotion management
- ·Refund and invoice processing
Screenshots
Technical Architecture
Security Audit (June 2026)
An independent security review in June 2026 identified and resolved a series of vulnerabilities before they reached production scale.
- Server-side payment verification - amounts re-derived from DB, no client-trusted prices
- Booking price floor blocks under-threshold exploit attempts
- JWT verification added to all edge functions
- Promo codes, gift card, and voucher credits honored server-side
- AI chat scoped to session owners via RLS
- Rate limiting on API endpoints
- Blog content XSS sanitization
- GDPR: delayed deletions executed, full data export delivered
- Supabase security advisor warnings: 105 to ~36
Performance
- ->Interaction Next Paint (INP) improved from 1,280ms to under 200ms via StyleSheet memoization
- ->Heavy operations deferred for immediate visual feedback
- ->OTA update support via EAS for instant production patches without app store review
What I Learned
A marketplace moves other people's money, which sharpens the consequences of being subtly wrong. The ones that stung most were all quiet.
A default that means "unset" has to be NULL
Every booking was taking a flat 20% commission. The pricing trigger branched on whether commission_rate was supplied: non-null meant an explicit B2B override, so it skipped both the GMV tier (24/22/20/18) and the Bluumo Pro discount. The column had a non-null default, so the value was never absent and the override branch always won. Nothing errored, the arithmetic was correct, and every row in the table was wrong. Dropping the default let an omitted rate arrive as NULL so the trigger computes authoritatively. If a branch keys on presence, then presence, not a value, is what the schema has to be able to express.
A permissive fallback hides a feature that never worked
The distance filter did not filter. Providers were included when they had no coordinates, which is a sensible default, and no code path ever wrote provider latitude or longitude, so every provider took the fallback and appeared at any radius. The feature looked implemented, returned plausible results, and was inert. A fallback that swallows the empty case will keep the empty case from ever being noticed.
Revoking a grant changes what select('*') means
Locking financial columns behind column-level grants broke reads that asked for everything. PostgREST cannot silently narrow a select('*') to the columns a role may actually see; without table-level SELECT it raises instead. The fix is to name columns explicitly at every call site, which is better practice anyway, but the lesson is that tightening permissions is a client-side change too, not only a database one.
Degrade the optional parts, not the transaction
Push notifications on this platform return a bare 500. Rather than let that surface as a failure, the flows that use them fall through to email and in-app, and a nullable provider contact address falls back to the account email rather than dropping the message. Deciding upfront which parts of a flow are allowed to fail quietly, and which must never, is most of what makes a payment path feel reliable.