คู่มือสำหรับทีมที่อีเมลอยู่ในโค้ด: ทำบัญชีรายการอีเมลด้วยเอเจนต์เขียนโค้ด นำมาไว้บนแคนวาสเดียวใน Brew ผ่าน MCP แล้วย้ายระบบและปรับปรุงอย่างปลอดภัย

อีเมลในโค้ดคืออีเมลของผลิตภัณฑ์ที่กำหนดไว้ใน codebase และปล่อยออกไปเหมือนฟีเจอร์อื่น ๆ ลองถามทีมที่ใช้ Resend และ React Email ว่าผลิตภัณฑ์ส่งอีเมลอะไรบ้าง แล้วคุณจะเจอความเงียบไปครู่หนึ่ง อีเมลต้อนรับเป็นคอมโพเนนต์ในโฟลเดอร์ emails/ อีเมลรีเซ็ตรหัสผ่านอยู่ในแดชบอร์ดของผู้ให้บริการระบบยืนยันตัวตน อีเมลแจ้งช่วงทดลองใช้ใกล้สิ้นสุดเป็นสตริงใน cron job ใบเสร็จส่งโดย Stripe และไม่มีใครจำได้ว่าอะไรเป็นตัวส่งอีเมล “เราคิดถึงคุณ”
อีเมลแต่ละฉบับปล่อยออกไปได้ง่าย จึงไม่มีใครดูแลภาพรวมทั้งหมด เราจะแก้ปัญหานี้ตามลำดับ คือทำให้มองเห็นก่อน แล้วจึงย้ายระบบ จากนั้นจึงปรับปรุง เอเจนต์เขียนโค้ดที่คุณใช้อยู่แล้วทำงานส่วนใหญ่นี้ได้ เมื่อเชื่อมต่อกับ Brew ผ่าน MCP
import_email ได้ การนำเข้าเป็นแบบเชิงกำหนด (deterministic) ไม่ใช้ AI และไม่มีค่าใช้จ่ายReact Email และ Resend ทำให้งานอีเมลดีขึ้นมากสำหรับนักพัฒนา ทั้งคอมโพเนนต์แบบมีไทป์และการตรวจทานผ่าน pull request แต่ราคาที่ต้องจ่ายจะปรากฏทีหลัง
อีเมลกระจัดกระจาย: มีทั้งคอมโพเนนต์, HTML แบบอินไลน์ในงานเบื้องหลัง, เทมเพลตในแดชบอร์ดของบริการภายนอก และอีเมลที่บริการอื่นส่งแทนคุณ ไม่มีไฟล์ไหนรวบรวมอีเมลไว้ครบทั้งหมด
ตัวอย่างไม่ใช่ของจริง: เซิร์ฟเวอร์พรีวิวของ React Email แสดงเทมเพลตทีละตัวด้วย props ตัวอย่าง ไม่ใช่สิ่งที่ลูกค้าได้รับจริง และไม่แสดงอะไรเลยสำหรับเทมเพลตอื่น
มีแต่วิศวกรที่ดูได้: ทีมผลิตภัณฑ์ ทีมออกแบบ และฝ่ายสนับสนุนต้องขอให้วิศวกรรันโปรเจกต์ให้ดู ส่วนใหญ่จึงไม่มีใครขอ
ไม่มีใครตรวจทานหลังเปิดใช้งาน: อีเมลที่ใช้มา 2 ปียังมีโลโก้เก่าและแพ็กเกจที่คุณเลิกขายไปแล้ว ไม่มีใครสังเกตเห็น เพราะไม่มีใครเห็นอีเมลทั้งหมดพร้อมกัน
เป้าหมายคือมุมมองเดียวที่ทุกคนเห็นอีเมลทุกฉบับร่วมกัน ขั้นตอนนี้ไม่แตะต้องระบบที่ใช้งานจริงเลย

