Rob Scott

Case study

Building the one feature instead of buying the platform

Oct 2026Build vs buy7 min

A retailer was about to buy an enterprise platform to show targeted offers. I built that one feature instead. It was live in four weeks, has shown over 765,000 offers since (production database, Oct 2026), and they own all of it.

Client
Online retailer, WooCommerce
Role
Architecture and build, at Comet CX
Timeline
Four weeks to build, a little more than a year in production
Stack
Next.js on Vercel, Redis, Google Tag Manager, Google Analytics

A retail client was close to signing with an enterprise personalisation platform. The contract was annual, the quote was large, and most of what it covered was never going to be switched on. What they did need was complex: rules for which products and categories each offer should include or exclude, and those rules would have been extremely difficult to achieve in the personalisation platform.

Before they signed, I sat down with them and worked out what they actually needed: to show a specific offer to specific shoppers, on specific products, at the right moment, and to let the marketing team control it without a developer.

That's good retail thinking. A sitewide sale hands margin to people who were going to buy anyway, and it trains everyone to wait for the next one. An offer shown to someone on their third visit to the same ring is spent where it's most likely to change a decision. To do that you need precise targeting and a way to change your mind quickly. You don't need a platform.

I told them that for a third of the price of the annual personalisation licence, we could build it and they would own all of it: the code, the data and the roadmap.

Two weeks of prototype, two weeks to production

On day one we scaffolded the admin UI together in a collaborative AI workshop, to get the initial features right. I also set up the database and sign-in, and wrote two documents: a solution architecture and an implementation plan.

The first two weeks went on the core loop: creating and editing campaigns, a dashboard listing them, and the first version of the storefront script. By the end of week two the whole path worked. A shopper opened a product page, the script called the API, and the API decided whether to show an offer.

The second two weeks were spent iterating, improving and making the software easier for the marketing team to use. Marketing got a live preview that shows a banner exactly as shoppers will see it, and I added a kill switch. Then came the work of fitting the real storefront: badges on listing pages, visitor-history rules, brand targeting, skipping out-of-stock products, campaign priority and impression counts. The first offers were being shown to customers by the end of week four.

So half the build was the engine, and the other half was everything that lets a marketing team run it without calling a developer.

How it fits together

There are four parts: a script on the storefront, a small service that decides who sees which offer, an admin console for marketing, and a single Redis database.

Google Analytics existing reports Storefront WooCommerce theme snippet.js visitor cookie reads existing tags banner + OFFER badges Marketing team signs in to admin Next.js on Vercel Public endpoints serves the script product page check listing page check store domains only input validation eligibility rules Admin console sign-in required campaign wizard campaign list Redis persisted to disk campaigns rules, codes visit counts per product page views event log track · badges banner or 204 snippet.js campaign view event builds campaigns reads campaigns updates counters saves campaigns
The whole system. One Next.js app serves both the storefront's offer checks and the marketing team's admin console. Redis is the only database. The highlight marks the one thing added to the storefront.

One script tag

The storefront needs one line added:

<script src="https://<deployment>/api/snippet.js" async></script>

Everything else happens inside that script. It sets a first-party cookie holding a random ID that lasts a year, which is all the identity it needs. There's no third-party tracking and no login.

The store already ran Google Tag Manager, so SKU, brand and category were already on every page. The script reads them from the tag manager data and only falls back to reading the page itself if they're missing. The storefront team had nothing new to build.

On a product page, the script asks the API whether to show a banner. On a listing page, it sends every product in one request and adds an OFFER badge to the ones that qualify. It also catches tiles that load later, after filtering or infinite scroll.

BEFORE AFTER · ONE SCRIPT TAG AURELIA $1,249 Size Add to cart Description Details Delivery AURELIA $1,249 Size Add to cart 15% off this ring with code GLOW15 ← injected Description Details Delivery
Product page, before and after. When a campaign matches, the script inserts the banner above the product tabs. The theme itself is unchanged. Wireframe with example data.
Rings 124 products FILTER BRAND OFFER AURELIA $1,249 MERIDIAN $890 LUNE $2,100 OFFER AURELIA $640 MERIDIAN $1,480 OFFER AURELIA $960 OUT OF STOCK AURELIA $3,200 LUNE $720
Listing page. One request checks every SKU on the page. Matching tiles get an OFFER badge, and out-of-stock tiles are skipped even when their brand is targeted. Wireframe with example data.

