# E-mails in code: zichtbaar maken, migreren en verbeteren

Canonieke URL: https://brew.new/nl/blog/emails-in-code-visibility-migration-mcp
Auteur: Thomas Park
Gepubliceerd: 2026-10-12
Bijgewerkt: 2026-10-15

E-mails in code zijn productmails die in je codebase zijn gedefinieerd en die je uitrolt zoals elke andere functie. Vraag in een team dat op Resend en React Email bouwt welke e-mails het product verstuurt, en het blijft even stil. De welkomstmail is een component in `emails/`. De wachtwoordreset staat in het dashboard van de auth-provider. De e-mail over het einde van de proefperiode is een string in een cronjob. Stripe verstuurt het betaalbewijs. Niemand weet meer wat de e-mail “we missen je” verstuurt.

Elke e-mail was makkelijk uit te rollen, dus niemand overziet het geheel. Wij zouden dat in deze volgorde aanpakken: eerst zichtbaarheid, dan migratie, dan verbetering. De coding agent die je al gebruikt, kan het meeste daarvan doen, gekoppeld aan Brew via MCP.

## De belangrijkste punten

- Begin met een inventaris, niet met een nieuwe tool: elke e-mail, met de trigger, het bestand en de variabelen.
- Een coding agent kan de verzendaanroepen vinden, elk sjabloon renderen en het importeren met de MCP-tool `import_email`. De import is deterministisch, gebruikt geen AI en is gratis.
- Geïmporteerde e-mails komen op één canvas per groep terecht, zodat het team ze naast elkaar bekijkt op desktop en mobiel.
- Bij de migratie wordt een verzending in Resend-stijl een trigger plus een gepubliceerde automatisering, die je activeert met één REST-aanroep. Productiecode gebruikt REST, niet MCP.
- Verhuis één type e-mail tegelijk achter een feature flag, en verhuis transactionele e-mail als laatste.

## Waarom e-mail in code onzichtbaar wordt

React Email en Resend hebben e-mail voor developers een stuk beter gemaakt: getypeerde componenten, beoordeeld in pull requests. De prijs betaal je later.

**De e-mails staan verspreid.** Componenten, inline HTML in een achtergrondjob, sjablonen in het dashboard van een derde partij, e-mails die een andere service voor je verstuurt. Geen enkel bestand zet ze allemaal op een rij.

**Een preview is geen productie.** De previewserver van React Email toont één sjabloon met de bijbehorende preview-props, niet wat een klant heeft ontvangen, en niets van de andere sjablonen.

**Alleen engineers kunnen kijken.** Product, design en support moeten een engineer vragen om het project te draaien, dus meestal doen ze dat niet.

**Na de lancering kijkt niemand er meer naar.** De e-mail van twee jaar oud heeft nog het oude logo en een pakket dat je niet meer verkoopt. Niemand merkt het, omdat niemand ze bij elkaar ziet.

## Stap 1: zichtbaarheid

Het doel is één gedeeld overzicht van elke e-mail. Niets hiervan raakt productie.

![De drie fases om e-mails uit je code te halen: zichtbaarheid met import_email en een canvas per groep, migratie met domeinen, contacten, triggers en automatiseringen, en optimalisatie met controles, weergavetests en statistieken.](/images/blog/emails-in-code-three-phases.png)

*Elke fase is op zichzelf nuttig, en je kunt na elke fase stoppen.*

### Koppel je coding agent aan Brew

De MCP-server van Brew staat op `https://brew.new/api/mcp` en gebruikt OAuth: je logt in bij Brew en kiest het merk waarin de agent mag werken. In Claude Code:

```bash
claude mcp add --transport http brew "https://brew.new/api/mcp"
```

Daarmee sla je de server alleen op in de lokale configuratie van dit project. Voeg `--scope user` toe om hem in elk project te gebruiken. Voer `claude mcp login brew` uit, of open `/mcp` en authenticeer je bij Brew, voordat je de tools van Brew gebruikt.