แต่ละระยะมีประโยชน์ในตัวเอง และคุณจะหยุดหลังระยะใดก็ได้
เซิร์ฟเวอร์ MCP ของ Brew อยู่ที่ https://brew.new/api/mcp และใช้ OAuth โดยคุณเข้าสู่ระบบด้วย Brew และเลือกแบรนด์ที่เอเจนต์ทำงานได้ ใน Claude Code:
claude mcp add --transport http brew "https://brew.new/api/mcp"การเพิ่มเซิร์ฟเวอร์เป็นเพียงการบันทึกการกำหนดค่าในเครื่องสำหรับโปรเจกต์นี้ ใส่ --scope user หากต้องการใช้ในทุกโปรเจกต์ ทั้งนี้ ก่อนใช้เครื่องมือของ Brew ให้รัน claude mcp login brew หรือเปิด /mcp แล้วยืนยันตัวตนกับ Brew
เอกสารการตั้งค่าไคลเอนต์มีวิธีตั้งค่าสำหรับ Cursor และ Codex ด้วย ไคลเอนต์ที่ไม่รองรับ OAuth ใช้คีย์ API ระดับแบรนด์ได้ จากนั้นให้เอเจนต์เรียก get_brew_capabilities เพื่ออ่านรายการเครื่องมือที่ใช้งานได้ในปัจจุบันแทนการเดา
ขอให้เอเจนต์ทำรายการให้:
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.เก็บส่วน “Unknown” ไว้ อีเมลยืนยันจากระบบยืนยันตัวตนและใบเสร็จของ Stripe ไม่ได้ถูกส่งจากโค้ดของคุณ แต่ก็ควรอยู่ในรายการด้วย จากนั้น commit ไฟล์ EMAILS.md ต่อให้หยุดแค่ตรงนี้ คุณก็ยังได้รายการที่เมื่อวานยังไม่มี
อินพุตที่ดีที่สุดสำหรับการนำเข้าคือ HTML ที่โค้ดของคุณสร้างอยู่แล้ว ยูทิลิตี render ของ React Email ให้สตริงนั้นมา:
import { render } from 'react-email'
import WelcomeEmail from './emails/welcome'
const html = await render(
<WelcomeEmail firstName="Ana" planName="Pro" trialDays={14} />
)โปรเจกต์รุ่นเก่านำเข้า render จาก @react-email/render ให้ใช้ fixture ที่สมจริง ไม่ใช่ "test" เพราะทีมของคุณจะตรวจทานอีเมลเหล่านี้ และให้เอเจนต์เขียนสคริปต์แสดงผลทุกเทมเพลตออกมาเป็นไฟล์
Brew ยังนำเข้าโมดูล React Email ได้โดยตรงด้วย format: "jsx" โดยอ่านโค้ดแบบสแตติกโดยไม่รันโค้ดนั้น ค่า props จะมาจากค่าเริ่มต้นหรือ PreviewProps ส่วนคอมโพเนนต์ที่นำเข้าจากไฟล์อื่นในโปรเจกต์จะถูกตัดทิ้งหรือถูกปฏิเสธ และ await หรือไวยากรณ์ทำนองเดียวกันจะไม่ได้รับอนุญาต หากเกินกว่าไฟล์เดียวที่สมบูรณ์ในตัว ให้ใช้ HTML ที่แสดงผลแล้ว
จากนั้นนำเข้าไฟล์ที่แสดงผลแล้วแต่ละไฟล์ด้วย import_email โดยแยก groupName ตามส่วนของผลิตภัณฑ์:
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.นักพัฒนามักถามว่าการนำเข้าจะเขียนอีเมลของตนใหม่หรือไม่ ซึ่งเป็นคำถามที่สมเหตุสมผล คำตอบคือไม่:
assetReport จะแสดงรายการรูปที่ดึงมาไม่ได้baseUrl ใช้แค่แปลงพาธรูปภาพแบบสัมพัทธ์แต่ละกลุ่มจะได้แคนวาสของตัวเอง เปิด brew.new/emails/onboarding แล้วคุณจะเห็นลำดับอีเมลทั้งชุดวางเรียงกัน พร้อมสวิตช์สลับมุมมองเดสก์ท็อปกับมือถือ และความคิดเห็นในแต่ละดีไซน์ ลิงก์แคนวาสใช้ได้กับคนที่เข้าสู่ระบบเวิร์กสเปซ Brew ของคุณ และไม่มีลิงก์แชร์แบบสาธารณะ จึงต้องเชิญผู้ตรวจทานเข้ามา
ตรงนี้เองที่ปัญหาจะโผล่ขึ้นมา เช่น โลโก้ 2 แบบ ลิงก์ปุ่มที่เสีย หรือส่วนท้าย 3 แบบ ให้เรียกใช้ audit_email ก่อนแก้ไขอะไร เครื่องมือนี้ตรวจลิงก์ รูปภาพ การปฏิบัติตามข้อกำหนด ขนาดเมื่อโหลด การรองรับของแอปอีเมล การเข้าถึงได้ และข้อความ ใช้ 5 เครดิตต่อการตรวจคุณภาพแบบสมบูรณ์ 1 ครั้ง และรับได้ทั้ง emailId ที่บันทึกไว้หรือ HTML ดิบ คุณจึงตรวจคุณภาพอีเมลที่ยังไม่ได้นำเข้าได้ด้วย เช็กลิสต์ตรวจคุณภาพก่อนส่งของเราอธิบายแต่ละจุดตรวจและเกณฑ์ไว้
ตอนนี้คุณมีบัญชีรายการอีเมล อีเมลทุกฉบับอยู่บนแคนวาส และมีรายการปัญหาแล้ว ขณะที่โค้ดของคุณยังส่งทุกอย่างเหมือนเดิม
ถึงตอนนี้ Brew จะส่งอีเมลบางฉบับแทนโค้ดของคุณ ไม่จำเป็นต้องย้ายอีเมลทุกฉบับ และอีเมลแต่ละประเภทควรถูกส่งจากระบบเดียวในแต่ละช่วงเวลา
| ในโค้ดของคุณตอนนี้ | ใน Brew |
|---|---|
resend.emails.send({ react: <WelcomeEmail /> }) |
เหตุการณ์ทริกเกอร์ คู่กับระบบอัตโนมัติที่เผยแพร่แล้วซึ่งมีขั้นตอนส่งอีเมล 1 ขั้น |
| props ที่ส่งให้คอมโพเนนต์ | ฟิลด์ในเพย์โหลดของทริกเกอร์ ซึ่งใช้เป็นแท็กตัวแปร เช่น {{ firstName | there }} หรือ {{ trigger.planName }} |
โดเมนใน from |
โดเมนผู้ส่งที่ยืนยันแล้วใน Brew |
| ตารางผู้ใช้ของคุณ | ผู้ติดต่อ ซึ่งสร้างผ่าน REST API หรือการเชื่อมต่อ |
| cron job ที่ส่งลำดับอีเมล | ระบบอัตโนมัติที่มีขั้นตอนรอ ตัวกรอง และขั้นตอนส่งอีเมล |
เอเจนต์เรียก create_domain เพื่อรับระเบียน DNS แล้วเรียก verify_domain เมื่อระเบียนมีผลแล้ว โดเมนที่สร้างผ่าน MCP และ API จะเป็นโดเมนการตลาด สำหรับอีเมลรีเซ็ตรหัสผ่านและใบเสร็จ ให้ตั้งค่าโดเมนที่ตั้งวัตถุประสงค์เป็นธุรกรรมในแอป Brew โดเมนแบบนี้จะไม่ใส่ส่วนท้ายสำหรับยกเลิกการรับข่าวสาร และส่งถึงผู้ติดต่อที่ยกเลิกการรับข่าวสารแล้วด้วย การยืนยันโดเมนผู้ส่งอธิบายระเบียนต่าง ๆ ไว้
MCP อัปเดตผู้ติดต่อได้แต่สร้างใหม่ไม่ได้ ให้สร้างผู้ติดต่อจากแบ็กเอนด์ด้วย POST /v1/contacts ซึ่งเพิ่มหรืออัปเดต (upsert) ผู้ติดต่อได้ทีละ 1 รายหรือเป็นชุดสูงสุด 1,000 ราย และสร้างฟิลด์ที่กำหนดเองให้อัตโนมัติ หรืออัปโหลดไฟล์ CSV ในแอป การเรียกทริกเกอร์ก็สร้างหรืออัปเดตผู้ติดต่อที่อยู่ในเพย์โหลดด้วย นำเข้าเฉพาะคนที่ยินยอมรับข่าวสารจากคุณเท่านั้น การเพิ่มผู้ติดต่ออธิบายว่า Brew ต้องการข้อมูลแบบใด
จุดที่พลาดได้ง่าย: อีเมลต้อนรับที่นำเข้ามาจะมีคำว่า “Hi Ana” อยู่ตรงตัว ให้แทนค่าจาก fixture แต่ละค่าด้วยแท็กตัวแปร เช่น {{ firstName | there }} โดยข้อความหลังขีดตั้งคือค่าสำรอง ใช้ตัวแก้ไขของ Brew หรือให้เอเจนต์เรียกใช้ edit_email โดยจำกัดให้แก้แค่การแทนค่าเหล่านั้น การแก้ไขนี้เป็นการแก้ไขด้วย AI ที่ใช้เครดิต จึงควรตรวจตัวอย่างอีกครั้ง แท็กตัวแปรแสดงไวยากรณ์ทั้งหมด
เอเจนต์สร้างทริกเกอร์ด้วย save_trigger โดยสคีมาเพย์โหลดต้องมี email ของผู้รับ กำหนดสคีมาเพย์โหลดแบบมีไทป์ให้ทริกเกอร์ (infer_payload_contract ร่างสคีมาจากเพย์โหลดตัวอย่าง และ set_trigger_contract บันทึกสคีมานั้น) แล้วดึงสคีมาด้วย get_trigger_contract ในรูปแบบ ts หรือ zod เพื่อให้แบ็กเอนด์ของคุณกับ Brew ใช้รูปแบบเพย์โหลดตรงกัน
จากนั้น save_automation จะสร้างทริกเกอร์พร้อมขั้นตอนส่งอีเมลที่ระบุอีเมล โดเมน และหัวเรื่อง ระบบอัตโนมัติจะยังเป็นฉบับร่างจนกว่าคุณจะเผยแพร่ ส่วน dryRun ใช้ตรวจสอบความถูกต้องของกราฟระบบอัตโนมัติ, test_automation ส่งสำเนาจริงไปยังที่อยู่อีเมลของคุณ และ check_trigger ยืนยันว่ามีระบบอัตโนมัติที่เผยแพร่แล้วรอรับทริกเกอร์อยู่ ก่อนที่ระบบที่ใช้งานจริงจะเรียกทริกเกอร์ใด ๆ
นี่เป็นการเปลี่ยนแปลงเดียวในโค้ดแอปพลิเคชันของคุณ ก่อนหน้านี้ เมื่อใช้ Resend:
await resend.emails.send({
from: 'Acme <hello@acme.com>',
to: user.email,
subject: 'Welcome to Acme',
react: <WelcomeEmail firstName={user.firstName} planName={plan.name} />,
})หลังเปลี่ยน เมื่อใช้ Brew:
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 จะ resolve แม้คำขอถูกปฏิเสธ จึงต้องตรวจ res.ok และให้ตัวรันงาน (job runner) ลองใหม่หรือแจ้งเตือน เอนด์พอยต์จะตอบ 202 เมื่อรับเหตุการณ์ และตอบ 422 หากไม่มีระบบอัตโนมัติที่เผยแพร่แล้วรอรับทริกเกอร์นั้น
สร้าง Idempotency-Key จาก ID ที่คงที่ เช่น ID ผู้ใช้หรือ ID คำสั่งซื้อ คีย์ที่ซ้ำจะได้รอบการทำงานเดิมกลับมาแทนการส่งซ้ำ และการใช้คีย์เดิมกับเนื้อหาคำขอที่ต่างไปจะได้ 409 IDEMPOTENCY_CONFLICT ทั้งนี้ Brew เก็บคีย์ของการเรียกทริกเกอร์แต่ละครั้งไว้นานเท่ากับบันทึกการรับทริกเกอร์ ซึ่งคือ 90 วัน ไม่ใช่ 24 ชั่วโมงอย่างที่ API จำนวนมากใช้ ให้ถือว่าคีย์เป็นของถาวร ใช้ 1 คีย์ต่อ 1 การกระทำ คีย์ที่สร้างจาก timestamp จะเปลี่ยนทุกครั้งที่ลองใหม่ และคีย์ที่นำกลับมาใช้กับการกระทำอื่นในอีกหลายสัปดาห์ต่อมาอาจกลายเป็นการเรียกซ้ำของครั้งก่อนแทนที่จะส่งอีเมลจริง
โค้ดที่ใช้งานจริงควรเรียกเอนด์พอยต์นี้ด้วยคีย์ API ไม่ใช่ผ่าน MCP เพราะเมื่อเชื่อมต่อผ่าน OAuth แล้ว MCP จะขอให้คนยืนยันก่อนการส่งจริง การเรียกทริกเกอร์ และการเผยแพร่ ซึ่งเหมาะกับการแชท แต่ไม่เหมาะกับ request handler คู่มือ MCP กับ REST อธิบายการแบ่งงานนี้
EMAILS.mdคู่มือการย้ายจาก Loops ก็ใช้รูปแบบเดียวกัน
อีเมลยืนยันและอีเมล Magic Link ของ Supabase ยังคงส่งจาก Supabase ต่อไป การเชื่อมต่อ Supabase ของ Brew รอรับเหตุการณ์ของผู้ใช้เพื่อเริ่มระบบอัตโนมัติ ไม่ได้มาแทนที่อีเมลของ Supabase Auth ส่วนใบแจ้งหนี้แบบแจกแจงรายการที่สร้างจากข้อมูลซับซ้อนตอนส่งก็อาจทำในโค้ดได้ง่ายกว่า จึงควรตรวจก่อนว่าแท็กตัวแปรของคุณต้องใช้อะไรบ้าง และเก็บสำเนาที่นำเข้าไว้บนแคนวาส เพื่อให้อีเมลเหล่านี้ยังมองเห็นได้
ตรวจคุณภาพอีเมลทั้งชุด: เรียกใช้ audit_email กับทั้งกลุ่ม แก้ไข แล้วเรียกใช้อีกครั้งก่อนการส่งขนาดใหญ่
ตรวจในแอปอีเมลจริง: test_email_rendering ส่งภาพหน้าจอจากแอปอีเมลและอุปกรณ์ที่ระบุกลับมา รวมถึงโหมดมืด โดยใช้ 10 เครดิตต่อการรัน 1 ครั้ง ส่วน create_inbox_placement_test ส่งสำเนาจริงไปยังอินบ็อกซ์ทดสอบและรายงานว่าอีเมลไปตกที่ใด โดยใช้ 10 เครดิตเช่นกัน
ทำให้อีเมลทั้งชุดสอดคล้องกัน: แก้จุดที่ไม่สอดคล้องกันในตัวแก้ไข หรือให้เอเจนต์ใช้ edit_email ปรับการเปลี่ยนแปลงเดียวกัน เช่น ส่วนท้ายแบบใหม่ กับทั้งกลุ่ม
อ่านผลลัพธ์: get_email_analytics รายงานการส่งและการมีส่วนร่วม ส่วน list_insights แสดงสิ่งที่ Brew ตรวจพบในบัญชีของคุณ พร้อมตัวชี้วัดเบื้องหลังของแต่ละรายการ
ทดสอบแต่ละแบบอย่างตั้งใจ: สร้างอีเมลหลายแบบบนแคนวาส แล้วส่งผู้ติดต่อไปตามเส้นทางด้วยขั้นตอนแยกเส้นทางตามพฤติกรรม เช่น เคยคลิกหรือไม่ Brew ไม่ได้เลือกผู้ชนะจากการแยกแบบสุ่มให้คุณ คุณจึงต้องเปรียบเทียบแต่ละแบบเอง
ทำให้อีเมลใหม่ยังมองเห็นได้: เพิ่มบรรทัดหนึ่งใน AGENTS.md หรือไฟล์ทำนองเดียวกันว่า อีเมลผลิตภัณฑ์ฉบับใหม่ให้สร้างใน Brew เรียกส่งด้วยทริกเกอร์ และเพิ่มลงใน EMAILS.md ใน pull request เดียวกัน
ได้ โดย import_email รับ HTML ที่แสดงผลแล้ว, MJML, ข้อความ .eml ที่บันทึกไว้ หรือโมดูล React Email ทั้งนี้ HTML ที่แสดงผลแล้วเชื่อถือได้มากที่สุด เพราะโมดูลจะถูกอ่านแบบสแตติก ค่า props จะกลายเป็นค่าเริ่มต้นหรือค่าตัวอย่าง และคอมโพเนนต์ที่นำเข้าจากไฟล์อื่นในโปรเจกต์จะถูกตัดออก
ไม่ได้ เพราะไม่มีการเชื่อมต่อ การนำเข้า หรือการส่งออกกับ Resend คุณนำเข้า HTML ที่โค้ดของคุณแสดงผล และหากย้ายระบบ โค้ดของคุณจะเรียกทริกเกอร์ของ Brew แทนการเรียก Resend
ไม่ควร ให้ใช้ MCP จากเอเจนต์เขียนโค้ดสำหรับการตั้งค่า การนำเข้า การตรวจคุณภาพ และการทดสอบ ส่วนโค้ดที่ใช้งานจริงควรเรียก REST API ด้วยคีย์ API เพราะในการเชื่อมต่อผ่าน OAuth นั้น MCP จะขอให้คนยืนยันก่อนการส่งจริงและการเรียกทริกเกอร์
การนำเข้าจะแปลงอีเมลเป็นดีไซน์ใน Brew ที่แก้ไขได้ และเก็บ HTML ต้นฉบับไว้ใช้ส่งจริง โดยจะตัดสคริปต์, iframe, ฟอร์ม และตัวจัดการเหตุการณ์ออก ทำให้พิกเซลติดตามที่มีอยู่เดิมใช้งานไม่ได้ และโฮสต์รูปภาพสาธารณะใหม่ ตรวจทานแต่ละฉบับบนแคนวาสทั้งในมุมมองเดสก์ท็อปและมือถือก่อนส่งจาก Brew
