A rule-driven marketing engine that only ever sends the right offer

A retail business had years of customer history in its point-of-sale system and no dependable way to use it. We designed an engine that finds the right customers for each offer, emails them properly and keeps social content flowing, with the owner releasing every send.

Locality
East Midlands, UK
Disciplines
Marketing and engagement, Product engineering, Systems integration
Stack
  • Django
  • PostgreSQL
  • Redis + RQ
  • Microsoft Graph
  • Next.js
  • Model Context Protocol

At a glance

  • 19

    tested audience rules

  • 10

    campaign recipes

  • 167,813

    transactions segmented

  • 41

    build steps, each with its own verify check

The challenge

The data was all there, sitting in a third-party salon system: every visit, every package, every lapsed regular. What wasn't there was a safe way to turn it into marketing. Finding a list meant hand-written queries, and social posting had drifted into long gaps with no measurement on one platform at all.

The owner wanted marketing that was deterministic and auditable, never emailed anyone who hadn't consented, and never went out without a person pressing the button.

Architecture

Marketing engine architectureArchitecture diagram with 9 components. Point-of-sale system to PostgreSQL replica. PostgreSQL replica to Audience rules. Audience rules to Offer evaluator. Offer evaluator to Campaign runner. Owner release to Campaign runner (go live). Campaign runner to Microsoft Graph. Microsoft Graph to Customers. Customers to Events + suppression (signed links). Events + suppression to PostgreSQL replica (suppression).go livesigned linkssuppressionPoint-of-sale systemRead-only sourcePostgreSQL replicaSynced every 5 minutesAudience rules19 tested queriesOffer evaluatorOne offer per customerCampaign runnerA/B, caps, test modeOwner releaseEvery send approvedMicrosoft GraphOne-click unsubscribeCustomersEvents + suppressionClicks, bounces, opt-outs

Swipe to explore the diagram

Audiences come only from reviewed, tested rules. Every send is owner-released, and every unsubscribe, bounce or complaint feeds straight back into suppression.

What we built

Audiences and offers

  • A fixed catalogue of 19 parameterised audience rules (recency, lapsed regulars, lost balances, membership eligibility and suppression among them), each a tested query with fixture customers
  • Four saving formulas and 10 campaign recipes, from a welcome to win-backs and membership invites
  • A daily evaluator that gives each customer at most one live offer, choosing the biggest saving to them rather than the best margin to us
  • Single-use offer codes and checkout links signed per customer, verified before any payment

Sending properly

  • A bespoke email design per campaign that still reads with remote images switched off
  • Deterministic A/B splits, frequency caps, staggered sending and prices recomputed at send time
  • A test mode that rewrites every recipient to the owner, and a live switch that fails closed
  • One-click unsubscribe with signed, replay-safe tokens, plus a mailbox reader that suppresses hard bounces at once and soft bounces after three in 30 days

Social and content

  • A post ledger with one approval queue, scheduled publishers per platform and insights ingested back for results
  • Still-image templates, a media catalogue on network storage and programmatic video with voiceover
  • A facts library with content rules, so nothing goes out that contradicts the business

Working with AI

  • An MCP interface so an engineer's AI session can load recipes and upload finished media, without customer data ever leaving the system
  • No tool can publish, send or activate anything. The interface prepares, the owner releases

Engineering decisions

A fixed vocabulary, not free-form queries

Every audience is built from reviewed, tested rules, so a new campaign can only combine parts that have already been proven. There's no way to write a rogue query from the campaign screen.

Rulings made from data

Two sources disagreed on lost balances, with only about half of 729 expired packages matching. We counted both, adopted a hybrid rule and sized every campaign against a copy of production before building anything.

Consent is an invariant

A suppressed or non-consenting customer is never assigned an emailed offer. It's enforced in the evaluator and tested, rather than left to a filter at send time.

Deterministic core

There's no language model inside the application. Drafting happens in an isolated container with no credentials and a restricted tool list, and nothing it produces goes anywhere without approval.

Outcome

Each campaign was sized against real history before a line of the runner was written: a welcome of around 13 customers a month, a win-back audience of 573 and around 400 lost regulars. The plan carries 41 build steps, each with invariants and a verify check.