PROTOTYPE Clickable mockup · simulates the finished app ◃ Product overview
Petanque Life Operator Console
Production OP

Newsletter & webinars

owned engine — replaces Mailchimp/ConvertKit/Customer.io

Marketing email lives in the platform's own database; Mailjet is only the delivery layer behind the EmailDistributor abstraction (ACS failover, Null in dev/test). Tenant admins get a read-only view in their own admin app.

Lists
ListScopeActivePendingUnsubscribedBounced / complained / suppressed
Product news (platform) platform 18 402 312 1 204 88 / 12 / 41
Competition digest platform 9 811 145 640 35 / 4 / 12
Scoreboard releases platform 2 145 58 90 9 / 1 / 3
Nordvik federation news tenant: Svenska Bouleförbundet 6 320 77 210 18 / 2 / 6
Double opt-in tokens: HMAC-SHA256-signed, purpose-bound (confirm 14 d · prefs 30 d · unsub 90 d). Public endpoints: subscribe (Turnstile + honeypot + 5/IP/h), confirm, preferences, unsubscribe, unsubscribe-all, RFC 8058 one-click, open-pixel and click-redirect tracking.
Subscriber actions: GDPR erase (removes PII, keeps suppression hash), move status pending→active, add to suppression list. CSV import requires an attested consent proof (source, method, timestamp) before any row is accepted.
Subscriber model — growth fields (D-GROWTH-0 Slice 0): role (enum: player · club_official · federation) and country (ISO 3166-1) are first-class subscriber fields, alongside UTM attribution utm_source / utm_medium / utm_campaign. All are captured at signup on the public waitlist/newsletter form and stored on the subscriber — they drive the country-by-country waves and segmentation, and the attribution reporting.
Segment builder — visual AND/OR groups Live recipient count: 3 218
GROUP · ALL of (AND)
language eq "sv" source in [webinar_registration, signup_form] role eq "club_official" country in [SE, FR] engagement_score gte 0.4
OR
GROUP · ANY of (OR) — tenant_role eq "club_admin" · + drag a condition here
Allow-listed fields (incl. role, country, source, language) & ops only (eq, neq, in, nin, gt/gte/lt/lte, escaped regex, exists) — no raw queries.
MJML editor — "August product drop"
<mj-section><mj-text>
  Hi {{first_name}} — here is what shipped in August…
  {% content-block cta variants=[club, federation, player] %}
</mj-text></mj-section>
Rendered HTML is cached per template version. Dynamic content blocks are evaluated per subscriber at send time (language / tenant / merge_data).
Premium gallery — clone to customise
announcementdigestevent_inviteproduct_dropcase_studywin_backre_engagementonboarding_steptournament_recapscoreboard_releasefederation_updateseasonal
Campaigns
CampaignListStatusProgressNotes
August product drop Product news sending 12 480 / 18 402 A/B subject (random-20 %) — B leads, z=2.1
Webinar invite: draw engine Competition digest scheduled send-time optimised per subscriber falls back to 2026-08-07 18:00 when confidence < 0.3
Season wrap-up Product news sent 18 118 delivered open 41 % · click 9.2 %
Win-back June Product news paused 4 021 / 6 300 paused by operator — resume/cancel available
Dispatcher runs every minute; per recipient: MJML render + merge vars + link rewrite + pixel + compliance footer + List-Unsubscribe headers.
Campaign wizard — settings
Drip automations — visual flow builder (trigger / wait / send / branch / exit)
FlowTriggerStatusEnrolment
7-day onboarding trigger: subscription_confirmed active 1 204 enrolled · 62 % complete
30-day re-engagement trigger: user_inactive_days=30 active 388 enrolled — fed by at-risk engagement score
90-day win-back trigger: user_inactive_days=90 active 112 enrolled
Webinar follow-up trigger: webinar_registered draft wait 1d → send recap → branch on click
Other triggers: tenant_onboarded, manual_enroll. Webinar registration offers a GDPR-strict, default-unticked newsletter opt-in that enrols as pending (source=webinar_registration).
Deliverability — per from-domain
DomainReputationSPF / DKIM / DMARCBounce 30dComplaint 30d
news.petanque.life Good (94/100) pass / pass / pass 0.6 % 0.02 %
mail.petanque.life Watch (71/100) pass / pass / p=quarantine 1.8 % 0.08 %
Auto-halt thresholds: bounce > 2 % or complaint > 0.1 %. Spam-score history per campaign kept alongside.
Delivery layer: EmailDistributor abstraction — MailjetDistributor (primary), ACSDistributor (failover), NullDistributor (dev/test). Webhook receiver verifies HMAC signatures, idempotent on provider_event_id.
Suppression rules: hard bounce → suppress immediately; 5 consecutive soft bounces → escalate to suppression; complaint → suppress + status=complained. Suppression list is checked before every send.
AI assist — Gemini 2.5 Pro
Subject lines — 10 suggestions ranked by predicted open rate 📝 Content polish — tone/length rewrite of a draft block 🎭 Audience reaction preview — 5 personas react before you send
Quota: 37 / 50 requests left (per operator, 24 h). Every call is audit-logged with prompt + response hash.
Engagement analytics
🔥 Click heatmap — click-coordinate aggregation per campaign 📊 Cohort open/click rates — by tenant, role, language, source 📉 Engagement score — rolling 30 d per subscriber; score < 0.1 flags at-risk and feeds the re-engagement automation
Event data (opens/clicks) is kept 90 days (TTL), aggregates forever.
Compliance: consent proof stored per subscribe (IP hash + UA hash + form id + timestamp + method) · CAN-SPAM physical address auto-injected in every footer · full audit trail (subscribe/confirm/preferences/unsubscribe/import/suppression/GDPR) · 3-year post-unsubscribe retention, then erase.

Tenant visibility: tenant admins see a read-only view of their own scope (lists, subscriber counts, campaigns) in their admin app — scoped by tenant_id, 403 without tenant context.

Webinar management

Webinars
SlugTitleStartsAttendeesStatusNote
getting-started-draw-engine Getting started with the draw engine 2026-08-14 19:00 96 / 150 SCHEDULED promoted (4 w before)
club-site-in-30-min A club site in 30 minutes 2026-08-28 18:30 41 / 100 SCHEDULED promoted
scoreboard-kits-in-practice Scoreboard kits in practice 2026-06-11 19:00 138 attended PUBLISHED recording attached
licensing-deep-dive Licensing deep dive DRAFT never leaks to public API
Status machine: DRAFT → SCHEDULED → LIVE → ENDED → PUBLISHED (or CANCELLED). Attendees are unique per (webinar, email); interest leads unique per email with topics, unsubscribed and notified counters.
Edit webinar — getting-started-draw-engine
Email flow (ACS): confirmation, reminder-24h, reminder-1h, recording-available, cancelled — with Google Calendar & Outlook deep links, EN/SV with EN fallback. Reminder job every 15 min (±15 min tolerance); post-event job hourly fans out to attendees + matching interest leads. Both idempotent via sent-at markers.
Google integration status: connected — service account with domain-wide delegation. Error states surfaced here: not_configured · client_library_missing · calendar_api_error (retry with backoff).
Empty — no webinars yet: "Create your first webinar; DRAFTs never leak to the public API."
Loading attendee list…
Error — calendar_api_error while syncing: the webinar saved, but the Meet event was not updated. Retry sync.