How we kept three marketplace surfaces in sync with one Django backend
Architecture notes from a 3-sided marketplace: a buyer web app, a seller/inventory Flutter app, and a courier/delivery Flutter app — all on the same Django API.
A recent project asked us to ship a 3-sided marketplace — buyers on the web, store owners managing inventory on a native app, couriers running deliveries on a native app — on a short deadline, with a small senior team. This post walks through the architecture decisions that held up, in case your next project has the same shape.
TL;DR
- One Django backend, one canonical API. No per-surface convenience layers.
- A shared Dart package between the two Flutter apps so platform infrastructure (API client, JWT, auth, geo, theme) cannot diverge.
- Feature-Sliced Architecture on the web client to bound the blast radius of any change.
- Pluggable AI providers so vendor lock-in is a config change, not a rewrite.
- Multi-tenant theming via middleware so onboarding a new tenant is content, not code.
If you take one thing from this post: pick the strictest architecture you can sustain, then enforce it.
The constraint
Three surfaces (buyer web + seller/inventory app + courier/delivery app), one team, one backend, short runway. The temptation in that situation is to split the team per surface and let each one grow its own API client. We chose the opposite: one Django backend, one canonical API, every surface forced to live within that contract.
The five decisions
| # | Decision | Why it works | Trade-off we accepted |
|---|---|---|---|
| 1 | One Django backend, one canonical API | When business logic changes, you change it once. Every surface sees it on the next deploy. No “the inventory app is two weeks behind on the new order status” tax. | Each surface moves at the speed of the slowest integrator. We mitigated it with schema, OpenAPI doc, and a versioning policy. |
| 2 | Shared Dart package between the two Flutter apps (lohay_core) |
Both Flutter apps — seller/inventory and courier/delivery — import platform infrastructure from one place: API client, JWT auth, geo, theme, settings. They cannot ship contradictory versions of how a request is authenticated, retried, or themed. | The package needs curation. We kept it scoped to platform concerns — “what is true of every LoHay app” — and left app-specific concerns (offers for couriers, SKUs for sellers) inside each app. |
| 3 | Feature-Sliced Architecture on the web client | 21 features under src/features/ (auth, products, cart, checkout, orders, wishlist, reviews, search, user, plus geo, navigation, recommendations, store-admin, and others) with unidirectional dependencies. Blast radius of any change is bounded by the graph. |
Learning curve. We kept the rules visible in CLAUDE.md and in code review checklists. |
| 4 | Pluggable AI endpoints (ai_providers.ModelEndpoint) |
AI providers change quality and cost constantly. We wire chat / embeddings / search-intent as roles pointing at any OpenAI-compatible endpoint. Adding or swapping a provider is a config change, not a code change. | We own the abstraction. Provider-specific features that do not fit the OpenAI-compatible interface are accepted as lost. So far, the loss has been small. |
| 5 | Multi-tenant theming via middleware, not forks | theming/middleware.py resolves the tenant per request; web client renders palette at request time. New tenant is a content operation, not a deploy. |
Theming customizations have to fit the template. Bespoke UI per tenant requires a separate project. We have not yet had a tenant ask for that. |
What we’d do the same
- One backend, one canonical API, no per-surface convenience layers.
- A shared package between the two native apps for platform infrastructure.
- Feature-Sliced Architecture on any frontend that will host more than five features.
What we’d change
- Contract testing between the API and the three surfaces, sooner. Drift caught us twice in the first month.
- Visual regression suite on the web client before the third feature shipped, not after the eighth.
TL;DR for your next project
If you are about to start a project with this shape — multiple surfaces on a small backend, a small team, a tight deadline — the boring advice is the right advice: pick the strictest architecture you can sustain, then enforce it.
Want help pressure-testing your architecture for a similar project? Book an intro call.