Problem
Jägermeister wanted a social activation inside ChargedUp's order-at-table app. The idea was a shared moment: a drinker sees a Jägermeister advert in the app, opens it, and is paired with someone at a different venue. Each person redeems one free Jägermeister shot, and the two toast together over the app, a "cheers" across locations. Each user could redeem exactly one free shot.
The trap for engineering was clear: bespoke campaigns are how a product codebase dies. Fork the app and you inherit two maintenance trees; hard-code a one-off activation and the next sponsor gets harder, not easier. Neither would let ChargedUp actually grow a sponsorship business.
Process
The reframe up front was the important one: the ask was for a campaign, but the business needed a campaign engine. If the first sponsor cost weeks of engineering, so would every one after it, and the sponsorship line would never scale. So I scoped for reuse from day one and deliberately dropped bespoke per-sponsor requests that couldn't be expressed as data.
- Campaigns as data, not code. A
Campaignentity in DynamoDB holds theme tokens, copy, hero artwork, the in-app advert, and the redemption rules. The app loads the active campaign at boot. - One free redemption per user, enforced server-side. Each user could claim exactly one shot. Redemption state lived server-side keyed by user, so it held up even if someone reinstalled or tried the offer twice.
- The pairing and "cheers" moment. When a user opened the advert, the backend paired them with another active participant and drove the shared toast, then recorded both redemptions and the completed pairing.
- Usage tracking built in from day zero. Every step (advert impression, tap, redemption, completed cheers) emitted an event with
campaign_id, so the brand had dashboards on day one.
I worked directly with Jägermeister's marketing leads on creative review: the advert, the splash animation, the palette, and how the "cheers" moment felt on a phone.
Outcome
The campaign drove strong engagement: around 7,400 users activated it, roughly 9,200 free shots were redeemed, and about 3,100 paired "cheers" moments were completed across participating venues. It gave Jägermeister exactly the shared, social brand moment they wanted, with the numbers to prove it. The bigger win for ChargedUp: onboarding the next sponsor took a day, not a sprint, because it was a new Campaign row on the same engine. Sponsorship became a repeatable revenue line.
"It gave us a genuinely social moment with drinkers, and the reporting was there from the first day."
Brand Marketing Lead
Architecture

For engineersTechnical Deep DiveExpand
Campaign data model
type Campaign = { id: string; // CAMPAIGN#jager-2020 venueIds: string[]; // allow-list startsAt: string; // ISO endsAt: string; theme: { primaryColor: string; accentColor: string; fontUrl?: string; splashAnimation?: string; // Lottie JSON URL heroImage: string; advertImage: string; // in-app advert that opens the activation }; copy: { splashHeadline: string; ctaLabel: string; }; redemption: { freeItemSku: string; // the Jägermeister shot perUserLimit: number; // 1 requiresPairing: boolean; // the cross-venue "cheers" }; };
Boot-time hydration
The PWA loaded campaign data on first venue scan and cached it in localStorage keyed by (venueId, campaignId). The <CampaignProvider> wrapped the app, exposing a single hook:
const { campaign, theme, redemption } = useCampaign();
Every UI component branched on campaign?.id: when null, the app rendered the default ChargedUp experience.
One redemption per user, enforced server-side
The whole offer hinged on "one free shot each", so the limit could not live on the client. A REDEMPTION#<userId>#<campaignId> record with a conditional write guaranteed a single successful claim per user, even across reinstalls, multiple devices, or a flaky connection retrying the request. The client showed an optimistic "claimed" state; the server was the source of truth.
The pairing "cheers"
When a user opened the advert, a Lambda matched them with another active participant and created a short-lived pairing record. Both clients subscribed to that pairing so the toast animation fired together, then each redemption completed independently. If no partner was available in time, the user could still redeem solo, so nobody was left waiting on an empty match queue.
Analytics pipeline
The app emits events to API Gateway → Kinesis Firehose → S3 (Parquet) → Athena. We added derived tables for campaign_impression, campaign_redemption and campaign_pairing, joined on (user_id, campaign_id). The Jägermeister team got a Quicksight dashboard on those tables with no engineering involvement past wire-up.
Trade-offs
- Themes as data, not stylesheets. We accepted constrained theming (colour tokens plus a hero image, not arbitrary CSS) to keep the design system intact. The team disliked it briefly; the next sponsor campaign shipped in days because of it.
- Solo fallback on pairing. Guaranteeing a partner for every "cheers" would have meant making people wait. We chose a solo-redeem fallback so the free-shot promise always held, accepting that not every redemption became a paired toast.