De [documentatie over het instellen van clients](https://docs.brew.new/api-reference/mcp/connect-your-client) behandelt ook Cursor en Codex. Clients zonder OAuth kunnen een API-sleutel voor één merk gebruiken. Laat de agent daarna `get_brew_capabilities` aanroepen, zodat hij de actuele lijst met tools leest in plaats van te gokken.

### Bouw de inventaris op

Vraag de agent om een lijst:

```text
Find every email this codebase sends. Search for provider calls
(resend.emails.send, postmark, sendgrid, nodemailer, SES), React Email
components and render() calls, and any email templates referenced from
config or a third-party dashboard.

For each email, record: a name, what triggers it, the file and line that
sends it, the template file, the provider, and the variables it uses.
Write the result as a table in EMAILS.md. List anything you could not
resolve under "Unknown" instead of guessing.
```

Laat de sectie “Unknown” staan. Auth-bevestigingen en betaalbewijzen van Stripe zijn geen verzendaanroepen in je code, maar ze horen wel op de lijst. Commit `EMAILS.md`. Stop je hier, dan heb je in elk geval een lijst die je gisteren nog niet had.

### Render elk sjabloon met realistische data

De beste input voor de import is de HTML die je code al produceert. De [render-utility](https://react.email/docs/utilities/render) van React Email geeft je die string:

```tsx
import { render } from 'react-email'
import WelcomeEmail from './emails/welcome'

const html = await render(
  <WelcomeEmail firstName="Ana" planName="Pro" trialDays={14} />
)
```

Oudere projecten importeren `render` uit `@react-email/render`. Gebruik realistische fixtures, niet `"test"`, want je team gaat deze e-mails beoordelen. Laat de agent een script schrijven dat elk sjabloon naar een bestand rendert.

Brew importeert een React Email-module ook direct met `format: "jsx"`, en leest die dan statisch, zonder hem uit te voeren. Props krijgen hun standaardwaarde of de waarde uit `PreviewProps`, componenten die uit andere lokale bestanden worden geïmporteerd, worden weggelaten of geweigerd, en `await` en vergelijkbare constructies worden geweigerd. Gaat het om meer dan één zelfstandig bestand, gebruik dan gerenderde HTML.

### Importeer alles op één plek

Importeer daarna elke render met `import_email`, met één `groupName` per productonderdeel:

```text
For each rendered email in ./email-renders, call import_email with
format "html", the title and subject line from EMAILS.md, and a
groupName by area: "Auth", "Onboarding", "Billing" or "Lifecycle".
Report the emailId for each one and any warnings in its assetReport.
```

Developers vragen terecht of de import hun e-mails herschrijft. Dat doet hij niet:

- Het is een deterministische compiler, geen AI-model, en hij kost geen credits.
- Hij levert een bewerkbaar ontwerp in Brew op, terwijl de bezorging voor de geïmporteerde rijen de oorspronkelijke bron-HTML behoudt.
- Openbare afbeeldingen host hij opnieuw, en het `assetReport` vermeldt welke hij niet kon ophalen.
- Hij verwijdert scripts, iframes, formulieren en event handlers, en schakelt bestaande trackingpixels voor openingen uit.
- Elke bron mag maximaal 5 MB zijn. Je kunt geen URL opgeven: de tool verwacht de markup zelf, en `baseUrl` zet alleen relatieve paden naar afbeeldingen om.

### Beoordeel de e-mails als team

Elke groep krijgt een canvas. Open `brew.new/emails/onboarding` en de hele reeks staat naast elkaar, met een schakelaar voor desktop en mobiel en opmerkingen bij elk ontwerp. Links naar een canvas werken voor mensen die zijn ingelogd in je werkruimte in Brew. Er is geen openbare deellink, dus nodig je reviewers uit.

Hier komen de problemen boven: twee logo’s, een knop met een dode link, drie footers. Voer `audit_email` uit voordat je iets repareert. De tool controleert links, afbeeldingen, compliance, de geladen grootte, ondersteuning in e-mailclients, toegankelijkheid en tekst, kost 5 credits per volledige controle en accepteert een opgeslagen `emailId` of ruwe HTML, dus je kunt ook e-mails controleren die je niet hebt geïmporteerd. Onze [QA-checklist voor het versturen](/nl/blog/pre-send-email-qa-checklist) legt elke controle en elke drempel uit.

Je hebt nu een inventaris, elke e-mail op een canvas en een lijst met problemen, en je code verstuurt nog alles zoals voorheen.

## Stap 2: migratie

Nu verstuurt Brew een deel van de e-mails in plaats van je code. Niet elke e-mail hoeft mee te verhuizen, en op elk moment verstuurt maar één systeem een bepaald type e-mail.

### Breng de concepten in kaart

| Nu in je code | In Brew |
| --- | --- |
| `resend.emails.send({ react: <WelcomeEmail /> })` | Een triggerevent plus een gepubliceerde automatisering met één stap E-mail versturen |
| Props die aan de component worden doorgegeven | Velden in de triggerpayload, gebruikt als personalisatietags zoals `{{ firstName \| there }}` of `{{ trigger.planName }}` |
| Het domein in `from` | Een geverifieerd verzenddomein in Brew |
| Je tabel met gebruikers | Contacten, aangemaakt via de REST-API of een integratie |
| Een cronjob die een reeks verstuurt | Een automatisering met de stappen Wachten, Filter en E-mail versturen |

### 1. Stel het verzenddomein in

De agent roept `create_domain` aan voor de DNS-records, en daarna `verify_domain` zodra die live staan. Domeinen die je via MCP en de API aanmaakt, zijn marketingdomeinen. Stel voor wachtwoordresets en betaalbewijzen in de app van Brew een domein in met het doel Transactioneel: dat laat de afmeldfooter weg en bezorgt ook bij afgemelde contacten. [Je verzenddomein verifiëren](https://docs.brew.new/get-started/verify-your-sending-domain) behandelt de records.

### 2. Haal je contacten binnen

Via MCP kun je contacten bijwerken, maar niet aanmaken. Maak ze aan vanuit je backend met `POST /v1/contacts`, dat één contact of een batch van maximaal 1.000 contacten toevoegt of bijwerkt en aangepaste velden automatisch aanmaakt, of upload een CSV in de app. Een trigger activeren maakt ook het contact uit de payload aan of werkt het bij. Importeer alleen mensen die ermee hebben ingestemd om van je te horen. [Contacten toevoegen](https://docs.brew.new/audience/add-contacts) legt uit wat Brew verwacht.

### 3. Vervang previewwaarden door personalisatietags

Makkelijk over het hoofd te zien: in de geïmporteerde welkomstmail staat letterlijk “Hi Ana”. Vervang elke fixturewaarde door een personalisatietag zoals `{{ firstName | there }}`, waarbij de tekst na de verticale streep de standaardwaarde is. Gebruik de editor van Brew, of laat de agent `edit_email` uitvoeren, beperkt tot die vervangingen. Dat is een AI-bewerking die credits kost, dus controleer het voorbeeld. [Personalisatietags](https://docs.brew.new/create-emails/merge-tags) beschrijft de syntaxis.

### 4. Maak de trigger en de automatisering aan

De agent maakt een trigger aan met `save_trigger`. Het payloadschema moet het veld `email` van de ontvanger bevatten. Geef de trigger een getypeerd contract (`infer_payload_contract` stelt er een op aan de hand van een voorbeeldpayload, `set_trigger_contract` slaat het op), en haal het daarna op met `get_trigger_contract` in het formaat `ts` of `zod`, zodat je backend en Brew het eens zijn over de vorm van de payload.

Daarna bouwt `save_automation` de trigger plus een stap E-mail versturen, met de e-mail, het domein en de onderwerpregel. De automatisering blijft een concept tot je publiceert. `dryRun` valideert de flow, `test_automation` stuurt een echt exemplaar naar je eigen adres, en `check_trigger` bevestigt dat een gepubliceerde automatisering luistert voordat productie iets activeert.

### 5. Vervang de aanroep

Dit is de enige wijziging in je applicatiecode. Voorheen, met Resend:

```ts
await resend.emails.send({
  from: 'Acme <hello@acme.com>',
  to: user.email,
  subject: 'Welcome to Acme',
  react: <WelcomeEmail firstName={user.firstName} planName={plan.name} />,
})
```

Nu, met Brew:

```ts
const res = await fetch(
  `https://brew.new/api/v1/automations/triggers/${process.env.BREW_WELCOME_TRIGGER_ID}/fire`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.BREW_API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `welcome-${user.id}`,
    },
    body: JSON.stringify({
      payload: { email: user.email, firstName: user.firstName, planName: plan.name },
    }),
  }
)

if (!res.ok) {
  // 422 means the automation is still a draft; 400 means the payload failed the schema.
  throw new Error(`Brew trigger failed: ${res.status} ${await res.text()}`)
}
```

`fetch` gooit ook bij een geweigerd verzoek geen fout, dus controleer `res.ok` en laat je jobrunner het opnieuw proberen of alarm slaan. Het endpoint geeft `202` terug als het het event accepteert, en `422` als er geen gepubliceerde automatisering naar de trigger luistert.

Bouw de `Idempotency-Key` op uit een stabiele ID, zoals die van de gebruiker of de bestelling. Een herhaalde sleutel geeft de oorspronkelijke run terug in plaats van twee keer te versturen, en wie een sleutel hergebruikt met een andere body, krijgt `409 IDEMPOTENCY_CONFLICT`. Brew bewaart de sleutel van een event even lang als de ontvangstregistratie van de trigger, en dat is 90 dagen, niet de 24 uur die veel API’s aanhouden. Behandel sleutels als permanent: één per actie. Een sleutel op basis van een tijdstempel verandert bij elke nieuwe poging, en een sleutel die je weken later voor een andere actie hergebruikt, levert mogelijk de oude run opnieuw op in plaats van een nieuwe verzending.

Productiecode roept dit endpoint aan met een API-sleutel, niet via MCP. Bij OAuth vraagt MCP een persoon om live verzendingen, het activeren van triggers en het publiceren te bevestigen: goed voor een chat, verkeerd voor een request handler. De [gids over MCP of REST](/nl/blog/mcp-vs-rest-api-for-email-marketing-which-workflow-should-you-use) behandelt die verdeling.

### 6. Zet één e-mail tegelijk over

1. Zet de nieuwe aanroep achter een feature flag naast de oude, zodat terugschakelen een kwestie van configuratie is.
2. Begin met een e-mail met weinig risico, zoals een duwtje in je lifecycleflow, en houd een paar dagen de bezorging en de klikken in de gaten.
3. Verhuis daarna de reeksen, zodra hun triggers met de juiste payloads worden geactiveerd.
4. Verhuis transactionele e-mail als laatste. Kapotte wachtwoordresets en magic links vallen gebruikers als eerste op.
5. Verwijder het oude pad na een week zonder problemen, en werk `EMAILS.md` bij.

De [migratiegids voor Loops](https://docs.brew.new/migrations/loops) volgt hetzelfde patroon.

### Wat in code moet blijven

Bevestigingsmails en magic links van Supabase blijven uit Supabase komen. De Supabase-integratie van Brew luistert naar gebruikersevents om automatiseringen te starten, en vervangt de e-mails van Supabase Auth niet. Een gespecificeerde factuur die bij het versturen uit complexe data wordt opgebouwd, is in code misschien ook eenvoudiger, dus controleer eerst wat je personalisatietags nodig hebben. Houd geïmporteerde kopieën op het canvas, zodat ook deze e-mails zichtbaar blijven.

## Stap 3: optimalisatie

**Controleer de hele set.** Voer `audit_email` uit voor een hele groep, los de problemen op en voer de controle opnieuw uit vóór grote verzendingen.

**Controleer echte e-mailclients.** `test_email_rendering` levert screenshots uit specifieke e-mailclients en apparaten, inclusief de donkere modus, voor 10 credits per run. `create_inbox_placement_test` stuurt een echt exemplaar naar seedinboxen en meldt waar het terechtkwam, ook voor 10 credits.

**Maak de set consistent.** Herstel inconsistenties in de editor, of laat de agent met `edit_email` één wijziging, zoals een nieuwe footer, op een hele groep toepassen.

**Lees de resultaten.** `get_email_analytics` rapporteert verzendingen en betrokkenheid. `list_insights` geeft bevindingen terug die Brew in je account heeft gevonden, elk met de metrics die eraan ten grondslag liggen.

**Test varianten bewust.** Maak varianten op het canvas en verdeel contacten met een stap Splitsen op basis van gedrag, bijvoorbeeld of ze hebben geklikt. Brew kiest bij een willekeurige splitsing niet zelf een winnaar, dus vergelijk de varianten zelf.

**Houd nieuwe e-mails zichtbaar.** Voeg een regel toe aan `AGENTS.md` of een vergelijkbaar bestand: nieuwe productmails worden in Brew gemaakt, via een trigger geactiveerd en in dezelfde pull request aan `EMAILS.md` toegevoegd.

## Veelgestelde vragen

### Kan Brew React Email-sjablonen importeren?

Ja. `import_email` accepteert gerenderde HTML, MJML, een opgeslagen `.eml`-bericht of een React Email-module. Gerenderde HTML is het betrouwbaarst, omdat een module statisch wordt gelezen: props krijgen hun standaard- of previewwaarde, en componenten die uit andere lokale bestanden worden geïmporteerd, worden weggelaten.

### Kan ik Brew aan mijn Resend-account koppelen?

Nee. Er is geen integratie, import of export voor Resend. Je importeert de HTML die je code rendert, en als je migreert, activeert je code een trigger in Brew in plaats van Resend aan te roepen.

### Moet mijn backend Brew via MCP aanroepen?

Nee. Gebruik MCP vanuit een coding agent voor de inrichting, imports, controles en tests. Productiecode hoort de REST-API aan te roepen met een API-sleutel, omdat MCP bij OAuth-koppelingen een persoon vraagt om live verzendingen en het activeren van triggers te bevestigen.

### Verandert het importeren hoe een e-mail eruitziet?

De import zet de e-mail om in een bewerkbaar ontwerp in Brew en bewaart de oorspronkelijke bron-HTML voor de bezorging. Hij verwijdert scripts, iframes, formulieren en event handlers, schakelt bestaande trackingpixels uit en host openbare afbeeldingen opnieuw. Bekijk elke e-mail op het canvas, in de desktop- en de mobiele weergave, voordat je hem vanuit Brew verstuurt.
