Building YoPekka: how a Bangladesh marketplace handles multi-vendor logistics
YoPekka is a hybrid marketplace + storefront platform serving Bangladesh. The architecture choices we made — single Laravel monolith, queue workers, per-vendor subdomains — and what we'd change.
YoPekka started with a simple question: Can a marketplace serve Bangladesh-specific commerce — local sellers, bKash/Nagad payments, Pathao/Steadfast couriers, Bengali-first UI — without being a clone of Daraz?
The product is a hybrid: a marketplace surface where buyers shop across vendors, plus per-vendor storefronts on subdomains for sellers who want to build their own brand. Two interfaces, one underlying platform.
Here’s the architecture, the trade-offs, and what we learned shipping it.
Stack at a glance
- Backend: Laravel 12, PHP 8.4, MySQL 8.4, Redis 7
- Frontend: Livewire 4 + Alpine.js + Tailwind CSS 4
- Queue: Redis-backed Laravel Horizon
- Storage: VPS-local for now, S3-compatible (BunnyCDN) on the roadmap
- Search: Meilisearch (ditched Elasticsearch — too heavy for the data volumes)
- Auth: Laravel Sanctum + per-role middleware
Why monolith, not microservices
The temptation in 2026 is to split everything into services from day one. We resisted. A single Laravel codebase with clear module boundaries (cart, catalog, payments, shipping, identity) lets two developers ship features in a week instead of three. We can always split later when one module’s load profile diverges enough to warrant it.
The rule we use: split a service out only when it has different scaling needs, different deploy cadence, or a different team owning it. None of those are true yet for YoPekka. So everything stays in one repo, one deploy artifact, one database.
Per-vendor subdomains
Sellers can opt into a custom storefront at {seller-slug}.yopekka.com. The same Laravel app handles both the marketplace surface and the storefronts — the request middleware detects the subdomain, scopes queries to that vendor, and switches the theme + branding.
The hard part isn’t the routing. The hard part is cache key collisions. A featured-product cache on the marketplace cannot share a key with a featured-product cache on a vendor storefront. We namespace every cache key by request context: cache:marketplace:featured vs. cache:vendor:{id}:featured.
Queue workers for the slow stuff
The pattern we’ve settled into: anything that touches an external API goes through a queue.
- Order placement: synchronous (cart -> charge -> create order)
- Inventory decrement: synchronous (must succeed before order confirms)
- Email confirmations: queued
- bKash callback verification: queued (occasionally bKash takes 4–8 seconds to confirm)
- Pathao courier label generation: queued
- Vendor payout calculations: queued, batched nightly
This split means the checkout page itself responds in 200–400 ms. The slow stuff happens in the background and the user sees their order confirmed instantly.
bKash vs. Nagad behavior
A real-world note for anyone integrating BD payment gateways:
- bKash charges a token-refresh handshake every 50 minutes. Naive implementations expire mid-checkout for one in 100 buyers. We refresh proactively at the 40-minute mark.
- Nagad uses a different signing flow per merchant account and is less tolerant of clock skew between your server and theirs. We sync against
ntp.ubuntu.comevery hour. - Both APIs are stable enough for production, but their sandbox environments are not reliable for end-to-end testing. We test final integration on a tiny live amount (৳1) before the official launch.
What we’d change
Two things, looking back at the first 8 months in production:
-
Schema-level multi-tenancy from day 1. We added
vendor_idcolumns to a dozen tables after launch. Painful but doable. If we were starting over, every domain table would have a tenant scope from the first migration. -
Snapshot pricing on orders, not foreign key. When a vendor changes a product’s price, old orders should reflect the price at time of purchase. We initially did
order_items.product_id -> products.price(a foreign key lookup at read time). When vendors started price-testing, the historical revenue reports became unreliable. We migrated to snapshot pricing on theorder_itemsrow directly.
The Bangladesh-specific UI bets
Three calls we’d make again:
- Bengali as a first-class UI option, not a translation toggle. Bengali text wraps differently, breaks lines differently, has different optical sizing requirements than English. Our Bengali stylesheet has its own font (Noto Sans Bengali Variable) and its own line-height ratios. The difference is invisible to readers but huge to readability.
- WhatsApp routing for support, not a chat widget. Bangladesh buyers will message a WhatsApp number with a screenshot 30x more often than they’ll type into a chat widget. We route to a real number with auto-routing to the right seller.
- COD as a peer of bKash, not an afterthought. 28% of YoPekka orders are COD. We give it equal real-estate at checkout, equal trust signals, equal confirmation flow.
The marketplace is profitable. Vendor signups grow 12-15% month over month. Most importantly, the codebase is still readable — one developer can hold the whole architecture in their head, which is the only stack choice that actually matters at this size.
Got a similar project in mind?
We're delivering 30-40 websites a month across Bangladesh. The story above is one of them — yours could be next month's post. Let's talk.