Pre-launch apps share one Resend account and one domain, apps.timschoch.com. This skill holds our conventions only. Code and commands beyond the templates come from the vendor skills: resend (SDK), resend-cli (every resend command), agent-email-inbox (when inbound mail triggers actions).
Conventions
- Record:
resend.jsonat the repo root, committed. Read it first and reuse what it records. - Default address:
<repo>@apps.timschoch.com.<repo>is the repository name from the git remote, lowercased,a-z0-9-only. - More addresses:
<name>.<repo>@apps.timschoch.com, one per concrete reason, the reason written inpurpose. - API key: one per repo, named
<repo>. - Secrets:
RESEND_API_KEYandRESEND_WEBHOOK_SECRET, in.env.localand the hosting env.resend.jsonholds ids, never secrets. - Admin key: creating keys and webhooks needs a full-access key. Ask the user to export it in the shell for this session. It stays out of the repo and out of
.env.local.
Steps
- Check the shared domain:
resend domains listshowsapps.timschoch.comverified, sending and receiving on. Missing or unverified: stop and tell the user. The domain is set up once by hand, for all repos. - Copy templates/resend.json to the repo root. Fill in the addresses. Set
"inbound": trueonly on an address the app must react to. - Create the API key, then record its name, id and permission in
resend.json:- Send only:
sending_access, scoped to the domain id ofapps.timschoch.com. - Any inbound address:
full_access. Resend has no narrower permission that reads received mail. Tell the user this key can manage the whole account, for every repo.
- Send only:
- Copy templates/mail.ts into the repo's lib folder and fix the import path to
resend.json. All mail goes throughsendMail(); pick the sender by address name:sendMail({ from: 'notifications', ... }). - Inbound only:
-
Copy templates/inbound.ts next to
mail.ts. Put the app's logic inhandleMail(). -
Add a POST route at
/api/mail/inboundthat hands the request tohandleInboundRequest:// Next.js: app/api/mail/inbound/route.ts export { handleInboundRequest as POST } from '@/lib/inbound';// TanStack Start: src/routes/api/mail/inbound.ts export const Route = createFileRoute('/api/mail/inbound')({ server: { handlers: { POST: ({ request }) => handleInboundRequest(request) } }, }); -
Create a webhook for
email.receivedon the production URL. Preview URLs change. Save its signing secret asRESEND_WEBHOOK_SECRETand record id and endpoint inresend.json.
-
- Prove it: send one mail from the default address to the user and confirm it arrives. Inbound: mail the inbound address and see
handleMail()run; mail an address of another repo and seeignoredwith status 200.
Shared account
- Webhooks are account-wide. Every
email.receivedwebhook gets the inbound mail of every repo on the domain.inbound.tsdrops mail for other repos with 200. Keep that check. - Quota: the free plan's 3,000 mails per month and 100 per day count across all repos. On a quota error, tell the user.
- Retention: received mail stays 30 days on the free plan. Store what the app needs in its own database.
Launch
The shared domain is for pre-launch only. At launch:
- Add the app's own domain in Resend.
- Update
domainand every address inresend.json. - Create a new key for the new domain, swap
RESEND_API_KEY, delete the old key.