# Email dalam kode: lihat, migrasikan, dan tingkatkan

URL kanonis: https://brew.new/id/blog/emails-in-code-visibility-migration-mcp
Penulis: Thomas Park
Dipublikasikan: 2026-10-12
Diperbarui: 2026-10-15

Email dalam kode adalah email produk yang didefinisikan di codebase Anda dan dirilis seperti fitur lainnya. Di tim yang dibangun di atas Resend dan React Email, coba tanyakan email apa saja yang dikirim produk, dan semua orang akan terdiam sejenak. Email sambutan adalah komponen di `emails/`. Reset kata sandi ada di dasbor penyedia autentikasi. Email berakhirnya masa uji coba berupa string di dalam cron job. Bukti pembayaran dikirim oleh Stripe. Tidak ada yang ingat apa yang mengirim email “kami merindukan Anda”.

Setiap email mudah dirilis, sehingga tidak ada yang memegang gambaran utuhnya. Kami akan membereskannya secara berurutan: visibilitas, lalu migrasi, lalu peningkatan. Agen coding yang sudah Anda pakai bisa mengerjakan sebagian besarnya setelah terhubung ke Brew lewat MCP.

## Poin penting

- Mulailah dengan inventaris, bukan alat baru: setiap email beserta pemicu, file, dan variabelnya.
- Agen coding bisa menemukan panggilan pengiriman, merender setiap templat, dan mengimpornya dengan alat MCP `import_email`. Impor bersifat deterministik, tidak memakai AI, dan gratis.
- Email yang diimpor ditempatkan di satu kanvas per grup, sehingga tim bisa meninjaunya secara berdampingan dalam tampilan desktop dan seluler.
- Untuk migrasi, pengiriman ala Resend diganti dengan pemicu dan otomatisasi yang sudah dipublikasikan, yang dijalankan dengan satu panggilan REST. Kode produksi memakai REST, bukan MCP.
- Pindahkan satu jenis email dalam satu waktu di balik feature flag, dan pindahkan email transaksional paling akhir.

## Mengapa email yang didefinisikan di kode menjadi tak terlihat

React Email dan Resend membuat urusan email jauh lebih baik bagi pengembang: komponen bertipe yang ditinjau lewat pull request. Harganya baru terasa belakangan.

**Emailnya tersebar.** Komponen, HTML inline di background job, templat di dasbor pihak ketiga, email yang dikirim layanan lain untuk Anda. Tidak ada satu file pun yang mencantumkan semuanya.

**Pratinjau bukan produksi.** Server pratinjau React Email menampilkan satu templat dengan props pratinjaunya, bukan apa yang diterima pelanggan, dan tidak menampilkan apa pun untuk templat lain.

**Hanya engineer yang bisa melihatnya.** Tim produk, desain, dan dukungan harus meminta engineer menjalankan proyeknya, jadi biasanya mereka tidak melakukannya.

**Tidak ada yang ditinjau setelah peluncuran.** Email yang berumur dua tahun masih memakai logo lama dan menyebut paket yang sudah tidak Anda jual. Tidak ada yang menyadarinya karena tidak ada yang melihat semua email itu bersamaan.

## Langkah 1: Visibilitas

Tujuannya adalah satu tampilan bersama untuk setiap email. Tidak ada langkah di sini yang menyentuh produksi.

![Tiga fase memindahkan email keluar dari kode: visibilitas dengan import_email dan satu kanvas per grup, migrasi dengan domain, kontak, pemicu, dan otomatisasi, serta optimasi dengan audit, uji tampilan, dan analitik.](/images/blog/emails-in-code-three-phases.png)

*Setiap fase sudah bermanfaat dengan sendirinya, dan Anda bisa berhenti setelah fase mana pun.*

### Hubungkan agen coding Anda ke Brew

Server MCP Brew berada di `https://brew.new/api/mcp` dan memakai OAuth: Anda masuk dengan akun Brew dan memilih merek tempat agen boleh bekerja. Di Claude Code:

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

Menambahkan server hanya menyimpannya di konfigurasi lokal proyek ini. Tambahkan `--scope user` agar server berlaku untuk semua proyek. Sebelum memakai alat Brew, jalankan `claude mcp login brew`, atau buka `/mcp` lalu lakukan autentikasi ke Brew.

