LoHay: geo-aware marketplace shipped across web and two native apps
3-sided marketplace on one Django backend: buyer web, seller/inventory app, courier/delivery app. PostGIS search, AI discovery, multi-tenant theming — production-ready.
The challenge
LoHay set out to launch a marketplace where buyers discover independent stores nearby, with AI helping them compare products. Three sides, three surfaces: buyers on the web, store owners managing inventory on a native app, couriers running deliveries on a native app. The technical surface was large from day one: geo search, AI-assisted discovery, multi-tenant branding, payments, payments splits between buyer / seller / courier. The team was small, the deadline was short, and every week of delay burned runway.
What we built
Delivered a unified platform where one Django backend serves all three surfaces. Web client (Next.js 15 + Tailwind v4 + shadcn/ui) for buyers to discover stores and purchase. Two Flutter apps — one for store owners to manage inventory and orders, one for couriers to claim and run deliveries — sharing a common Dart package (`lohay_core`) for platform infrastructure (API client, JWT, auth, geo, theme, settings). PostGIS powers geo search, MeiliSearch handles full-text, a wallet + ledger system with Stripe Connect runs payments with splits, S3 stores media. AI endpoints are pluggable via an OpenAI-compatible `ModelEndpoint` registry with role-based assignment (chat / embeddings / search intent), so the team can swap providers without redeploys. Multi-tenant theming lets each store keep its own brand without forking the codebase.
Tech stack
TL;DR
- One backend, three surfaces — buyer web, seller/inventory Flutter app, courier/delivery Flutter app on a single Django API.
- Geo search at the core — PostGIS + MeiliSearch, served from the same endpoints to web and mobile.
- Multi-tenant without forks — each store’s brand, catalog, and backoffice from one codebase.
- AI without lock-in — pluggable provider layer lets the team swap models without redeploys.
- Production-grade from day one — payments + splits, audit log, feature flags, OpenAPI, sanitization. No “we’ll add it later”.
The challenge
LoHay’s product vision was ambitious: a marketplace where buyers open the app, see nearby independent stores on a map, and chat with an AI assistant to find what they need. Each store needs its own brand, its own catalog, and its own backoffice.
That kind of product normally takes a large team and a long runway. LoHay had neither. The constraints were:
| Constraint | Why it mattered |
|---|---|
| Three surfaces, one codebase | Buyer web + seller/inventory Flutter app + courier/delivery Flutter app, sharing one backend and one domain model. |
| Geo search everywhere | PostGIS-style queries on every map view, on web and mobile. |
| Payment splits | Each order splits the payout between seller and courier — Stripe had to handle this from day one. |
| AI without vendor lock-in | Discovery had to work out of the box, but swap models without a rewrite. |
| Multi-tenant theming | Each store with its own palette, logo, copy — no code duplication. |
| Production foundations on day one | Payments, audit, flags, OpenAPI, sanitization. Not bolted on later. |
Every week of delay was runway lost. There was no room for a fragile MVP that had to be rewritten once traction appeared.
What we built
| Capability | How |
|---|---|
| One backend, three surfaces | Django 6 + DRF. Buyer web on Next.js 15 + TanStack Query. Seller/inventory + courier/delivery Flutter apps on the same API. Platform infrastructure shared in lohay_core. |
| Geo-aware discovery | PostGIS (via django.contrib.gis) for spatial queries; MeiliSearch for full-text. Same endpoints serve web map and mobile. |
| Pluggable AI endpoints | ai_providers.ModelEndpoint registry with OpenAI-compatible HTTP API. Roles (chat / embeddings / search intent) point at endpoints. Add or swap providers without code changes. |
| Multi-tenant theming | theming/middleware.py + palette.py (allow-list + regex guard so palette values cannot escape CSS context). New tenant = content operation, not a deploy. |
| Wallet + ledger | Per-store wallet with hold/release and margin economics. Stripe Connect as the payout gateway; the split rules live in our ledger, not in Stripe. |
| Production-grade from day one | See the table below. |
Production foundations
| Concern | What we shipped |
|---|---|
| Auth | Google OAuth + JWT, full audit trail (django-auditlog). |
| Payments | Stripe webhooks, idempotent handlers, S3 media storage. |
| Feature flags | django-flags for dark launches and gradual rollouts. |
| API docs | drf-spectacular generates OpenAPI from the DRF views, always in sync. |
| Security | NH3 sanitizes any user-generated HTML on the way in. |
| i18n | ES/EN first-class across web and mobile. |
Results
The platform is in production with three live surfaces backed by one API: the buyer-facing web app, the seller inventory app, and the courier delivery app. The team can:
- Onboard a new store by creating a tenant record — no deploy required.
- Swap or A/B test AI providers without code changes downstream.
- Roll out new features behind flags and measure impact before broad exposure.
- Add a new surface (admin web, ops CLI, partner portal) without re-implementing domain logic.
Quantitative results are placeholders pending client confirmation — real numbers (time-to-launch, stores onboarded, transactions processed, conversion rate) will be wired in once the client signs off.
What’s next
Roadmap is open: B2B accounts, marketplace analytics, expansion to additional geographies. The architecture supports each without rewrites.
Building a multi-tenant product with a tight deadline and a small senior team? Book an intro call. We’ll tell you honestly whether we can help.
Have a similar project?
We help startups build the right thing, the right way. Let's talk about yours.