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.

Bankero.dk

Din pålidelige partner til lånsammenligning i Danmark. Vi finder de bedste lånetilbud til dig.

Kontakt

Nexa Finance A/S

Vesterbrogade 149

1620 København V

CVR: DK37798037

info@bankero.dk

Bankero.dk er ikke en långiver, men en uafhængig lånesammenligningstjeneste. Når du ansøger om lån via siden, videresendes du til en ekstern långiver. Bankero.dk kan modtage kommission fra samarbejdspartnere for formidling af låneansøgninger. Dette har ingen indflydelse på dine lånevilkår eller omkostninger.

© 2026 Bankero. Alle rettigheder forbeholdes.