GoShipped
Product

✉️ Emails

Supabase sends Auth mail. Resend is optional product email — welcome, trial, and payment events — off until you enable it.

Send product emails from day one — or don’t. The app boots either way.

GoShipped splits email in two. Mix them up and you will debug the wrong dashboard.

Signup / reset password     →  Supabase Auth
Welcome / trial / receipt   →  Resend (optional)
Stripe invoices             →  Stripe

Two channels

ChannelOwnerExamples
AuthSupabase Auth + supabase/templates/Confirm signup, reset password
Productapps/api/app/email/ via ResendWelcome, trial ending, payment failed
ReceiptsStripeInvoices

Resend is not required to start GoShipped. Default is EMAIL_ENABLED=false. Auth mail still works. See Authentication.

Turn product email on

Create an API key, then in apps/api/.env:

EMAIL_ENABLED=true
RESEND_API_KEY=re_...
EMAIL_FROM=Your App <hello@your-domain.com>
EMAIL_REPLY_TO=
FRONTEND_URL=https://localhost:3000

Doctor only requires Resend when EMAIL_ENABLED=true.

VariableRole
EMAIL_ENABLEDMaster switch
RESEND_API_KEYResend API key
EMAIL_FROMMust be a domain you verify in Resend
EMAIL_REPLY_TOOptional
FRONTEND_URLLinks inside the email (required to actually send)
EMAIL_DEV_RECIPIENTNon-production only — redirect all product mail to one inbox
EMAIL_PRODUCT_NAME / EMAIL_SUPPORT_EMAILOptional overrides; prefer starter.config.json

Brand strings should come from app.name and support.email. Templates do not hardcode “GoShipped”.

What happens when it is off

ConditionResult
EMAIL_ENABLED=falseSkip, log email_skipped reason=email_disabled
Missing key or EMAIL_FROMSkip (error-level log in production)
Duplicate idempotency keyNo second Resend call
Resend errorSoft fail — /me and Stripe webhooks still succeed

Users can sign up and chat with product email disabled.

Where the code lives

PiecePath
Configapps/api/app/email/config.py
Send + ledgerapps/api/app/email/service.py
Lifecycle helpersapps/api/app/email/events.py
HTML / textapps/api/app/email/templates.py
Resend HTTPapps/api/app/email/client.py

Call events.py (or send_email) — not Resend — from your own hooks.

Welcome already fires from authenticated GET /me (once per user):

# apps/api/app/email/events.py
async def maybe_send_welcome_email(db, user, *, cfg=None):
    key = f"welcome:{user.id}"
    existing = await find_by_idempotency_key(db, key)
    if existing is not None and existing.status in {"sent", "skipped", "pending"}:
        return None
    return await send_welcome_email(db, user, cfg=cfg)

Billing mail (trial ending, payment failed, …) runs after subscription sync in the Stripe webhook. Keep customer.subscription.trial_will_end on the webhook endpoint or that email never sends. See Payments.

To send something new: add a template in templates.py, an idempotent helper in events.py, and call it from the event you already own.

Production

  • Verify the sending domain in Resend
  • EMAIL_FROM on that domain
  • EMAIL_ENABLED=true, RESEND_API_KEY, FRONTEND_URL = real web origin
  • APP_ENV=production (disables EMAIL_DEV_RECIPIENT)
  • Auth still needs Supabase SMTP for confirmation and reset — that is not Resend

The deploy wizard can upload Resend keys. Domain verification stays a dashboard task.

On this page