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
List
Scope
Active
Pending
Unsubscribed
Bounced / 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.
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).
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
Domain
Reputation
SPF / DKIM / DMARC
Bounce 30d
Complaint 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.
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
Slug
Title
Starts
Attendees
Status
Note
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.
Esc
β΅ opens the first hit Β· Esc closes Β· π = locked function (still shown β with an explanation)