Each time a banner is shown, the script sends a campaign-view event to Google Analytics, so marketing sees results in the reports they already use and has no separate dashboard to check. If the API errors or returns nothing, the shopper just sees the normal page.

The script is served from the app, not pasted into the theme, so I can change it without touching the store. The kill switch is one environment variable. Flip it and the script comes back empty, which turns every campaign off at once.

The decision

Every product view runs one check:

  1. Load the active campaigns and drop any outside their dates.
  2. Read how many times this visitor has viewed this product, but only if a live campaign uses that number.
  3. Apply each campaign's rules: included or excluded SKUs, brands, a minimum number of views ("show the offer on the third visit"), and new or returning visitor.
  4. If more than one campaign qualifies, pick the one with the highest priority, so the result is always predictable.
  5. Record the view and the impression in one round trip to the database.

Personalisation platforms sell "a returning visitor sees an offer on the product they keep coming back to" as a feature. Here it's about 50 lines of TypeScript that anyone on the team can read and test.

The admin

Marketing builds campaigns in a five-step wizard: basics, products, display settings, rules, then preview and publish.

StoreKit Dashboard Campaigns Profile STAGING New campaign 4 5 Basics Products Display Settings Rules & Triggers Preview & Publish Minimum visit count 1 2 3 4 5 3 views Show the offer from the visitor's 3rd view of a product. Customer type All customers New customers only Returning customers only Back Next
The rules step. The visitor-history targeting that platforms charge for comes down to two form controls. The badge in the top bar shows which environment you're editing. Wireframe with example data.
StoreKit Dashboard Campaigns Profile STAGING New campaign 5 Basics Products Display Settings Rules & Triggers Preview & Publish Summary Name Halo returning visitors Dates 1 Aug – 31 Aug Priority High (1) Products Brand: Aurelia Code GLOW15 Rules 3+ views · returning Storefront preview AURELIA $1,249 15% off this ring with code GLOW15 Back Save as draft Publish
Preview and publish. The preview uses the same styling as the live banner, so what marketing approves is what shoppers see. Campaigns can be saved as drafts. Wireframe with example data.
StoreKit Dashboard Campaigns Profile PROD Campaigns New campaign NAME STATUS PERIOD PRIORITY IMPRESSIONS Halo returning visitors Active 1 Aug – 31 Aug High (1) 12,480 Meridian brand week Active 4 Aug – 10 Aug Medium (2) 3,906 Lune third-view nudge Paused 15 Jul – 15 Aug Low (3) 1,152 Spring bands Draft 1 Sep – Medium (2) 0
Campaign list. Several campaigns can run at once, and when more than one matches a product the highest priority wins. Wireframe with example data.

Development, staging and production are separate environments, and the admin shows a badge for each so nobody edits the live store thinking it's staging.

One database

Campaigns, visit counts and a page-view log all live in Redis. There's no warehouse and no second database to keep in step, which keeps each decision fast and the running cost low. Fifteen months in, it holds view counts for about 3.4 million visitor and product pairs. The page-view log is there if the client ever wants deeper reporting, but day-to-day use doesn't need it.

Access

Only the marketing team can sign in to the admin. The storefront endpoints only answer the client's own domains, and the service rejects malformed input before it reaches the rules.

After launch

It has been in production for a little more than a year. In that time it has run 25 campaigns and shown a targeted offer more than 765,000 times (production Redis, queried Oct 2026). That figure counts banners on product pages only. Badges on listing pages aren't counted, so the real number of offers seen is higher.

Figures from read-only queries against the production Redis database, October 2026.

What the client ended up with

The code is theirs, and the data stays in their own database and their own Google Analytics, not a vendor's. It runs on standard tools with usage-based pricing and nothing that locks them in. They aren't paying for a visual editor, an A/B testing suite or a recommendation engine they'd never open. And when they want something changed, they decide when it happens.

If you're weighing this up

Buying a platform makes sense when you need most of what it does. When you need one or two features, check how much of the platform those features really depend on. Here it came down to a script tag, a rules function, a form and a key-value store.

Building isn't free, and this does need a small amount of maintenance. But the end result is software that the client owns, doesn't pay a monthly licence for, and can shape however they want from here.

Wireframes are simplified drawings of the real screens. Brands, products, prices and figures in them are examples, not client data.

All writing