From hypervibe
Add transactional email support (Resend or Brevo) to an existing T3 project. The skill auto-detects which provider to use based on env vars - no question asked when the choice is unambiguous. Can be called by /bootstrap or standalone.
How this skill is triggered — by the user, by Claude, or both
Slash command
/hypervibe:add-emailThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Adds transactional email support to the current project. **Supports two providers** (Resend and Brevo) with a single unified flow: the skill decides which one to install based on the user's existing API keys, without asking when the choice is unambiguous.
Adds transactional email support to the current project. Supports two providers (Resend and Brevo) with a single unified flow: the skill decides which one to install based on the user's existing API keys, without asking when the choice is unambiguous.
The deterministic core (SDK install, mail.ts + contact tRPC router scaffolding, root.ts patching, env var push) is handled by scripts/setup-email.mjs --provider <resend|brevo>. This SKILL takes care of: provider selection, prereqs validation, re-config detection, post-install steps (domain config, contact page, RGPD update), and the final summary.
This skill reads the email key (Resend/Brevo) from the vault, so first make sure it is unlocked (follow _ensure-vault): node "${CLAUDE_SKILL_DIR}/../../scripts/vault/vault.mjs" status then if locked/expired, run launch.mjs unlock (window, once a day); if the vault does not exist yet, delegate to _add-keyring.
First of all, invoke _check-deps email to detect the project state:
result=$(node "${CLAUDE_SKILL_DIR}/../../scripts/check-deps.mjs" email)
email_ok=$(echo "$result" | node -e "console.log(JSON.parse(require('fs').readFileSync(0,'utf8')).email.ok)")
email_provider=$(echo "$result" | node -e "console.log(JSON.parse(require('fs').readFileSync(0,'utf8')).email.provider || 'none')")
email_ok = true then silent re-configuration mode (Resend or Brevo)The project already has a provider installed ($email_provider is in {resend, brevo}). Stay on that provider (no question, no automatic switch). Do NOT run setup-email.mjs (the script refuses to overwrite existing mail.ts + contact.ts).
Show a menu tailored to the provider:
📬 ${email_provider === "resend" ? "Resend" : "Brevo"} is already set up on your project
What do you want to do?
- Change the sending address (e.g. switch to
[email protected])- Change the address where you receive messages from the contact form
- Create the
/contactpage if it does not exist yet- Start over from scratch (if the config is broken - first delete the current keys from
.envAND the filessrc/server/mail.ts+src/server/api/routers/contact.ts)- Something else - tell me what you want
Wait for the answer.
Depending on the answer:
| Choice | Resend action | Brevo action |
|---|---|---|
| 1 (change sender) | Jump to Step 6 (domain config via _dns-resend). | Ask for the new BREVO_SENDER_EMAIL. If custom domain, invoke _dns-brevo. Then _push-env-vars BREVO_SENDER_EMAIL=<email> (and BREVO_SENDER_NAME if changed). Reminder: the sender must be verified in the Brevo dashboard (Settings then Senders & IPs). |
| 2 (change recipient) | Invoke _create-contact-page in "update recipient only" mode - run just its Step 2 (CONTACT_RECIPIENT_EMAIL) and skip creation if it already exists. | |
| 3 (create contact page) | Invoke _create-contact-page directly. | |
| 4 (start over) | For Resend: delete RESEND_API_KEY from .env + prompt to revoke it in the Resend dashboard. For Brevo: delete BREVO_API_KEY from .env. In both cases: delete src/server/mail.ts + src/server/api/routers/contact.ts. Then go back to the normal Step 1. | |
| 5 (other) | Ask for clarification. Do not run the install flow by default. |
At the end, jump straight to Step 9 (summary).
email_ok = false then fresh install, continue to Step 1Read the user-scope API keys:
HAS_RESEND_KEY=$(node "${CLAUDE_SKILL_DIR}/../../scripts/_read-user-env.mjs" RESEND_API_KEY 2>/dev/null | grep -c . || echo 0)
HAS_BREVO_KEY=$(node "${CLAUDE_SKILL_DIR}/../../scripts/_read-user-env.mjs" BREVO_API_KEY 2>/dev/null | grep -c . || echo 0)
Apply the decision rule (NO question to the user):
| Available user keys | Chosen provider | Flag in the final summary? |
|---|---|---|
| None | Resend | ✅ "I installed Resend by default. Create an API key and I will store it in your vault (item RESEND) to finish." |
| Resend only | Resend | no (silent) |
| Brevo only | Brevo | no (silent) |
| Both | Brevo | ✅ "You have both keys. I went with Brevo. To switch back to Resend later: delete your Brevo config and re-run /add-email." |
Store CHOSEN_PROVIDER (in {resend, brevo}) and CHOICE_NOTE (string or empty) for the following Steps.
Show a short message to the user, for example:
↳ Provider detected: Brevo (Brevo key detected) ✅
Or for the Resend default with no key:
↳ No email key detected - I am installing Resend by default. You will finish with the key afterwards. ✅
Invoke _detect-project-root to get PROJECT_NAME, WEB_DIR, IS_NEXTJS. Abort if IS_NEXTJS=no.
CHOSEN_PROVIDER = resendNo more Resend CLI. Check that the Resend key is in the vault (the app sends via the SDK + RESEND_API_KEY, not via the CLI):
VAULT="${CLAUDE_SKILL_DIR}/../../scripts/vault/vault.mjs"
node "$VAULT" get RESEND api_key >/dev/null 2>&1; RC=$?
RC=0 then key present, move on to Step 3.RC=2/3 (vault locked/expired) then warn the user, node "${CLAUDE_SKILL_DIR}/../../scripts/vault/launch.mjs" unlock (blocking), retry.RC=4 (key missing) then have it created + stored in the vault:
To send emails, I need a Resend key (just once - I store it in your vault).
- Go to https://resend.com/api-keys then Create API Key then Full Access and copy it.
- A window will open: paste it in (masked input).
node "${CLAUDE_SKILL_DIR}/../../scripts/vault/launch.mjs" add --name RESEND --service Resend --fields "api_key:secret"
Then retry the get.⚠️ The script setup-email.mjs --provider resend then reads RESEND.api_key from the vault (non-interactive), which is why the key must be there BEFORE calling it.
CHOSEN_PROVIDER = brevoCheck that the Brevo key is in the vault (the app sends via the SDK + BREVO_API_KEY):
VAULT="${CLAUDE_SKILL_DIR}/../../scripts/vault/vault.mjs"
node "$VAULT" get BREVO api_key >/dev/null 2>&1; RC=$?
RC=0 then key present, move on to Step 3.RC=2/3 (vault locked/expired) then warn the user, node "${CLAUDE_SKILL_DIR}/../../scripts/vault/launch.mjs" unlock (blocking), retry.RC=4 (key missing) then have it created + stored in the vault:
To use Brevo, I need an API key (just once - I store it in your vault).
- Go to https://app.brevo.com/settings/keys/api then Generate a new API key (name
claude-code) and copy it (xkeysib-...).- A window will open: paste it in (masked input).
node "${CLAUDE_SKILL_DIR}/../../scripts/vault/launch.mjs" add --name BREVO --service Brevo --fields "api_key:secret"
Then retry the get.⚠️ The script setup-email.mjs --provider brevo then reads BREVO.api_key from the vault (non-interactive), so the key must be there BEFORE calling it.
For Brevo, the script needs --brevo-sender <email>. By default, ask the user for their desired sending email, OR take the email from the user's Resend / Brevo account (found in their user doc or their .env). For --brevo-sender-name, default = <PROJECT_NAME>.
For Resend, no need - by default the script writes [email protected] (test sender) and we ask at Step 6 whether the user wants a custom domain.
Run the script with the chosen --provider:
node "${CLAUDE_SKILL_DIR}/../../scripts/setup-email.mjs" \
--provider resend \
--name "<PROJECT_NAME>" \
--web-dir "<WEB_DIR>"
node "${CLAUDE_SKILL_DIR}/../../scripts/setup-email.mjs" \
--provider brevo \
--name "<PROJECT_NAME>" \
--brevo-sender "<sender-email>" \
--brevo-sender-name "<PROJECT_NAME>" \
--web-dir "<WEB_DIR>"
The script chains 7 sub-steps: preflight, get/read API key, install SDK, write mail.ts, write contact tRPC router, register contactRouter in root.ts, push env vars.
The script prints live:
▸ <step> when it starts each sub-step✅ <result> at the end of each one⚠️ <warning> for non-blocking warnings (rateLimitedProcedure missing, etc.){"success":true,"provider":"resend","envVars":["RESEND_API_KEY","RESEND_FROM_EMAIL"]}{"success":true,"provider":"brevo","envVars":["BREVO_API_KEY","BREVO_SENDER_EMAIL","BREVO_SENDER_NAME"]}Let the output through live (no > /tmp/..., no capture).
❌ Failed at: <step>). The name maps 1:1 to a function in the script - open setup-email.mjs and read the function to understand.preflight then usually an already existing file (mail.ts or contact.ts) or no Next.js / no tRPC. Handle specifically.getApiKey Resend/Brevo then the key is not in the vault (or the vault is locked). Store the key: node scripts/vault/launch.mjs add --name RESEND --service Resend --fields "api_key:secret" (or --name BREVO --service Brevo), after a launch.mjs unlock if needed. Then re-run.installSdk then a pnpm error (network, registry). Retry by hand: cd <WEB_DIR> && pnpm add <pkg>.writeMailTs / writeContactRouter then FS permission (rare).registerRouter then T3 may have reorganized root.ts. Patch manually: add import { contactRouter } from "~/server/api/routers/contact"; + contact: contactRouter, in the createTRPCRouter({...}).pushEnvVars then all the code is in place, only the env vars did not land. Invoke _push-env-vars manually.Invoke _update-claude-md with the sections tailored to the chosen provider.
stack: - **Email**: Resend (transactional emails via \sendMail()` in `<WEB_DIR>/src/server/mail.ts`)`env-vars:
- \RESEND_API_KEY` - Resend API key`- \RESEND_FROM_EMAIL` - default sender email`- \CONTACT_RECIPIENT_EMAIL` - contact form recipient (optional, falls back to `RESEND_FROM_EMAIL`)`conventions:
- Email: always use \escapeHtml()` from `~/server/mail` on user data before injecting it into an email's HTML.`stack: - **Email**: Brevo SDK v5 (\BrevoClient`) - transactional emails via `sendMail()` in `<WEB_DIR>/src/server/mail.ts``env-vars:
- \BREVO_API_KEY` - Brevo transactional API key`- \BREVO_SENDER_EMAIL` - default sender email`- \BREVO_SENDER_NAME` - default sender name`- \CONTACT_RECIPIENT_EMAIL` - contact form recipient (optional, falls back to `BREVO_SENDER_EMAIL`)`conventions:
- Email: always pipe user data through \escapeForBrevo()` (from `~/server/mail`) before inserting into `htmlContent`. NEVER let a raw `{{` (user input, stack trace, JSON) through into `htmlContent` or `textContent` - Brevo does a silent Mustache-style templating pass that drops the email with no visible error.`Also add a dedicated section about the Brevo quirk (under the heading ## Email - Brevo quirk):
## Email - Brevo quirk
Brevo runs implicit Mustache-style templating on \`htmlContent\` and \`textContent\` at send time. Any \`{{\` in the body (stack traces, malformed JSON, user input) raises an async parse failure - the SDK call already returned 201 by then, so the \`try/catch\` sees nothing and the email is silently dropped.
**Rules:**
- Always pipe user-provided or error-derived strings through \`escapeForBrevo()\` before inserting into \`htmlContent\`.
- For \`textContent\`, at minimum neutralize \`{{\` with \`.replace(/\{\{/g, "{ {")\`.
- Treat \`await sendMail(...)\` as "best effort, not confirmed sent" - for critical flows (password resets, payments), add a Brevo webhook to catch async failures.
Add the provider to the project's RGPD subprocessor registry:
node "${CLAUDE_SKILL_DIR}/../../scripts/update-privacy-policy.mjs" --add <provider>
(where <provider> = resend or brevo depending on CHOSEN_PROVIDER). The helper is idempotent. If the page politique-de-confidentialite/page.tsx exists, it updates automatically.
Note: Brevo (Sendinblue SAS) is EU-resident - no outside-EU transfer mechanism to declare. Resend is in the US - a transfer mechanism (standard contractual clauses) is already documented in the template.
At this point, the scaffolding is in place. What remains is to decide on the final sender.
[email protected], test sender)Show:
✅ The setup is in place - last step: choose the sending email
For now I set the Resend test address (
[email protected]). It works but can only send to your own email (your Resend account's). Great for testing, not for writing to users.If you want to send from your own address (e.g.
[email protected]), I can configure your domain now in Resend (add DNS records + verification, 1-2 min).Three options:
- Leave the test address for now
- Use my domain - tell me the email
- I already have a Resend domain configured - tell me the email to use
Extract the domain. Check it is not already verified (Resend API, key from the vault):
RTOK=$(node "${CLAUDE_SKILL_DIR}/../../scripts/vault/vault.mjs" get RESEND api_key)
curl -s "https://api.resend.com/domains" -H "Authorization: Bearer $RTOK"
If the domain is there with status == "verified" then go to Case 3. Otherwise check Cloudflare:
result=$(node "${CLAUDE_SKILL_DIR}/../../scripts/check-deps.mjs" cloudflare)
cf_ok=$(echo "$result" | node -e "console.log(JSON.parse(require('fs').readFileSync(0,'utf8')).cloudflare.ok)")
If cf_ok = true, check that the domain is in a CF zone:
curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
"https://api.cloudflare.com/client/v4/zones?name=<domain>" \
| node -e "const d = JSON.parse(require('fs').readFileSync(0, 'utf8')); console.log(d.result?.[0]?.id ? 'EXISTS' : 'NOT_FOUND');"
EXISTS then invoke _dns-resend with the domain. That skill creates the domain in Resend, adds SPF/DKIM/MX via the CF API, polls the verification, updates RESEND_FROM_EMAIL.NOT_FOUND then propose /add-domain (which will switch DNS to CF then chain Resend).If cf_ok = false then propose /start to configure Cloudflare, or to leave the default.
Invoke _push-env-vars with RESEND_FROM_EMAIL=<email>.
The Brevo sender was passed to the script at Step 3 and is already in place. If the user wants to change it now:
Ask which sender they want. If custom domain, invoke _dns-brevo with the domain to configure it in Brevo (SPF, DKIM, DMARC) + via the Cloudflare API. Then _push-env-vars BREVO_SENDER_EMAIL=<new>.
⚠️ Manual action Brevo: the sender must be verified in the Brevo dashboard (Settings then Senders & IPs). Remind them of this.
The tRPC back-end is created by the script (the contact.send procedure with honeypot + provider-specific escape + rate limiting). What is missing is the front-end page.
Propose to the user:
📨 And a contact page that works right away?
I set up the engine that sends emails on the server side. If you want, I can create a working
/contactpage now - a Name / Email / Message form, a Send button, and all the anti-spam guards.It takes 30 seconds.
Invoke _create-contact-page. That skill automatically detects the provider via _check-deps email and adapts what it creates.
Skip - mention at Step 9 that they can ask "create me a contact page" later.
Adapt depending on the provider and the situation:
[email protected]✅ Resend is configured. Your emails will go out from
[email protected]- a test sender that can only write to your own email for now. Enough for testing. When you want to send to your users, tell me "configure my domain for email sending".
✅ Resend is configured. Your emails will go out from
<email>. You can write to anyone now.
✅ Brevo is configured (
sendMail()+escapeForBrevo()in<WEB_DIR>/src/server/mail.ts).⚠️ Manual action required: check that
<sender-email>is a verified sender in Brevo (Settings then Senders & IPs). Without it, emails fail silently on the Brevo side.A note on the Brevo templating quirk has been added to CLAUDE.md (
## Email - Brevo quirk) so future Claude sessions do not reintroduce the silent-send bug.
contact.send tRPC procedure, with honeypot + escape + rate limiting)/contact page is ready, you can test it right awayCHOICE_NOTE is non-empty (cf. Step 1)Always add the choice note at the start or the end of the summary. Examples:
RESEND).".env + mail.ts + contact.ts) and re-run /add-email."If any warnings were raised by the script (e.g. rateLimitedProcedure missing), mention them here as well.
npx claudepluginhub flavien-ia/hypervibe-harness --plugin hypervibeOperate the Resend platform from the terminal: send emails (including React Email .tsx templates), manage domains, contacts, broadcasts, and more via the `resend` CLI. Use for shell scripts or CI/CD pipelines.
Covers transactional email, marketing automation, deliverability, and email infrastructure decisions. Helps with email best practices, DNS setup, and scaling.
Integrates email sending into projects with Resend/SendGrid. Covers domain verification (SPF/DKIM/DMARC), HTML templates, rate limiting, testing, and troubleshooting for verification, notifications, or marketing emails.