Broadcast Kampagner – Komplet Teknisk Guide
Formål: Denne guide beskriver i detaljer, hvordan Broadcast Email Kampagne-systemet fungerer i Kredia DK-appen, og hvad der kræves for at implementere den samme funktion i den klonede app (Kredia24.se / Silentium-appen).
1. Overblik – Hvad er Broadcast Kampagner?
En Broadcast Kampagne er en enkeltstående email-udsendelse til hele databasen (eller en udvalgt del af den). Det er ikke en automatiseret flow-kampagne – det er en one-shot email der sendes manuelt eller på et planlagt tidspunkt.
Eksempel på use case:
- "Send et nyhedsbrev til alle 4.200 leads med gyldige emails"
- "Planlæg en kampagne til at gå ud kl. 10:00 fredag morgen"
2. Database Entiteter
Der kræves to entiteter i databasen:
2.1 BroadcastCampaign
Indeholder selve kampagne-konfigurationen og statustallene.
{
"name": "string", // Kampagnenavn (intern reference)
"template_id": "string", // Ref til EmailTemplate entity
"subject": "string", // Basis subject (fra template)
"subject_override": "string", // Valgfrit: overstyrer template subject
"html_content": "string", // HTML email indhold (kopieres fra template)
"from_email": "string", // Afsender email
"from_name": "string", // Afsender navn
"status": "string", // Se statusflow nedenfor
"scheduled_send_at": "date-time", // Sættes kun hvis planlagt
"sent_at": "date-time", // Hvornår afsendelse afsluttedes
"total_recipients": "number", // Antal leads i BroadcastRecipient
"will_receive_count": "number", // Antal der faktisk modtager (ikke opt-out)
"opted_out_count": "number", // Antal opt-out ekskluderede
"manually_excluded_count": "number",// Antal manuelt ekskluderede
"sent_count": "number", // Løbende tæller for sendte emails
"failed_count": "number" // Løbende tæller for fejlede emails
}
Statusflow:
draft → (tryk "Beregn modtagere") → ready → (tryk "Send nu") → sending → sent
↓
(vælg "Planlæg") → scheduled → sending → sent
↓
paused (manuel pause)
↓
sending (genoptag)
2.2 BroadcastRecipient
Én post per lead per kampagne. Genereres af generateBroadcastRecipients funktionen.
{
"campaign_id": "string", // Ref til BroadcastCampaign.id
"lead_id": "string", // Ref til Lead.id
"email": "string", // Råt email (som gemt på lead)
"email_normalized": "string", // Lowercase + trimmed email
"first_name": "string",
"last_name": "string",
"is_opted_out": "boolean", // true = afmeldt, sendes ALDRIG til
"is_manually_excluded": "boolean", // true = admin har ekskluderet manuelt
"will_receive": "boolean", // = !is_opted_out && !is_manually_excluded
"sent": "boolean", // true = email er sendt
"sent_at": "date-time", // Hvornår sendt
"failed": "boolean", // true = 3 forsøg fejlede
"error_message": "string" // Fejlbesked fra Brevo API
}
3. EmailTemplate Entitet (forudsætning)
Broadcast Kampagner kræver at der eksisterer en EmailTemplate entitet med:
{
"name": "string",
"subject": "string",
"html_content": "string", // HTML med {{variabel}}-placeholders
"from_email": "string",
"from_name": "string",
"is_active": "boolean"
}
Understøttede template-variabler:
{{firstName}}– modtagerens fornavn{{lastName}}– modtagerens efternavn{{email}}– modtagerens email{{UNSUBSCRIBE_LINK}}– autogenereret afmeld-URL{{sendDate}}– afsendelsesdato formateret (fx "15. marts 2026"){{getTomorrowFormatted}}– morgendagens dato formateret
4. EmailOptOut Entitet (forudsætning)
Systemet kræver en EmailOptOut entitet:
{
"email": "string",
"opted_out": "boolean", // true = afmeldt
"opted_out_at": "date-time"
}
Alle emails med opted_out: true ekskluderes automatisk ved generering af modtagerlisten.
5. Backend Funktioner
Der kræves 5 backend funktioner. Alle er Deno/TypeScript funktioner.
5.1 generateBroadcastRecipients
Formål: Henter alle leads, deduplicerer, tjekker opt-out og opretter BroadcastRecipient-poster.
Kræver: Admin auth | Input: { campaignId: string }
Logik trin-for-trin:
- Auth-tjek:
user.role === 'admin', ellers returner 403 - Hent kampagne:
BroadcastCampaign.get(campaignId)– fejl hvis ikke fundet - Slet eksisterende recipients: Hent og slet alle
BroadcastRecipientmedcampaign_id = campaignId(tillader regenerering) - Hent alle leads: Pagineret, max 5000 leads (500 per page)
- Filtrer: Kun leads med
emailder indeholder@ - Deduplikér: Én entry per normaliseret email – behold den senest oprettede
- Hent opt-outs:
EmailOptOut.filter({ opted_out: true })→ byg Set med emails - Ekskluderede domæner (hard-coded):
@salus.group,@axogroup.com,@example.com,@mv.evolution360.net,@lendme.dk⚠️ TILPAS TIL SE-APPEN - Ekskluder hard bounced:
lead.email_hard_bounce_count > 0 - Ekskluder complaints:
lead.email_complaint_count > 0 - Bulk opret:
BroadcastRecipient.bulkCreate(recipientRecords) - Opdater kampagne status til
readymed counts
5.2 sendBroadcastCampaign
Formål: Den primære sendefunktion. Trigges direkte fra UI'et.
Kræver: Admin auth | Input: { campaignId: string }
Logik:
- Sæt kampagne status til
sending - Hent ALLE recipients → deduplikér → filtrer kun
!sent && !failed - Sikkerhedsfilter: Fjern opt-out, interne domæner, manually_excluded
- Send i batches af 5 med 200ms spacing per email
- 500ms delay mellem batches
- Rate limit (429): Exponential backoff, starter 2s, max 16s
- Per email: Max 3 forsøg med stigende retry-delay (1s/2s/3s)
- Template variable replacement per recipient:
{{firstName}},{{lastName}},{{email}},{{UNSUBSCRIBE_LINK}},{{getTomorrowFormatted}}
- Afmeld-link:
[APP_URL]/api/functions/handleEmailOptOutAction?email=[encoded_email]⚠️ TILPAS URL - Fallback footer: Hvis afmeld-link ikke i HTML, tilføjes footer automatisk
- Final: Sæt kampagne status
sent+sent_at
5.3 resumeBroadcastCampaigns (CRON – hvert 5. min)
Formål: Finder kampagner med status = 'sending' og sender 50 emails per kørsel.
Logik:
- Find alle
status = 'sending'kampagner - Per kampagne: hent
will_receive: true, sent: falserecipients → deduplikér → send max 50 - Tjek om der er recipients tilbage → hvis 0: sæt status
sent - Kampagnen fortsætter automatisk næste kron-kørsel
Cron opsætning: Scheduled automation, repeat_interval: 5, repeat_unit: "minutes"
5.4 processScheduledBroadcasts (CRON – hvert 5. min)
Formål: Aktiverer planlagte kampagner når scheduled_send_at er nået.
Logik:
- Find alle
status = 'scheduled'kampagner - Filtrer:
campaign.scheduled_send_at <= now - Tjek at recipients eksisterer – ellers skip med fejl-log
- Opdater status til
sending→resumeBroadcastCampaignssender dem
5.5 sendTestEmail
Formål: Sender testmail til admin under wizard trin 4.
Input: { campaignId, toEmail, fromName?, fromEmail? }
Erstatter variabler med dummy-data, sender via Brevo – logger IKKE til BroadcastRecipient.
5.6 toggleAllBroadcastRecipients
Input: { campaignId, exclude: boolean }
Sæt is_manually_excluded og will_receive på alle recipients der ikke er opted-out.
5.7 optOutFailedBroadcastRecipients
Input: { campaignId }
Opt-out alle recipients med failed: true → opret EmailOptOut-post for hver.
6. Frontend Sider
6.1 BroadcastCampaigns (listeside)
- Kampagneliste med status-badges (draft/ready/scheduled/sending/paused/sent/cancelled)
- Pause / Genoptag af
sending-kampagner - Slet (kun draft, ready, scheduled)
- Klon kampagne (kopierer til ny
draft) - Preview HTML i modal
- Statistik-link →
BroadcastStats?id=[id] - Auto-refresh hvert 30s hvis nogen er
sending sending-kampagner sorteres øverst
6.2 BroadcastCampaignWizard (4-trins)
Trin 1 – Opret:
- Kampagnenavn + vælg EmailTemplate
- Valgfrit: Subject override (med emoji-inserter ✓ ✔ ☑ ✅)
- Valgfrit: Afsender navn/email override
- Auto-sync: Template-indhold synkroniseres hvis ændret siden sidst
Trin 2 – Beregn:
- Knap → kalder
generateBroadcastRecipients - Auto-advance til trin 3 ved succes
Trin 3 – Gennemse:
- Stats: Total / Opt-out / Ekskluderet / Vil modtage
- Søg på emailtekst → bulk opt-out (fx søg på "test")
- Toggle alle / Slet alle
- Søgbar recipient-liste med individuel exclude/include
Trin 4 – Send:
- Advarselsboks
- Send nu ELLER planlæg til præcist tidspunkt (datetime-picker)
- Test-email sektion
- Bekræftelse: checkbox + skriv "SEND"
6.3 BroadcastStats (statistik)
- 6 stats-bokse: Total, Sendt, Fejlet, Afventer, Afmeldte, Success rate
- Kampagne-detaljer
- Sortérbar recipients-tabel: Email, Navn, Status, Opens, Clicks, Sendt-tidspunkt
- "Opt-out alle fejlede"-knap
7. Brevo Email API
Endpoint: POST https://api.brevo.com/v3/smtp/email
{
"sender": { "name": "Afsender", "email": "fra@domain.se" },
"to": [{ "email": "til@email.com", "name": "Modtager" }],
"subject": "Email Subject",
"htmlContent": "<html>...</html>"
}
Rate limits: ~5 RPS → 200ms spacing per email + 500ms batch-delay. Med 50 emails per 5-min cron: ~600 emails/time.
Secrets:
BREVO_API_KEY– Brevo API nøgleAPP_URL– App's base URL (afmeld-links)
8. Afmeldings-mekanisme
- Afmeld-URL:
[APP_URL]/afmeld?email=[encoded_email]eller viahandleEmailOptOutAction - Opretter/opdaterer
EmailOptOutpost +Lead.email_opt_out_status = 'opt_out' - Fremtidige
generateBroadcastRecipientsekskluderer automatisk
9. Implementeringscheckliste for SE-appen
Entiteter:
- [ ]
BroadcastCampaign - [ ]
BroadcastRecipient - [ ]
EmailTemplate - [ ]
EmailOptOut
Secrets:
- [ ]
BREVO_API_KEY - [ ]
APP_URL=https://kredia24.se
Backend Funktioner:
- [ ]
generateBroadcastRecipients - [ ]
sendBroadcastCampaign - [ ]
resumeBroadcastCampaigns - [ ]
processScheduledBroadcasts - [ ]
sendTestEmail - [ ]
toggleAllBroadcastRecipients - [ ]
optOutFailedBroadcastRecipients - [ ]
handleEmailOptOutAction
Automations:
- [ ]
resumeBroadcastCampaigns– hvert 5. minut - [ ]
processScheduledBroadcasts– hvert 5. minut
Frontend Sider:
- [ ]
BroadcastCampaigns - [ ]
BroadcastCampaignWizard - [ ]
BroadcastStats - [ ]
Afmeld(afmeld-side)
SE-specifikke tilpasninger:
- [ ] Opdater ekskluderede domæner i
generateBroadcastRecipients - [ ] Opdater hardcoded test-email liste
- [ ]
APP_URLpeger på kredia24.se - [ ] Templates med
@kredia24.seafsender email - [ ] Tjek Lead-entitets feltnavne matcher (email, email_hard_bounce_count, email_complaint_count, email_opt_out_status)
10. Vigtige Designbeslutninger og Gotchas
-
Recipient-listen er låst ved generering – Nye leads der tilmelder sig efterfølgende er IKKE inkluderet. Intentionelt for konsistens.
-
Deduplicering sker to steder – I
generateBroadcastRecipientsOG i sendefunktionerne. Dobbelt-sikkerhed. -
Sending er asynkron og kan genoptages –
sendBroadcastCampaignstarter processen,resumeBroadcastCampaigns(cron) klarer det tunge arbejde. Fejler den midtvejs, fortsætter cron'en automatisk. -
Pause-funktion – Sæt status
pausedfra UI → cron springer over. Genoptag: sæt statussending. -
Template synkronisering – Wizard auto-synkroniserer template-indhold til kampagnen ved load.
-
Opens og Clicks er ikke kampagne-specifikke – Stats viser Lead's totale
email_open_count/email_click_count, ikke kun for denne kampagne. -
Throughput: 50 emails per 5-min cron = ~600 emails/time. 4.000 leads ≈ 7 timer. 1.000 leads ≈ 1-2 timer.
