Transactional email.
Send verification codes and other transactional text email from any permitted product using one server-side SMTP connection.
Current deployment
HQ sends through Resend SMTP using smtp.resend.com:465 with implicit TLS, username resend, and sender alex@alexstudiohq.com. The domain is verified and the owner confirmed receipt of the live test. Receiving is disabled. Resend Free allows 100 sent and received messages combined per UTC day and 3,000 per month, shared across products. The internal 500-attempt safeguard does not raise that provider allowance. See current provider limits and latest setup guides.
Enable a product
The owner must enable email on the project and issue or update a key with the email scope. Existing keys gain no permission automatically. Keep this key on your product backend. The SMTP password stays in the HQ Worker secret and is never returned to products.
const result = await hq.email.send(
"user@example.com",
"Verify your email for My Product",
"Your verification code is 123456. It expires in five minutes.",
{idempotencyKey: "registration-unique-event-id"}
);
// result.status === "accepted" means SMTP accepted the message.
// It does not confirm inbox delivery.
POST /v1/email/send accepts exactly to, subject and body as JSON plus an Idempotency-Key header. It supports one recipient, a 200-character subject and a UTF-8 plain-text body up to 32 KiB. All messages use the server-configured sender, alex@alexstudiohq.com. Arbitrary From headers, attachments, bulk recipient lists and client-selected SMTP servers are unavailable.
Duplicate prevention and errors
Keep the same idempotency key when checking the outcome of the same event. Reusing it with different content returns 409. Concurrent requests claim one send; accepted requests return the recorded result. A sending or unknown record blocks replay. SMTP has no built-in idempotency guarantee: a disconnect during submission may occur after the server queued the message. Do not retry such an outcome with a fresh key; reconcile the message ID in SMTP server logs. No automatic transport retry is performed.
Missing setup returns 503. Invalid messages return 400. Missing permission returns 403, revoked keys return 401 and exhausted shared allowance returns 429. SMTP authentication, rejection, connection and timeout errors use sanitized codes and messages. Logs contain stage, SMTP status and an opaque message ID, never credentials, recipient addresses, verification codes, subjects or body content. Message history stores status and a keyed request fingerprint rather than plaintext mail content. The shared email API permits 500 new attempts per UTC day; per-project activity is recorded separately.
Managed Resend SMTP
Set SMTP_PROVIDER=resend and store a sending-only, domain-restricted key as RESEND_SMTP_PASSWORD on both gateway and website-auth. This uses smtp.resend.com:465 with immediate TLS, username resend and the existing sender. The previous SMTP_PASSWORD remains available when SMTP_PROVIDER=smtp. No automatic fallback occurs. Provider delivery events and inbox placement are separate checks.
Original SMTP setup
Configure SMTP_PASSWORD as a Secret on alex-hq-gateway. Host: mailserver.businessidentity.llc; port: 465; username and sender: alex@alexstudiohq.com. TLS starts immediately, certificate validation remains enabled and submission has a 15-second total deadline. There is no plaintext or insecure TLS fallback. Public DNS must authorize your mail server: obtain its exact SPF and DKIM requirements, preserve existing authorized senders and configure DMARC deliberately. No hosting targets or provider DNS keys are guessed.
Existing website registration
The private Better Auth website-account Worker uses the same reusable sender. Save the selected provider secret on alex-hq-website-auth too. When configured, new registrations receive a six-digit verification code, stored only as a hash, valid for five minutes and single use with at most three incorrect attempts. Resends wait 60 seconds and are limited to three sends per recipient per hour and 500 website verification attempts per day. These website limits are separate from the generic email API allowance.
// Through your existing authenticated backend proxy:
POST /v1/auth/email/email-otp/send-verification-otp
{"email":"user@example.com","type":"email-verification"}
POST /v1/auth/email/email-otp/verify-email
{"email":"user@example.com","otp":"123456"}
These account endpoints require the existing website project's auth-scoped key and trusted origin. They are not a cross-product account directory. Add a code-entry form and proxy the two exact routes from your website backend. The current frontend is maintained separately and is not changed by this release. After that flow is ready, set EMAIL_VERIFICATION_REQUIRED=true on the website-account Worker to prevent password login until verification. This flag defaults to false to preserve existing login behavior. Verification does not automatically create a new login session; sign in normally afterward. Missing SMTP blocks required verification rather than granting access.
Other products can use the generic email API with their own authentication system. Generate codes securely, store a hash and expiry on their backend, limit guesses and resends, and mark users verified only after a valid code is consumed. Sending an email alone never proves ownership.
One-message connection test
The repository supplies npm run email:test. Put SMTP_PASSWORD, SMTP_TEST_TO (your own working inbox), and SMTP_TEST_CONFIRM=send-one in the ignored .env.smtp.local file, or set them in the process environment. The script sends exactly one message through the same SMTP client using Node TLS. Check inbox/spam and the message's SPF/DKIM/DMARC results. Never commit that file or paste its contents into chat. SMTP acceptance and inbox placement are separate verification steps.