Reference
Email delivery
Operate domain-scoped sending and signed inbound forwarding without turning Chalupa into a public mail relay.
Reviewed 2026-07-25
Chalupa can send mail from its verified domain and receive mail at
hello@chalupa.run. Incoming messages are forwarded to one private,
operator-controlled destination. The destination exists only in encrypted
runtime configuration; it is never committed, returned by an API, or written
to application logs.
Email is an operational channel, not part of the fleet control protocol. Launching, observing, or sinking an environment never depends on Resend.
Security boundary
POST /api/webhooks/resend is a provider callback, not a public send API. It:
- accepts only
application/jsonbodies up to 64 KiB; - verifies the unmodified body with the endpoint-specific Svix secret;
- handles only signed
email.receivedevents; - requires the signed event's
data.tolist to contain onlyhello@chalupa.run, cross-checks optional envelope-recipient metadata when Resend supplies it, and repeats the exact-recipient check against the authenticated Receiving API response; - ignores other recipients captured by the domain-wide receiving rule;
- blocks messages that would loop through the inbound, forwarding, or private destination addresses;
- makes one authenticated
ReceivingAPI request for the matching event only, with a 10-second timeout, no redirects, no cache, and a 4 MiB streamed JSON limit; - forwards bounded HTML/text with
inbox@chalupa.runas the fixed sender and the validated originalReply-To(falling back toFrom); it deliberately omits attachment bytes and adds a neutral notice when attachments exist; and - keeps 90 days of SHA-256 pseudonymized digests of the Svix and Resend identifiers for replay suppression. The digests remain joinable provider identifiers, so they are not anonymous data; Chalupa stores neither message content, plaintext provider IDs, sender, nor destination. A lease and provider idempotency key reduce duplicates, but email delivery is not an absolute exactly-once protocol.
Resend stores received mail and treats a receiving domain as a catch-all. An ignored local part can therefore still consume provider quota and remain visible in the Resend account. Monitor that quota and use the provider dashboard to recover a message when webhook delivery is unavailable.
Email content is untrusted input. It must never be interpreted as a Chalupa command, passed to an agent, or used to authorize an infrastructure action.
Credential separation
| Secret | Permission | Runtime |
|---|---|---|
RESEND_SEND_API_KEY |
Resend sending_access, restricted to chalupa.run |
Vercel Production server runtime; only the alert cron and the delivery webhook read it. |
RESEND_RECEIVE_API_KEY |
Resend full_access |
Vercel Production server runtime; only the inbound webhook code reads it. |
RESEND_WEBHOOK_SECRET |
The email.received webhook endpoint |
Vercel Production server runtime; only the inbound webhook code reads it. |
RESEND_DELIVERY_WEBHOOK_SECRET |
A separate delivery-events webhook endpoint | Vercel Production server runtime; only the delivery webhook code reads it. |
RESEND_FORWARD_TO |
One private email address | Vercel Production server runtime; only the current webhook code reads it. |
CHALUPA_ALERT_EMAIL_TO |
One private email address; optional | Alert destination. Falls back to RESEND_FORWARD_TO. |
CHALUPA_MONTHLY_BUDGET_USD |
Not a secret | Spend the alert bands are measured against. Defaults to 25. |
Use separate named keys for sending and receiving. Never give a browser, Preview deployment, Compose workload, or droplet either key. A generic route that accepts request-controlled recipients, senders, subjects, or HTML is deliberately absent.
Resend full-access keys cannot be restricted to one domain and can see events for every receiving domain in the team. The route filters signed events for the exact Chalupa address before touching Neon or the Receiving API. A compromise of the Production webhook runtime nevertheless crosses a wider provider boundary than the domain-scoped send key. Vercel environment variables are project-runtime scoped, so any server-side code in the same deployment could read them even though Chalupa currently reads them only in the webhook path. True route-level isolation requires a separate deployment or secret broker. Keep these values out of Preview, use a separate receiving key per product, and audit or revoke each key independently.
Provision the domain
Create the domain with sending and receiving enabled and enforced TLS. Publish the exact SPF, DKIM, MX, and optional tracking records returned for that domain; do not copy example record values from documentation. Add a DMARC policy after SPF and DKIM verify.
The root MX routes every address at chalupa.run through Resend. If the root
domain later gains a normal mailbox provider, move Resend reception to a
dedicated subdomain before changing MX records.
Sending and receiving are verified independently, and a working inbound route
proves nothing about the sending records. As of this revision chalupa.run
publishes all of them: send.chalupa.run carries the SPF TXT and the
feedback-smtp.us-east-1.amazonses.com bounce MX, resend._domainkey carries
the DKIM key at the root so signatures align as d=chalupa.run, and the root
MX points at inbound-smtp.us-east-1.amazonaws.com for receiving. The one gap
is _dmarc.chalupa.run: it is p=none with strict alignment but no rua
address, so nobody is collecting the reports the monitoring stage exists to
produce. Add a report destination before treating the observation window below
as started, and note that strict SPF alignment cannot pass while the envelope
sender lives on send.chalupa.run; alignment rests on DKIM.
Alerts send from the root domain because that is where the domain is already
verified and where the inbound forwarder already sends from, and because the
volume is a handful of operational messages. Move transactional mail to a
dedicated subdomain such as mail.chalupa.run before sending anything
bulk, marketing, or multi-recipient: a subdomain keeps a reputation problem in
one stream instead of spreading it across the root domain that also carries
the operator's own mail.
After the application route is deployed:
- create a webhook for
https://chalupa.run/api/webhooks/resend; - subscribe it only to
email.received; - save the one-time signing secret directly into TinyVault and the Vercel Sensitive Production environment;
- create a separate full-access receiving key and store it the same way;
- store the private forwarding address as
RESEND_FORWARD_TO; - keep the domain-scoped send key in TinyVault unless a reviewed server-side template needs it; and
- redeploy, then inspect one signed inbound delivery and one outbound test.
Do not enable these secrets for Preview. The public landing page, documentation, health route, and local demo remain independent from email configuration.
Outbound alerts
Chalupa mails operational alerts from alerts@chalupa.run with Reply-To: hello@chalupa.run. There is deliberately no no-reply sender: every alert
describes something the operator may want to answer, and hello@chalupa.run is
already forwarded to the private destination, so a reply reaches a human
through machinery that already exists. There is also no contact@, no
billing@, and no ci@; a single-operator control plane gains nothing from
splitting identities that all deliver to the same mailbox. Filtering is
supported by stable subject prefixes ([Chalupa] <label>: …), the
X-Chalupa-Alert-Kind and X-Chalupa-Alert-Severity headers, and Resend tags.
alerts@ is a sending identity only. The domain-wide receiving rule makes it
addressable, but the inbound route accepts hello@chalupa.run alone, so mail
sent to alerts@ is discarded. The Reply-To header is what makes replies
work, and the inbound loop guard now also ignores messages that claim to come
from alerts@chalupa.run.
The alert catalog and its noise budget live in src/lib/email/catalog.ts:
| Kind | Fires when | Repeat policy |
|---|---|---|
environment.idle |
Compute has been up past 6, 12, 24, 48, or 96 hours | One message per band per deployment; critical from 24 h |
environment.teardown_failed |
The reaper deadline passed and the deployment is still up | At most one per environment per day |
environment.heartbeat_lost |
Heartbeats stopped while the deployment is still billed | One per silence episode |
ci.run_finished |
A pipeline run reaches a terminal result | Failures and errors always; a green run only when it is the first pass after a failure |
ci.run_overdue |
A run passed its declared worst case without reporting | One per run |
ci.run_scan_truncated |
The running-run scan hit its hard cap, so overdue detection may be blind to some stuck runs | One per UTC day while it stays true |
cost.monthly_threshold |
Month-to-date estimate crosses 50, 80, 100, or 150 percent of the budget | One per band per calendar month |
email.delivery_failed |
The provider reports a bounce, complaint, or failure | Recorded in the console; never mailed to the address that failed |
Five independent controls keep the channel readable, and they are enforced in this order:
- a suppressed destination stops everything, because mailing an address that rejected us costs more than the missed alert;
- an explicit mute on one alert key wins over severity;
- a per-condition cooldown, which only a rising severity may interrupt;
- fleet-wide ceilings of six messages per hour and twenty-four per day; and
- grouping, so one evaluation produces at most one message per kind. A hundred simultaneous idle environments produce one message, not a hundred.
Anything held back is counted, and the next message of that kind says how many notifications were swallowed. Alerts refused by the hourly ceiling stay claimable and are delivered by a later run rather than dropped.
Every decision is durable. email_messages is the console's sent-mail log, the
idempotency ledger, and the correlation table the delivery webhook writes back
into; its unique dedupe key is what makes a duplicate alert impossible. Message
bodies are never stored, and destinations are stored only as SHA-256 digests,
matching how inbound mail is recorded. The provider Idempotency-Key is
derived from the same dedupe keys, so a retry after an ambiguous timeout
resolves to the stored result instead of a second alert.
GET /api/cron/alerts performs the evaluation; it requires CRON_SECRET like
every other cron route and refuses to run outside CHALUPA_DATA_MODE=database.
DMARC observation and promotion
DMARC changes DNS and can reject legitimate mail, so the application never
promotes the policy automatically. Use a report destination operated by a
DMARC processor or mailbox provider; do not point rua at an arbitrary
@chalupa.run local part because Chalupa forwards only the exact inbound
address.
Use this promotion gate:
- keep
p=noneand aggregate reporting enabled while SPF and DKIM are verified; - inventory every authorized sender for the organizational domain, including infrequent operational tools;
- review aggregate reports for at least 14 consecutive days and at least one
full sending cadence, and inspect representative delivered-message headers
for
dmarc=pass; - proceed only when every legitimate source is aligned and unexplained sources are either removed or understood;
- move to
p=quarantinewhile retaining reporting, then observe another 14 consecutive days with no legitimate failures; and - move to
p=rejectonly after the quarantine gate passes.
Record the date, previous policy, DNS TTL, authorized sources, report window,
and operator for every promotion. If a legitimate source starts failing,
restore the previous policy, wait at least the published TTL, repair alignment,
and restart that stage's observation window. Do not rely on pct as the only
safety control because receivers may not apply it consistently. See Resend's
DMARC guide for the current
provider guidance.
Bounce and complaint monitoring
The inbound endpoint deliberately subscribes only to email.received. Do not
add delivery-event subscriptions to POST /api/webhooks/resend: that route
acknowledges non-receiving events without building an alert or suppression
workflow.
Delivery events belong to a second endpoint with its own signing secret:
POST /api/webhooks/resend/delivery, verified with
RESEND_DELIVERY_WEBHOOK_SECRET. Create that webhook separately in Resend and
subscribe it to:
email.delivered;email.bounced;email.complained; andemail.failed.
The route also accepts email.delivery_delayed and every other event type,
but acknowledges them without acting; subscribe to them only if the extra
visibility in the provider dashboard is wanted.
A permanent bounce or any complaint suppresses that destination immediately:
the digest is written to email_suppressions and no further alert is sent
until an operator clears it. A temporary bounce deliberately does not
suppress, because a full mailbox should delay one alert rather than silence
the channel. Because a delivery failure cannot be mailed to the address that
just failed, the notice is written to the outbox with a suppressed status
and the console becomes the fallback channel. Only the SHA-256 digest of the
destination is stored; the provider dashboard and suppression list remain the
authority on the underlying address, and webhook payloads are never copied into
Chalupa logs.
Exercise this path before release. Send to Resend's documented test recipients and confirm that the outbox row moves to the expected status, that the suppression is created for permanent failures only, and that a following alert is correctly withheld.
Exercise the alert path before release with Resend's documented
bounced+<label>@resend.dev and complained+<label>@resend.dev test
recipients, then verify alert receipt, deduplication, and suppression behavior.
Repeat the drill after changing the monitoring endpoint or signing secret.
Resend documents the current event
types, safe test
addresses,
and account thresholds.
Rotate the receiving key
RESEND_RECEIVE_API_KEY does not expire automatically and has team-wide full
access. Rotate it independently from the domain-scoped send key and the webhook
signing secret. A normal rotation uses an overlap window:
- confirm the only expected consumers are TinyVault and the Vercel Production runtime for this webhook;
- create a new, uniquely named
full_accesskey and record only its key ID, creation date, owner, and planned retirement date in the private operations log; - copy the one-time raw value directly into TinyVault and the Vercel
Sensitive Production
RESEND_RECEIVE_API_KEY; never put it in a shell argument, clipboard manager, ticket, chat, repository file, or command output; - redeploy Production so the new environment value is active;
- send a uniquely identified inbound smoke message, require a
200webhook response and exactly one forwarded copy, and confirm the new key has a recent Receiving API request in Resend's key-filtered logs; - revoke the old key only after that evidence exists; and
- repeat the inbound smoke after revocation, then record completion and the revoked key ID.
If compromise is suspected, revoke the exposed key first and accept a bounded
forwarding outage while creating and deploying a replacement. Do not rotate
RESEND_WEBHOOK_SECRET as though it were the API key: replace that secret
through a separately verified webhook-endpoint rotation. Resend's key
handling guide
describes the provider's create, verify, overlap, and revoke model.
Verify and recover
Run the local contract before release:
cd cloud
bun test tests/email-config.test.ts tests/email-operations.test.ts tests/inbound-email.test.ts tests/resend-webhook-route.test.ts
bun run typecheck
For an outbound smoke test, send a short plain-text message with the
domain-scoped send key and a unique idempotency key. For inbound verification,
send one message to hello@chalupa.run and confirm that:
- Resend records one received message;
- the webhook returns
200; - the private destination receives the bounded body copy (without attachments); and
- the application logs contain no sender, recipient, subject, body, attachment name, or provider response.
A 401 means the webhook secret or signed bytes do not match. A 413 means
the event envelope exceeded the route limit. A 502 means forwarding failed
after signature and policy validation, so Resend may retry. A 503 means the
Production email configuration is incomplete or another delivery currently
holds the replay lease; retry after the response's Retry-After interval.