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.