Two shops selling the same thing were incomparable until the catalog was shared
A directory where every local business keeps its own inventory and reviews, and a shared catalog suggests products as they're added. A search for one item finds it in four shops instead of finding four spellings of it.
Who, what, and how long
- Industry
- E-commerce & Retail
- Duration
- 12 weeks
- Cooperation model
- Fixed price
Client name withheld under NDA. Engagement details are shown to the extent our agreement permits.
The hard problem
- 01
The first version let every shop type its own product names. Within a month the same item existed as six different strings, search returned one shop instead of six, and the directory's only real promise (telling you who nearby has the thing) didn't work.
Free-text entry across many sellers isn't a data-quality problem that cleanup can solve, because there's nothing to reconcile against. Six spellings of one item are six items until somebody decides otherwise, and nobody in a town directory has the standing to decide. A search returned the one shop that had spelled it the searcher's way, which is worse than returning nothing.
A shared catalog behind free-text entry. As an owner types, the platform suggests an existing catalog entry; picking one links the shop's stock to a product every other shop can also link to. Owners still set their own price and quantity, but the identity of the product is shared.
How the pieces fit
- 01
Put a suggestion step in front of free-text product entry instead of policing names afterward
Typing a product name suggests matching catalog entries before free text is accepted, so the correction happens at entry, where it can still work.
- 02
Kept price and stock per business while sharing product identity across them
The split was tested with shop owners before it was built, because sharing product identity is the part a business is most likely to object to.
- 03
Allowed a genuinely new product to be added, so the suggester never becomes a wall
The suggester proposes and never blocks, and a genuinely new product enters the shared catalog and becomes a suggestion for the next shop.
- 04
Scoped reviews and banners to the business, since those are the shop's own voice
Scoping reviews to the business kept one shop's bad week from attaching itself to a product every other shop also sells.
- 05
Made following a shop the lightweight action, ahead of any account-heavy feature
Following a shop is the lightest engagement action and shipped before anything account-heavy, because it's the one people actually take.
The system we were asked to build
A single directory lists the stock of independent shops across a town in one place. We built the public directory and search, the per-business dashboard where owners manage inventory, banners and reviews, and the shared catalog that keeps their listings comparable.
A town directory listing the stock of independent shops in one place, whose first version had let every owner type their own product names. Within a month the same item existed under six strings and the directory's only real promise (telling you who nearby has the thing) had stopped working. The engagement was scoped around fixing the catalog, with the search on top of it to follow.
Full-Stack Engineering
Suggest before free text
Adding a product starts with matching an existing catalog entry, which is why the same item doesn't exist under six names.
Typing a product name suggests matching catalog entries before it accepts free text, so the ordinary path links a shop's stock to a product that already exists. That's what stops one item existing under six spellings across six shops. The correction has to happen at entry, because no amount of later deduplication recovers a catalog that was never shared.
- Suggestion offered before free text is accepted
- Linking is the default path, not a cleanup task
- One item can't accumulate six spellings
Burke's Outdoor Supply types "voltmere aa" and the catalogue offers its matches first. Picking the 8-pack links the shop's own price and quantity to the entry five other shops already link, and adding a new product stays one row below.
Hadley Hardware's dashboard: what was looked at this week, what has run down to nothing, and its own price for each product set against the nearest shop linking the same catalog entry.
Shared identity, local price
Product identity is shared across shops; price and stock stay with the business that set them.
Product identity is shared and commercial detail isn't: every shop linking to the same catalog entry sets its own price, its own quantity and its own availability. The split is the whole design. Shared identity makes search work across the town, and local pricing keeps each business in control of the only thing it actually needs to control.
- Identity shared; price, stock and availability stay local
- Search works across shops because identity is shared
- Businesses keep control of what actually matters to them
New products still allowed
The suggester proposes, it doesn't block: a genuinely new item can always be added.
The suggester proposes and never blocks. A genuinely new product can always be added, because a matcher that refuses unfamiliar items would push owners straight back to whatever they were using before. New entries enter the shared catalog and become suggestions for the next shop, which is how the catalog keeps growing after the initial seed.
- The matcher proposes; it never refuses an entry
- New products enter the shared catalog immediately
- The catalog grows from real entries, well past its seed
Asking for a genuinely new entry: six near-matches are put in front of the owner with a link-instead option on each, and none of them stands in the way of adding the lithium 8-pack to the catalog.
Search resolves to products
Misspelled as "voltmer aa" on a phone, the query still resolves to the catalogue product, and all six shops that stock it are listed nearest first with price and stock.
A query lands on a catalog product and returns every shop that stocks it, not one shop that spelled it your way.
A search resolves to a catalog product and returns every shop stocking it, with price and distance, however the searcher happened to spell it. That's the behavior the shared catalog was built to make possible, and it's the difference between a directory and something a resident actually uses twice.
- Queries resolve to a product, then list every shop stocking it
- Price and distance shown per shop
- Spelling no longer decides which shop is found
Reviews stay with the shop
Reviews and banners are the business's own voice and are scoped to it, never to the shared catalog.
Reviews, banners and shop copy are scoped to the business and never to the shared catalog entry, because a shop's reputation is its own and a product's identity is everyone's. Mixing the two would let one shop's bad week attach itself to a product every other shop also sells.
- Reviews and banners scoped to the business, not the product
- A shop's reputation can't attach to a shared catalog entry
- Each business keeps its own voice on its own page
The reply queue, where a two-star review that mentions the battery pack stays attached to Hadley Hardware's page, not to the shared catalog entry the other five shops link.
Phase by phase
Phase 1: The Directory
Businesses And Their Pages
Built business profiles and category browsing: the base the whole product sits on, and the surface a shop is judged by before anyone looks at stock.
- Business Profiles
- Categories
- Public Pages
Phase 2: The Shared Catalogue
Suggest Before Free Text
Built catalog suggestion into the add-product flow. An owner types, sees matching entries, and picks one, linking their stock to a shared product identity instead of a new string.
- Catalogue Model
- Suggestion Endpoint
- Add-Product Flow
Phase 3: Inventory
Price And Stock Stay Local
Built the per-business inventory: the same catalog product carries a different price and quantity in each shop, which is exactly the comparison the directory exists to show.
- Per-Business Inventory
- Pricing
- Stock Levels
Phase 4: Search
One Query, Every Shop
Built search across catalog products instead of shop-entered strings, so a query resolves to a product and the result is the list of shops that have it.
- Product Search
- Shop Results
- Availability Display
Phase 5: Merchant Tools
Banners, Reviews, Followers
Built the dashboard surfaces a shop owner actually returns for: promotional banners, replying to reviews, and seeing who follows them.
- Banners
- Review Replies
- Followers
A shop's own page: its banner, its stock and prices, its opening hours, its reviews and a Follow button. The catalog supplies only the identity of each product it sells.
What it carries now
Shared
Product identity
Per shop
Price and stock
Catalogue, not strings
Search targets
None
Blocked new products
Product identity, price and stock, and search targets are all descriptions of the model, not measurements. Blocked new products is none: the matcher can't refuse an entry, which is the property that keeps the catalog growing past whatever it was seeded with.
Client name withheld under NDA. Figures are approximate, drawn from the engagement’s own reporting.
What the architecture settled
Free-text product entry across many sellers destroys search within weeks. The fix belongs at entry, not in cleanup.
Cleanup can't recover a catalog that was never shared: six spellings are six products, and by the time anyone notices there's no authority to merge them.
Share product identity and keep price local. That split is what makes a directory comparable and still fair to each shop.
Sharing identity and keeping price local is the split that makes the whole thing work: comparison across the town, control within each shop.
A suggester that blocks becomes a support queue. It has to propose and then get out of the way.
A matcher that refuses unfamiliar items becomes a support queue and, worse, sends owners back to whatever they were using before it existed.
The lightest engagement action, following a shop, earns more return visits than any account feature built before it.
Following outperformed every account feature considered ahead of it, because it asks for one tap and no commitment and still produces a reason to return.
How the work was run
We ran the suggestion flow past eight shop owners before building it, and the design changed because of it: the suggester proposes and never blocks. Owners had been burned by a system that refused entries it didn't recognize, and wouldn't have adopted another one.
Twelve weeks, fixed, with the suggestion step built and tested with real shop owners before the search work, because search is downstream of the catalog in every sense. The rule that the suggester proposes and never blocks came from that testing: an owner refused an unfamiliar item would go back to whatever they had been using before.
One item, searched two ways
Six spellings made one item look like several. One catalog entry finds all six shops.
Pick a query and run it against the strings six shops typed for the same pack of batteries, then against the shared catalog. The third tab adds a product the catalog doesn't have yet. Switch tabs, or use the arrow keys once one is focused.
Each shop's listing was whatever the owner typed. A search could only look for its words inside those strings, so one item under six names came back as a few separate products, or as nothing.
What each shop typed for the same item
- VOLTMERE AA-8Pine Street General Matched
- Voltmere AA Alkaline 8pkHadley Hardware Matched
- voltmer AA 8 packCorner Pantry Market Not found
- AA batteries (8) - VoltmereCedar Bend Pharmacy Matched
- Alkaline double-A 8ctLakeside Pet & Feed Not found
- Batteries, AA, 8 countBurke's Outdoor Supply Not found
What the resident saw
- VOLTMERE AA-8A product of its own · 1 shop · Pine Street General$8.99
- Voltmere AA Alkaline 8pkA product of its own · 1 shop · Hadley Hardware$9.49
- AA batteries (8) - VoltmereA product of its own · 1 shop · Cedar Bend Pharmacy$10.99
3 listings for one item, 3 of 6 shops never shown, and no way to compare the prices.
Why the answer changed: shops no longer name the product. They link their own price and stock to an entry every other shop can link to, so a search has one thing to find.
From a typed product name to every shop that stocks it
The correction happens at entry, where it can still work. Search sits downstream of the catalog, so it only ever has products to find, never strings.
- 01 · TriggerAn owner adds a productEntry starts in the per-business dashboard, the one place a product name is ever typed.
- 02 · SuggestSuggestion endpointMatching catalog entries are offered before free text is accepted, so linking is the default path, never a cleanup task.
- 03 · EngineShared catalogue modelOne identity per product. The matcher proposes and never refuses: a genuinely new product enters the catalog at once and becomes the next shop's suggestion.
- 04 · StatePer-business inventory in PostgreSQLPrice, quantity and availability are stored against the business, as are reviews and banners. None of them is written to the catalog entry.
- 05 · DeliveryProduct search on Next.jsA query resolves to a catalog product, then returns every shop stocking it with price and distance.
What sharing a catalog could break
Catalogue integrity & shop isolation
Duplicates are stopped at entry
Typing a product name offers matching catalog entries before free text is accepted, and a request for a new entry is shown its near-matches first. Linking is the ordinary path, so one item doesn't collect six spellings.
Nothing one shop sets reaches another
Only product identity is shared. Price, quantity, availability, reviews, banners and shop copy are scoped to the business, so one shop's bad week can't attach itself to a product every other shop also sells.
No owner is refused a listing
The suggester proposes and never blocks. A genuinely new product can always be added; it enters the shared catalog immediately and becomes a suggestion for the next shop.
Listing many sellers who each name the same products their own way? 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.














