Mobile

In-app brand campaign for Jägermeister

A bespoke in-app sponsorship campaign for Jägermeister, embedded inside the ChargedUp order-at-table flow. Custom branding, drink-bundle offers and venue activation tracking — all without forking the codebase.

JägermeisterEngineering LeadOct 2020 – Feb 2021
20+
Campaign venues
+30%
Activation rate uplift
0
Codebase forks

Problem

Jägermeister wanted to run a hospitality activation through ChargedUp's order-at-table app: their branding on the splash and menu, a curated drinks bundle promoted on launch, and analytics back to their marketing team.

The risk for engineering was obvious: bespoke campaigns are how SaaS codebases die. Either we forked the app (and inherited two long-term maintenance trees), or we hard-coded a one-off branding mode (and made it harder to do the next sponsor).

Neither was acceptable. We needed a campaign engine, not a campaign.

Process

I designed the work around three principles:

  1. Campaigns as data, not code. A new Campaign entity in DynamoDB holds theme tokens, copy, hero artwork URLs, drink-bundle SKUs and a venue allow-list. The React Native app loads the active campaign at boot from /v/<venue-id>.
  2. One feature-flag toggle per venue. A campaign goes live by flipping a single flag. Off by default, no risk of leaking branding into non-participating venues.
  3. Built-in analytics from day zero. Every interaction inside the campaign overlay (impression, view bundle, add to basket, complete order) emits an event with campaign_id, dropped into the existing analytics pipeline so the Jägermeister team got dashboards on day one.

I worked directly with Jägermeister's marketing leads on creative review — vetting the Lottie splash animation, the colour palette, and the way bundle offers expressed themselves on a phone screen. Two iterations of design review, then in-venue testing across five sites before full rollout.

Outcome

Jägermeister activated in 20+ venues across the campaign window. Order activation rate (scan → completed order) lifted by ~30% versus the non-campaign baseline at participating venues. Critically, zero engineering work was needed to onboard the next sponsor — the next campaign reused the same engine with a different Campaign row.

For engineersTechnical Deep Dive
Expand

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; }; copy: { splashHeadline: string; bundleHeadline: string; ctaLabel: string; }; bundle: { sku: string; items: Array<{ menuItemId: string; modifiers?: string[] }>; priceOverride?: number; }; };

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 entire app, exposing a single hook:

const { campaign, theme, bundle } = useCampaign();

Every UI component branched on campaign?.id — when null, the app rendered the default ChargedUp experience.

Why feature-flagged, not config-flagged

We considered storing the active-campaign ID directly on the venue document. Feature flags won because:

  • Instant rollback. Killing a campaign mid-night was a flag toggle, not a deploy.
  • Gradual rollout. We turned campaigns on across 5 → 20 venues over three days, watching analytics for issues before each tier.
  • A/B compatibility. We could send a fraction of customers in a participating venue to the non-campaign experience to measure lift.

Analytics pipeline

Existing infra: app emits events to API Gateway → Kinesis Firehose → S3 (Parquet) → Athena. We added two derived tables: campaign_impression and campaign_conversion, joined on (session_id, campaign_id). The Jägermeister team got a Quicksight dashboard reading those tables — no engineering involvement past wire-up.

Trade-offs

  • Themes as data, not stylesheets. We accepted constrained theming (colour tokens + a hero image, not arbitrary CSS) to keep the design system intact. The team disliked it briefly; the next two sponsor campaigns shipped in days because of it.
  • Bundle pricing. We stored an optional override on the campaign rather than splicing into the menu schema. Cleaner, but it meant the bar-side print receipt needed a small change to render the bundle SKU.