[Dokumentasi penyiapan klien](https://docs.brew.new/api-reference/mcp/connect-your-client) juga memuat petunjuk untuk Cursor dan Codex. Klien tanpa OAuth bisa memakai kunci API dengan cakupan merek. Lalu minta agen memanggil `get_brew_capabilities` agar agen membaca daftar alat yang berlaku saat ini, bukan menebak-nebak.

### Susun inventarisnya

Minta agen membuat daftar:

```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.
```

Pertahankan bagian “Unknown”. Email konfirmasi autentikasi dan bukti pembayaran Stripe memang bukan panggilan pengiriman di kode Anda, tetapi keduanya tetap harus masuk daftar. Commit file `EMAILS.md`. Jika Anda berhenti di sini pun, Anda sudah memiliki daftar yang kemarin belum ada.

### Render setiap templat dengan data yang realistis

Input impor terbaik adalah HTML yang sudah dihasilkan kode Anda. [Utilitas render](https://react.email/docs/utilities/render) dari React Email memberikan string tersebut:

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

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

Proyek yang lebih lama mengimpor `render` dari `@react-email/render`. Gunakan data fixture yang realistis, bukan `"test"`, karena tim Anda akan meninjau hasilnya. Minta agen membuat skrip yang merender setiap templat ke sebuah file.

Brew juga bisa mengimpor modul React Email secara langsung dengan `format: "jsx"`, dengan membacanya secara statis tanpa mengeksekusinya. Props diisi dengan nilai default atau `PreviewProps`, komponen yang diimpor dari file lokal lain akan dibuang atau ditolak, dan `await` serta sejenisnya tidak diterima. Untuk apa pun di luar satu file yang berdiri sendiri, gunakan HTML hasil render.

### Impor semuanya ke satu tempat

Lalu impor setiap hasil render dengan `import_email`, satu `groupName` untuk setiap area produk:

```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.
```

Wajar jika pengembang bertanya apakah impor akan menulis ulang email mereka. Jawabannya tidak:

- Impor adalah compiler deterministik, bukan model AI, dan tidak memakai kredit.
- Impor menghasilkan desain Brew yang bisa diedit, sementara pengiriman tetap memakai HTML sumber asli untuk baris-baris yang diimpor.
- Impor meng-hosting ulang gambar publik, dan `assetReport` mencantumkan gambar yang tidak bisa diambil.
- Impor menghapus skrip, iframe, formulir, dan event handler, serta menetralkan piksel pelacakan pembukaan yang sudah ada.
- Setiap sumber bisa berukuran hingga 5 MB. Tidak ada input berupa URL: alat ini menerima markup-nya langsung, dan `baseUrl` hanya dipakai untuk me-resolve path gambar relatif.

### Tinjau email bersama tim

Setiap grup mendapat satu kanvas. Buka `brew.new/emails/onboarding`, dan seluruh rangkaian tampil berdampingan, lengkap dengan sakelar tampilan desktop dan seluler serta komentar di setiap desain. Tautan kanvas bisa dibuka oleh orang yang sudah masuk ke ruang kerja Brew Anda. Tidak ada tautan berbagi publik, jadi undang para peninjau Anda.

Di sinilah masalah mulai terlihat: dua logo, tautan tombol yang mati, tiga footer. Jalankan `audit_email` sebelum memperbaiki apa pun. Alat ini memeriksa tautan, gambar, kepatuhan, ukuran saat dimuat, dukungan klien email, aksesibilitas, dan teks, memakan 5 kredit per audit lengkap, dan menerima `emailId` yang tersimpan atau HTML mentah, sehingga Anda bisa mengaudit email yang belum diimpor. [Daftar periksa QA sebelum mengirim](/id/blog/pre-send-email-qa-checklist) kami menjelaskan setiap pemeriksaan dan ambang batasnya.

Kini Anda memiliki inventaris, setiap email di kanvas, dan daftar masalah, sementara kode Anda tetap mengirim semuanya seperti sebelumnya.

## Langkah 2: Migrasi

Sekarang Brew mengirim sebagian email menggantikan kode Anda. Tidak semua email harus dipindahkan, dan pada satu waktu hanya satu sistem yang mengirim jenis email tertentu.

### Petakan konsepnya

| Di kode Anda saat ini | Di Brew |
| --- | --- |
| `resend.emails.send({ react: <WelcomeEmail /> })` | Peristiwa pemicu ditambah otomatisasi yang sudah dipublikasikan dengan satu langkah Kirim email |
| Props yang diteruskan ke komponen | Field payload pemicu, yang dipakai sebagai merge tag seperti `{{ firstName \| there }}` atau `{{ trigger.planName }}` |
| Domain `from` | Domain pengirim terverifikasi di Brew |
| Tabel pengguna Anda | Kontak, yang dibuat lewat REST API atau integrasi |
| Cron job yang mengirim rangkaian | Otomatisasi dengan langkah Tunggu, Filter, dan Kirim email |

### 1. Siapkan domain pengirim

Agen memanggil `create_domain` untuk mendapatkan record DNS, lalu `verify_domain` setelah record-record itu aktif. Domain yang dibuat lewat MCP dan API adalah domain pemasaran. Untuk reset kata sandi dan bukti pembayaran, siapkan domain khusus transaksional di aplikasi Brew: domain ini tidak menambahkan footer berhenti berlangganan dan tetap mengirim ke kontak yang sudah berhenti berlangganan. [Panduan memverifikasi domain pengirim](https://docs.brew.new/get-started/verify-your-sending-domain) membahas record-record tersebut.

### 2. Masukkan kontak

MCP bisa memperbarui kontak, tetapi tidak bisa membuatnya. Buat kontak dari backend Anda dengan `POST /v1/contacts`, yang membuat atau memperbarui satu kontak atau satu batch berisi hingga 1.000 kontak dan otomatis membuat field kustom, atau unggah file CSV di aplikasi. Menjalankan pemicu juga membuat atau memperbarui kontak yang ada di payload-nya. Impor hanya orang-orang yang sudah setuju menerima email dari Anda. [Panduan menambahkan kontak](https://docs.brew.new/audience/add-contacts) menjelaskan apa yang dibutuhkan Brew.

### 3. Ganti nilai pratinjau dengan merge tag

Hal ini mudah terlewat: email sambutan yang diimpor benar-benar bertuliskan “Hi Ana”. Ganti setiap nilai fixture dengan merge tag seperti `{{ firstName | there }}`, dengan teks setelah garis tegak sebagai nilai cadangan. Gunakan editor Brew, atau minta agen menjalankan `edit_email` yang dibatasi pada penggantian tersebut. Ini adalah pengeditan dengan AI yang memakai kredit, jadi periksa pratinjaunya. [Dokumentasi merge tag](https://docs.brew.new/create-emails/merge-tags) mencantumkan sintaksnya.

### 4. Buat pemicu dan otomatisasinya

Agen membuat pemicu dengan `save_trigger`. Skema payload-nya wajib menyertakan `email` penerima. Berikan kontrak bertipe untuk pemicu itu (`infer_payload_contract` menyusun draf kontrak dari contoh payload, `set_trigger_contract` menyimpannya), lalu ambil kontraknya dengan `get_trigger_contract` dalam format `ts` atau `zod` agar backend Anda dan Brew sepakat tentang bentuk payload.

Setelah itu, `save_automation` membangun pemicu beserta langkah Kirim email dengan email, domain, dan subjeknya. Otomatisasi ini tetap berupa draf sampai Anda memublikasikannya. `dryRun` memvalidasi diagramnya, `test_automation` mengirim salinan sungguhan ke alamat Anda, dan `check_trigger` memastikan sudah ada otomatisasi terpublikasi yang siap menerima pemicu sebelum kode produksi menjalankan apa pun.

### 5. Ganti titik pemanggilan

Inilah satu-satunya perubahan pada kode aplikasi Anda. Sebelumnya, dengan 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} />,
})
```

Sesudahnya, dengan 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` tetap resolve meskipun permintaannya ditolak, jadi periksa `res.ok` dan biarkan job runner Anda mencoba ulang atau mengirim peringatan. Endpoint ini mengembalikan `202` saat menerima peristiwa dan `422` jika tidak ada otomatisasi terpublikasi yang mendengarkan pemicu tersebut.

Susun `Idempotency-Key` dari ID yang stabil, seperti ID pengguna atau pesanan. Kunci yang berulang akan mengembalikan eksekusi aslinya alih-alih mengirim dua kali, dan memakai ulang kunci dengan isi permintaan yang berbeda akan menghasilkan `409 IDEMPOTENCY_CONFLICT`. Brew menyimpan kunci setiap pemicuan selama tanda terima pemicunya disimpan, yaitu 90 hari, bukan 24 jam seperti yang dipakai banyak API. Anggap kunci bersifat permanen, satu kunci untuk satu tindakan. Kunci berbasis stempel waktu berubah pada setiap percobaan ulang, dan kunci yang dipakai ulang untuk tindakan lain beberapa minggu kemudian bisa diperlakukan sebagai pengulangan alih-alih dikirim.

Kode produksi memanggil endpoint ini dengan kunci API, bukan lewat MCP. Lewat OAuth, MCP meminta seseorang mengonfirmasi pengiriman nyata, pemicuan, dan publikasi: tepat untuk chat, keliru untuk request handler. [Panduan MCP atau REST](/id/blog/mcp-vs-rest-api-for-email-marketing-which-workflow-should-you-use) membahas pembagian ini.

### 6. Alihkan satu email dalam satu waktu

1. Letakkan panggilan baru di balik feature flag, berdampingan dengan panggilan lama, sehingga kembali ke cara lama cukup dengan mengubah konfigurasi.
2. Mulailah dengan email berisiko rendah, seperti email pengingat siklus hidup, lalu pantau penerimaan dan klik selama beberapa hari.
3. Pindahkan rangkaian berikutnya, setelah pemicunya berjalan dengan payload yang benar.
4. Pindahkan email transaksional paling akhir. Pengguna paling cepat menyadari reset kata sandi dan magic link yang rusak.
5. Hapus jalur lama setelah satu minggu berjalan tanpa masalah, lalu perbarui `EMAILS.md`.

[Panduan migrasi dari Loops](https://docs.brew.new/migrations/loops) memakai pola yang sama.

### Yang sebaiknya tetap di kode

Email konfirmasi dan magic link Supabase tetap dikirim dari Supabase. Integrasi Supabase di Brew mendengarkan peristiwa pengguna untuk memulai otomatisasi, bukan menggantikan email dari Supabase Auth. Invoice terperinci yang disusun dari data kompleks saat pengiriman mungkin juga lebih sederhana jika tetap di kode, jadi periksa dulu apa yang dibutuhkan merge tag Anda. Simpan salinan hasil impornya di kanvas agar email-email ini tetap terlihat.

## Langkah 3: Optimasi

**Audit seluruh email.** Jalankan `audit_email` untuk satu grup, perbaiki, lalu jalankan ulang sebelum pengiriman besar.

**Periksa di klien email sungguhan.** `test_email_rendering` mengembalikan tangkapan layar dari klien email dan perangkat tertentu, termasuk mode gelap, dengan biaya 10 kredit per eksekusi. `create_inbox_placement_test` mengirim salinan sungguhan ke kotak masuk pengujian dan melaporkan ke mana email itu masuk, juga dengan biaya 10 kredit.

**Buat semua email konsisten.** Perbaiki ketidakkonsistenan di editor, atau minta agen menerapkan satu perubahan, seperti footer baru, ke seluruh grup dengan `edit_email`.

**Baca hasilnya.** `get_email_analytics` melaporkan pengiriman dan keterlibatan. `list_insights` mengembalikan temuan yang dideteksi Brew di akun Anda, masing-masing dengan metrik yang mendasarinya.

**Uji varian secara terencana.** Buat varian di kanvas dan arahkan kontak dengan langkah Percabangan berdasarkan perilaku, misalnya apakah mereka mengklik. Brew tidak memilihkan pemenang dari pembagian acak untuk Anda, jadi bandingkan variannya sendiri.

**Pastikan email baru tetap terlihat.** Tambahkan satu baris ke `AGENTS.md` atau file serupa: email produk baru dibuat di Brew, dikirim lewat pemicu, dan ditambahkan ke `EMAILS.md` dalam pull request yang sama.

## Pertanyaan yang sering diajukan

### Bisakah Brew mengimpor templat React Email?

Bisa. `import_email` menerima HTML hasil render, MJML, pesan `.eml` yang tersimpan, atau modul React Email. HTML hasil render adalah yang paling andal, karena modul dibaca secara statis: props diisi dengan nilai default atau nilai pratinjaunya, dan komponen yang diimpor dari file lokal lain tidak disertakan.

### Apakah Brew terhubung ke akun Resend saya?

Tidak. Tidak ada integrasi, impor, atau ekspor untuk Resend. Anda mengimpor HTML yang dirender kode Anda, dan jika Anda bermigrasi, kode Anda menjalankan pemicu Brew alih-alih memanggil Resend.

### Apakah backend saya sebaiknya memanggil Brew lewat MCP?

Tidak. Gunakan MCP dari agen coding untuk penyiapan, impor, audit, dan pengujian. Kode produksi sebaiknya memanggil REST API dengan kunci API, karena pada koneksi OAuth, MCP meminta seseorang mengonfirmasi pengiriman nyata dan pemicuan.

### Apakah mengimpor email mengubah tampilannya?

Impor mengubah email menjadi desain Brew yang bisa diedit dan tetap menyimpan HTML sumber aslinya untuk pengiriman. Impor menghapus skrip, iframe, formulir, dan event handler, menetralkan piksel pelacakan yang sudah ada, dan meng-hosting ulang gambar publik. Tinjau setiap email di kanvas, dalam tampilan desktop dan seluler, sebelum mengirimnya dari Brew.
