Skip to content

Case Study

Bluumo: Two-Sided Wellness Marketplace

Role: Solo Full-Stack Developer & UX DesignerDuration: ~12 weeks (initial build)Client: Bluumo (Finnish wellness startup)Status: Live demo environment; native apps in beta
Expo 54React NativeReact 19TypeScriptSupabaseVisma PayEdenredClaude APIAgoraUpCloud

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

FrontendExpo 54, React Native, React 19, TypeScript (strict mode) - single codebase for iOS, Android, and web via Expo Router
BackendSupabase (PostgreSQL, Auth) with 80 Deno edge functions and Row Level Security across four user roles
PaymentsVisma Pay (Finnish banking, card, MobilePay), Edenred wellness cards; server-side amount verification and booking price floor; promo codes, gift cards, and social fund credits applied server-side; Smartum/Epassi gated pending credentials
EmailMailgun (EU) with 12+ automated email types
AIClaude API - Helmi concierge across three languages
VoiceAgora SDK with secure token generation
i18n13 namespaces, 3,937 active translation keys - 3 languages live (EN/FI/SV), 7 in infrastructure
SEOPer-route metadata, OG/Twitter cards, dynamic sitemap, JSON-LD for blog posts and provider pages, FAQPage schema, PNG OG image via sharp
DeploymentUpCloud + Caddy with zero-downtime blue/green atomic deploys; EAS for native app builds and OTA updates with runtimeVersion policy

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.

Links