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:

  1. Auth-tjek: user.role === 'admin', ellers returner 403
  2. Hent kampagne: BroadcastCampaign.get(campaignId) – fejl hvis ikke fundet
  3. Slet eksisterende recipients: Hent og slet alle BroadcastRecipient med campaign_id = campaignId (tillader regenerering)
  4. Hent alle leads: Pagineret, max 5000 leads (500 per page)
  5. Filtrer: Kun leads med email der indeholder @
  6. Deduplikér: Én entry per normaliseret email – behold den senest oprettede
  7. Hent opt-outs: EmailOptOut.filter({ opted_out: true }) → byg Set med emails
  8. Ekskluderede domæner (hard-coded): @salus.group, @axogroup.com, @example.com, @mv.evolution360.net, @lendme.dk ⚠️ TILPAS TIL SE-APPEN
  9. Ekskluder hard bounced: lead.email_hard_bounce_count > 0
  10. Ekskluder complaints: lead.email_complaint_count > 0
  11. Bulk opret: BroadcastRecipient.bulkCreate(recipientRecords)
  12. Opdater kampagne status til ready med counts

5.2 sendBroadcastCampaign

Formål: Den primære sendefunktion. Trigges direkte fra UI'et.

Kræver: Admin auth | Input: { campaignId: string }

Logik:

  1. Sæt kampagne status til sending
  2. Hent ALLE recipients → deduplikér → filtrer kun !sent && !failed
  3. Sikkerhedsfilter: Fjern opt-out, interne domæner, manually_excluded
  4. Send i batches af 5 med 200ms spacing per email
  5. 500ms delay mellem batches
  6. Rate limit (429): Exponential backoff, starter 2s, max 16s
  7. Per email: Max 3 forsøg med stigende retry-delay (1s/2s/3s)
  8. Template variable replacement per recipient:
    • {{firstName}}, {{lastName}}, {{email}}, {{UNSUBSCRIBE_LINK}}, {{getTomorrowFormatted}}
  9. Afmeld-link: [APP_URL]/api/functions/handleEmailOptOutAction?email=[encoded_email] ⚠️ TILPAS URL
  10. Fallback footer: Hvis afmeld-link ikke i HTML, tilføjes footer automatisk
  11. 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: false recipients → 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 sendingresumeBroadcastCampaigns sender 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øgle
  • APP_URL – App's base URL (afmeld-links)

8. Afmeldings-mekanisme

  1. Afmeld-URL: [APP_URL]/afmeld?email=[encoded_email] eller via handleEmailOptOutAction
  2. Opretter/opdaterer EmailOptOut post + Lead.email_opt_out_status = 'opt_out'
  3. Fremtidige generateBroadcastRecipients ekskluderer 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_URL peger på kredia24.se
  • [ ] Templates med @kredia24.se afsender email
  • [ ] Tjek Lead-entitets feltnavne matcher (email, email_hard_bounce_count, email_complaint_count, email_opt_out_status)

10. Vigtige Designbeslutninger og Gotchas

  1. Recipient-listen er låst ved generering – Nye leads der tilmelder sig efterfølgende er IKKE inkluderet. Intentionelt for konsistens.

  2. Deduplicering sker to steder – I generateBroadcastRecipients OG i sendefunktionerne. Dobbelt-sikkerhed.

  3. Sending er asynkron og kan genoptagessendBroadcastCampaign starter processen, resumeBroadcastCampaigns (cron) klarer det tunge arbejde. Fejler den midtvejs, fortsætter cron'en automatisk.

  4. Pause-funktion – Sæt status paused fra UI → cron springer over. Genoptag: sæt status sending.

  5. Template synkronisering – Wizard auto-synkroniserer template-indhold til kampagnen ved load.

  6. Opens og Clicks er ikke kampagne-specifikke – Stats viser Lead's totale email_open_count/email_click_count, ikke kun for denne kampagne.

  7. Throughput: 50 emails per 5-min cron = ~600 emails/time. 4.000 leads ≈ 7 timer. 1.000 leads ≈ 1-2 timer.