User Manual

Marketing Engine — User Manual

Campaigns, QR codes, redemptions — synced to the POS.

The Marketing Engine is the campaign-management and QR-redirect service that sits in front of your online ordering app. Operators use it to build campaigns (Valpak inserts, table tents, receipt coupons, Yelp ads, app-launch promos, etc.), generate the QR codes that print on those materials, watch scans roll in live, and push the offer definitions to the POS so that every redemption is correctly attributed and discounted.

It runs as a standalone web app on port 3100 and exposes two surfaces: a public redirector at /c/:slug (the URL that QR codes encode) and an authenticated admin panel at /marketing. Campaigns created here are synced every 60 seconds to the merchant POS so the customer-facing PWA and the in-store terminals see the same offers.


1. Signing in

Open https://<your-host>/marketing/login. Enter the email and password you were issued by your administrator and click Sign in.

Passwords are stored with Argon2id, and the login form is rate-limited per IP — after five bad attempts in a row you will be locked out for fifteen minutes. If that happens, wait it out rather than retrying; the lockout window does not reset on success.

Once you are signed in, every page shows the top navigation bar:

Sign out when you are finished. Sessions persist via an HTTP-only cookie until they expire or you explicitly log out.


2. The campaign list

The home page of the admin panel is the campaign list. Each row corresponds to one campaign and shows:

To create a new campaign, click + New Campaign in the top-right of the page. To inspect or edit an existing one, click its name.

Campaign list


3. Creating or editing a campaign

The New Campaign and Edit forms are identical except that the slug becomes read-only once a campaign has been saved (changing a slug after the QR is printed would invalidate every printed code). The form is laid out in three sections.

3.1 Identity and offer

3.2 Campaign type and offer scope

Two campaign types are supported:

The two scopes are mutually exclusive: a Coupon campaign has a target, a BOGO campaign has a trigger plus reward, and the form hides whichever section does not apply.

3.3 Schedule restriction

Schedule restriction is optional. Leave everything blank and the offer is valid every day, all day, throughout the validity window. Otherwise:

Combine them to express things like "weekdays 3pm – 6pm only" for a happy-hour campaign.

3.4 Saving

Click Save campaign to persist your changes. The form will redirect back to the campaign list on success and pop up an alert if validation fails (the slug must be unique, the end date must be after the start date, the discount type must be percent or fixed_cents, and so on).

Campaign edit form


4. The campaign detail page

Click any campaign in the list to open its detail page. It is divided into a header, a left-hand information panel, and a right-hand QR-code panel.

Campaign detail page

4.1 Header buttons

The buttons available depend on the current status:

4.2 Information panel (left)

Shows the slug, channel, status, validity window in Pacific Time, and a one-line summary of the offer (discount, fulfillment restriction, minimum order, and per-customer cap). Underneath is a 7-day summary of total scans and the number that were redirected.

Below that is the recent-scans table — the twenty most recent scans, newest first. For each scan you see:

The recent-scans table is capped at twenty rows for readability. For the full log, use Export CSV.

4.3 QR code panel (right)

Shows a 200 × 200 preview of the campaign's QR code, the underlying URL (https://<host>/c/<slug>), and two download buttons:


5. How a scan works (and what the customer sees)

The QR codes you print encode https://<your-marketing-host>/c/<slug>. When a customer scans:

  1. The redirector normalizes the slug (uppercase, alphanumeric only) and looks it up.
  2. It rate-limits the request by hashed IP (with a daily-rotated salt) to prevent enumeration and abuse.
  3. If the campaign exists, is active, and the current time is inside the validity window, it logs a scan row with outcome redirected and 302-redirects to your redirect_target with three query parameters appended: c=<slug>, src=<source_label>, and t=<scan_id> (the ULID, used to correlate the scan with the eventual order in the POS).
  4. If the campaign is draft, paused, ended, or out-of-window, the outcome is fallback and the customer is sent to the campaign's fallback_url (or the default landing page if none is set).
  5. If the slug is unknown, the outcome is invalid_slug and the customer is sent to the default landing page with ?src=unknown_campaign.

Every scan is recorded — even invalid ones — so you can investigate misprints or scraping attempts in the CSV export.

For campaigns with coupon_code_required = 1, the URL is /c/<slug>/<code> and the redirector additionally validates the per-coupon code, rejects duplicates, and stamps the code as scanned. The merchant POS later flips it to redeemed once the order is paid (via the /internal/coupon/redeem endpoint).


6. Pause vs. End — which to use?

In both states the campaign is excluded from the public GET /api/campaigns feed that the customer-facing PWA polls, so even ambient (auto-apply) offers will disappear from the checkout screen.


7. Deploy

The Deploy button in the top nav runs git pull origin main followed by sudo systemctl restart marketing-engine on the host. It is intended for the operator pushing a hotfix from the source repository — not for everyday use. After clicking, the button shows "Restarting…" and the page automatically reloads after about eight seconds; if the deploy fails, an alert will tell you and the deploy log lives at ~/deploy-marketing.log on the server.


8. Metrics

The Metrics link in the top nav returns a small JSON document with scan totals broken down by outcome across three rolling windows: the last hour, the last 24 hours, and the last 7 days. It is intended for integration with monitoring dashboards rather than as an interactive UI. For per-campaign analytics, open the campaign detail page or download its CSV.


9. Operational notes


10. Quick reference — campaign lifecycle

  1. Draft — create the campaign with status draft while you finalize copy, dates, and the discount.
  2. Active — flip to active when the QR is printed and the offer is live. The PWA and POS will pick the change up within 60 seconds.
  3. Paused — temporarily disable scans (kitchen issue, pricing bug, weather closure). Resume when ready.
  4. Ended — permanent retirement. The campaign stays in the list for historical reporting, but no longer redirects and is no longer synced as live.

That's the whole loop. Build the campaign, print the QR, watch the scans, export the CSV when the drop closes, and end the campaign when you are done.