A marketplace where the vendors list, the merchandiser arranges, and neither waits for us
A marketplace with vendors, a three-level catalog, campaigns and support tickets, plus a storefront home page the merchandising team rearranges without a deployment.
The shape of the work
- Industry
- Online Marketplaces
- Duration
- 32 weeks
- Cooperation model
- Dedicated team
Client name withheld under NDA. Engagement details are shown to the extent our agreement permits.
The hard problem
A marketplace has three groups who all need to move at their own speed: vendors listing stock, merchandisers arranging the shop, and support answering for both. When any of them needs a developer to do their job, the marketplace grows at the speed of the release train.
Each dependency had a different cause. Vendors couldn't list because the catalog was two levels deep and a new product type needed a migration. Merchandisers couldn't change the home page because it was a template in the codebase. And support couldn't answer a question without opening three systems, because a ticket carried a customer's email and nothing else.
Vendors list against a three-level catalog with typed attributes; merchandisers arrange the home page from the admin; support works a ticket queue tied to orders. None of the three needs a deployment to do their day's work.
The system we were asked to build
The marketplace operator runs a marketplace where third-party vendors sell alongside their own stock. We built the vendor side, the catalog, and the merchandising tools that let the storefront change without an engineer.
A marketplace operator selling their own stock alongside third-party vendors, where three separate groups (vendors, merchandisers and support) all needed a developer to do their day's work. Growth was capped by the release train, whatever the demand. The engagement was thirty-two weeks and aimed at all three dependencies at once.
Platform & Commerce Engineering
How the pieces fit
- 01
Modeled the catalog three levels deep so a vendor lists where buyers actually look
Adding the third level meant re-homing an existing catalog, and the operator used the migration to retire around a fifth of it as genuinely dead.
- 02
Made attributes and their options data, so a new product type doesn't need a migration
Making attributes data was what unblocked the merchandising and vendor work at once, so it went first despite being the least visible change.
- 03
Gave merchandisers a home-page composer in place of a template an engineer edits
Merchandisers compose sliders, campaign strips and rails in the admin against a scheduled window, so Friday's front page can be built on Wednesday.
- 04
Built bulk product import as a typed CSV path for vendors with large catalogs
Bulk import came after the taxonomy, not alongside it, because importing thousands of rows into a model still being argued about is unrecoverable.
- 05
Tied support tickets to orders so an answer has the order in front of it
Tickets open with the order, the shipment status and the vendor already attached, instead of an email address and three systems to check.
A catalogue with real depth
Category, sub-category and child category, so a vendor lists where buyers actually browse.
Category, sub-category and child category are three real levels, with no naming conventions doing the work, because buyers browse at the third level and a vendor listing at the first is invisible. Products attach at the leaf, breadcrumbs and filters derive from the path, and moving a category takes its products with it instead of orphaning them.
- Three genuine catalog levels, not names doing the work
- Products attach at the leaf, where buyers actually browse
- Moving a category carries its products with it
A vendor listing in the seller hub, placed three levels deep at Bakery & Pantry › Bread › Sourdough where products attach, with seven typed attributes stored as records, no migration needed.
A new product type as a form: Sourdough starter with seven typed attributes and nine options, published as rows in the attribute tables with no migration, and one definition driving the listing form, filters and comparison.
Attributes as data
A new product type is a form, not a migration: options and values are records.
Attribute types, options and values are records, so adding a product type with its own fields is a form an operator fills in, with no migration for an engineer to write and deploy. Attributes drive the listing form, the filters and the comparison table from one definition, which is why a new category arrives complete instead of arriving and then needing three follow-up tickets.
- Attributes, options and values stored as records
- A new product type in one form
- One definition drives the listing form, filters and comparison
A composable shop window
Sliders, campaigns and rails are arranged in the admin, so merchandising never queues behind a deployment.
Sliders, campaign strips and product rails are arranged in the admin against a scheduled window, so a merchandiser builds Friday's front page on Wednesday and it publishes itself. Nothing about the shop window queues behind a deployment. That's the difference between a marketplace that responds to a weekend and one that responds to a release calendar.
- Home page composed in the admin, never in code
- Scheduled windows, so a page publishes itself
- Merchandising no longer queues behind a deployment
The home-page composer: five rows live, four scheduled, two kept but switched off, this week's windows with Friday's front page built on Wednesday, and a change log showing none of it needed a deployment.
Bulk listing for big vendors
Bulk import for a vendor arriving with 1,842 products: every column typed and mapped first, a dry run holding 46 rows back with their reasons, and the queued write locked until the file types cleanly.
A typed CSV path for vendors who arrive with thousands of products already in a spreadsheet.
Large vendors arrive with thousands of products already in a spreadsheet, so there's a typed CSV path with per-column validation, an attribute mapping step and a dry run that reports failing rows before anything is written. Imports are chunked through a queue so a ten-thousand-row file doesn't time out, and the vendor watches progress as it happens instead of waiting in silence.
- Typed CSV with per-column validation and attribute mapping
- Dry run reporting failing rows before any write
- Chunked through a queue with visible progress
Support with context
Tickets open with the order, the shipment and the vendor already attached.
A ticket opens with the order, the shipment status and the vendor already attached, because the first four messages of every support conversation used to be the agent assembling that context by hand. Tickets are linked to the order record instead of an email thread, so the history stays with the order and survives whoever happened to answer it.
- Tickets open with order, shipment and vendor attached
- Linked to the order record, not to an email thread
- History survives whichever agent picked it up
Support with the order already attached: the ticket opens with the order lines and totals, the shipment's carrier events and the vendor in front of the agent before they reply.
Phase by phase
Phase 1: The Catalogue
Three Levels, Typed Attributes
Built category, sub-category and child category as real levels, with attributes and their options held as data, so a new product type is a form an operator fills in.
- Three-Level Catalogue
- Attribute System
- Brand Taxonomy
Phase 2: The Vendor Side
Listing Without Us
Built vendor onboarding, listing and order management, plus a typed CSV import for vendors arriving with thousands of products already in a spreadsheet.
- Vendor Onboarding
- Listing Tools
- CSV Import
Phase 3: Merchandising
The Shop Window Is Data
Built the home-page composer so sliders, campaigns and featured rails are arranged in the admin, which is what stops merchandising queueing behind releases.
- Home Composer
- Campaign Scheduling
- Promo Codes
Phase 4: Support
Tickets That Know The Order
Built the ticket queue against orders and tracking, so a support agent opens a conversation with the order, the shipment and the vendor already attached.
- Ticket Queue
- Order Linking
- Tracking Surface
Phase 5: Reach
Currencies, Languages, Feeds
Added currency and language handling and the sitemap and feed surfaces a marketplace needs to be found, without complicating the vendor's listing flow.
- Multi-Currency
- Multi-Language
- Sitemap & Feeds
Merchandising as a board: vendor listings dragged into shelf columns that feed home-page rows, ranked by position as they land, and a change live the moment it's let go.
What it carries now
3 levels
Catalogue depth
0
Merchandising changes needing a deploy
A form
New product type
100%
Vendor self-service listing
Catalog depth is a count of levels. Merchandising changes needing a deploy is zero. A new product type being a form instead of a migration is a capability claim. Vendor self-service listing is the share of listings created by vendors rather than by the operations team, after rollout.
Client name withheld under NDA. Figures are approximate, drawn from the engagement’s own reporting.
What the architecture settled
- 01
A marketplace grows at the speed of whichever group still needs a developer.
Three groups each blocked on a developer means three separate ceilings, and removing two of them still leaves the marketplace growing at the release cadence.
- 02
Attributes as data is the difference between adding a product type and shipping a migration.
Attributes as data is the difference between a category launched this afternoon and one scheduled behind a migration and a deploy.
- 03
A home page an engineer owns is a home page that changes four times a year.
A home page an engineer owns changes about four times a year. That isn't a merchandising cadence; it's a release cadence wearing merchandising's name.
- 04
Support without the order attached is a second investigation for every conversation.
A ticket without the order attached makes every conversation a fresh investigation, and the first four messages are the agent assembling context by hand.
How the work was run
- 01
We built the catalog and the attribute system first, because everything else depended on a product being expressible without a migration. Vendor onboarding followed, and the merchandising composer came last, by which point the trading team had a long list of things they wanted to rearrange.
A dedicated team, with the catalog and attribute model built first because the vendor work and the merchandising work both depend on it. Vendors, merchandisers and support each had someone in the review every two weeks. The three groups had genuinely different definitions of what was blocking them, and building for one at a time would have left the other two on the release train.
Adding a product type
The same new category, once as a migration and once as a form.
A “Sourdough starter” type with seven typed attributes, added first the way the old catalog needed, as columns on the products table, and then the way Canopyard does it, as attribute records. Switch tabs, or use the arrow keys once one is focused.
A product type's fields were columns on the products table. Adding one changed the schema, so it needed a migration, a review and a place on the release train, and the category arrived before anything that used its fields.
The path it took
- Merchandising asks for a “Sourdough starter” type
- An engineer writes a migration: 7 new columns
- Review, then wait for the next release
- Deployed: the category exists, empty
- Listing form, filters, comparison: three follow-up tickets
What changes in the database: the schema
// database/migrations/2026_09_16_add_starter_fields_to_products.php
Schema::table('products', function (Blueprint $t) {
$t->decimal('culture_age_years', 4, 1)->nullable();
$t->unsignedTinyInteger('hydration_pct')->nullable();
$t->string('starter_flour_fed')->nullable();
$t->string('starter_form')->nullable();
$t->json('starter_allergens')->nullable();
$t->boolean('ships_chilled')->nullable();
$t->text('feeding_notes')->nullable();
});
Where “Sourdough starter” shows up
- Vendor listing formFollow-up ticket
- Shopper filtersFollow-up ticket
- Comparison tableFollow-up ticket
A schema change, a deploy and three follow-ups before a vendor could list a starter with its own fields.
Why a type no longer needs a schema change: the products table holds what every product has; what a starter has lives in attribute records keyed to its type. Steps and pacing are illustrative, and no durations are shown.
From a vendor’s spreadsheet to the shop window, with no deploy in the path
The catalog and attribute model went first, because the vendor work and the merchandising work both depend on it. Whatever a vendor, merchandiser or support agent changes lands as records the storefront already knows how to read.
- 01 · SourceSeller hub and adminVendors list their own products, merchandisers compose rows and support opens tickets, each in a form of their own, with no request to a developer.
- 02 · IngestionTyped CSV importPer-column validation, attribute mapping and a dry run that reports failing rows before any write, then chunks through a queue with visible progress.
- 03 · EngineCatalogue and attribute modelProducts attach at the third level, and one attribute definition drives the listing form, the filters and the comparison table.
- 04 · StateRecords in MySQLAttribute types, options and values, home rows with their scheduled windows, and tickets linked to order records are all data, not code.
- 05 · DeliveryStorefront on LaravelThe home page renders the rows inside their window, so a Friday front page built on Wednesday publishes itself with no deployment.
What a bad file or a busy Friday can reach
Catalogue & storefront integrity
An import is checked before it writes
Every column is typed and mapped to attributes first, and a dry run reports failing rows before anything is written. The write then runs in queued chunks, so a large file doesn't time out partway.
Listings cannot be orphaned
Products attach at the leaf of a three-level catalog, and moving a category carries its products with it. Attribute values come from one definition, the same one the filters and comparison read.
Merchandising changes data, not the build
Rows are composed in the admin and publish inside a scheduled window. A row taken down is switched off and kept, and none of it passes through a deployment.
Are your vendors and merchandisers still waiting on a release? Scope your build in 3 minutes.
Scope your buildNearby engagements
Data & AnalyticsA checkout that stopped losing sales to a 6-second load
Profiling found the real bottleneck behind a slow checkout (an N+1 query and an oversized bundle) and tuned both, with before-and-after metrics locked in as a baseline against future regressions.
E-commerce & Retail · 5 weeks
Web PlatformsA reader people finish, and a library that remembers where they stopped
A reading platform for a comics catalog: a browse surface people can actually navigate, a reader that gets out of the way, and a history that puts everyone back on the page they left.
E-commerce & Retail · 18 weeks
Web PlatformsOne number, a whole handset, and a database that keeps up with 125 brands
A metered IMEI lookup service that turns fifteen digits into a device, its specifications and its status. It's sold three ways to three audiences and backed by a device database that maintains itself.
Consumer Electronics · 22 weeks
Let's talk
Running a large platform, shaping a first MVP, or getting a product ready for a funding round? Tell us where you are. We'll shape the process around it, and stay with you after launch.














