A multi-vendor marketplace where several sellers compete on one product card: a storefront, a seller cabinet and an admin panel on a single API, with search, delivery, payments and fiscal receipts wired in. These are the decisions that shaped it.
A product is not an offer
The catalogue product and the seller's offer are separate entities. One card describes the thing; each seller attaches an offer to it with its own price, stock, media and wholesale price tiers.
Everything difficult about a marketplace follows from that split: matching an incoming item to an existing product, merging duplicates that slipped through, and deciding which offer a buyer sees first. Getting it wrong the other way — one product row per seller — produces a catalogue where the same item appears forty times and no filter can fix it.
Search that does not hand the page to one seller
Search runs on OpenSearch, with product attributes indexed as nested documents so filters can be built per category instead of per field, and with the full category path indexed so a filter on a parent category still finds everything underneath.
Results are not a flat relevance list. Offers are bucketed by seller, buckets are ordered by their best offer, and then interleaved — so a marketplace with one large seller and fifty small ones still shows the small ones. A marketplace that lets its biggest seller own every result page stops attracting new sellers.
Sellers upload feeds, not spreadsheets by hand
Catalogue growth comes from an import pipeline: raw items land first, field mappings are saved per seller as presets, media problems are recorded per item instead of failing the batch, and matching proposes the catalogue product each item belongs to.
New brands and categories wait for approval before they reach search facets. Without that gate, one careless feed renames a whole branch of the catalogue.
Eleven states, and who moved them
An order goes from created through paid, processing, awaiting shipment, shipped, delivered and completed, with cancellation, refund and return as first-class states rather than flags.
Every transition records what caused it and who did it: order creation, a payment webhook, a refund job, a Nova Poshta tracking push, an admin, a seller's manager or a scheduler. When a buyer asks why an order says what it says, the answer is in the row, not in the logs.
Money, delivery and the tax office
Payments run through LiqPay, monobank and Hutko behind one provider interface, so adding an acquirer is a new file rather than a new branch through checkout. Cash on delivery stays a payment method like any other.
Delivery covers Nova Poshta, Ukrposhta, Meest and Delivery Auto — branch and locker directories, label printing, and tracking that pushes status back into the order. Fiscal receipts go through Checkbox, Cashalot or Vchasno, because in Ukraine a receipt is not a feature request, it is the law.
Built to be operated
Buyer-to-seller chat runs over websockets with Redis behind it. Notifications, reviews, complaints, audit trails, CMS blocks for the footer and static pages, seller analytics and an advertising module all live as their own modules on the same API.
One hundred and three migrations is the honest measure of a marketplace: the schema keeps moving because the business keeps discovering what it actually sells.
The result
A working multi-vendor marketplace at enez.com.ua — buyer storefront, seller cabinet and admin panel on one API, with faceted search, four carriers, three acquirers and fiscal receipts in production.