All documentation

    Email System

    All outbound email is sent through Lovable's managed email delivery service — the app never talks to an SMTP server or a third-party ESP directly for transactional mail. This document covers the sending pipeline, the template registry, the lifecycle emails that ride on it, tracking, and the separate Mailchimp integration used for marketing/lifecycle segmentation.

    Sending pipeline

    The single entry point for sending a transactional email is sendTemplateEmail in src/lib/email-templates/send-email.ts. It is server-only (it reads LOVABLE_API_KEY and must never be imported into client components). Given a template name and a recipient:

    1. It looks the template up in the registry (src/lib/email-templates/registry.ts); an unknown name throws immediately with the list of valid names.
    2. If the template defines a fixed to address (used for internal notification templates, e.g. admin-new-signup), that always wins over the caller-supplied recipient.
    3. It renders the React Email component to both HTML and plain text via @react-email/render.
    4. It resolves the subject, which can be a static string or a function of the template data.
    5. It calls sendLovableEmail from @lovable.dev/email-js, passing:
      • from: "Rank Pilot AI SEO <noreply@{FROM_DOMAIN}>" (the cosmetic From header domain).
      • sender_domain: the verified sending subdomain delegated to Lovable's nameservers (kept separate from the root domain deliberately — never send from the root domain).
      • purpose: 'transactional' and a label set to the template name, for observability on Lovable's side.
      • An idempotency_key, defaulting to a random UUID (callers can pass one to dedupe retries of the same logical send).
      • An optional reply_to.

    Suppression is handled server-side by Lovable: if a recipient is suppressed, sendTemplateEmail returns { sent: false, reason: 'recipient_suppressed' } rather than throwing, so callers can treat it as an expected, non-error outcome. Any other failure surfaces as an EmailAPIError (with .code and .status) or a generic error, and is expected to propagate so the caller/cron job can log or retry appropriately.

    Template registry

    src/lib/email-templates/registry.ts maps a template name (string) to a TemplateEntry: a React Email component, a subject (string or function of the template data), and optional displayName, previewData, and a fixed to. New templates must be created as a component in this directory and then registered in the TEMPLATES map — the send pipeline only knows about registered names.

    Registered templates include, among others:

    • welcome, verification-setup, magic-link, recovery, reauthentication, email-change — account lifecycle and auth-adjacent emails.
    • weekly-digest, monthly-rank-report, audit-complete, rank-increase, rank-drop — product/reporting emails.
    • trial-ending-3day, trial-ending-1day, trial-expired, payment-failed — billing lifecycle emails.
    • winback, winback-report, reengage-autopilot, reengage-keywords, reengage-content, reengage-technical, reengage-engine — re-engagement sequence for inactive accounts.
    • sales-confirmation, customer-invite, google-connection-checklist — onboarding/sales emails.
    • contact-acknowledgement, contact-notification, support-acknowledgement, support-reply, chat-handover, admin-new-signup — support and internal notification emails (several of these use a fixed to address for internal staff).

    Lifecycle emails

    Welcome

    Sent when a new account is created, via the welcome template, introducing the product and pointing the user to next steps.

    Trial lifecycle

    The trial-nudge-emails-daily job (see scheduled-jobs.md) sends trial-ending-3day and trial-ending-1day as a trial approaches its end, and trial-expired once it has ended. payment-failed is sent separately when a billing charge fails.

    Win-back

    The send-winback-daily job runs the win-back sequence for accounts that have gone inactive, using staged logic in src/lib/winback-logic.ts to decide which of the winback, winback-report, or reengage-* templates (autopilot, keywords, content, technical, engine) is appropriate based on how the account has been using the product. This is the sequence tracked on the /admin/email-engagement dashboard.

    Verification nurture

    The verification-setup template is sent to accounts that have signed up but not completed key setup steps (e.g. connecting/verifying their site), reminding them what's left to do and linking to the setup guide. It carries daysInactive and websiteDomain context so the copy can reflect how long the account has been stalled.

    Weekly digest with blog block

    The weekly-digest template (sent by send-weekly-digest, Mondays 09:00 UTC) is the richest lifecycle email: it reports total tracked keywords, improved/declined/unchanged counts, top gaining and losing keywords, quick-win suggestions, the single biggest mover, and an optional Google Ads summary (impressions, clicks, cost, conversions, ROAS, CTR) when ad data is available. It also carries an optional featured blog block (featuredPostTitle, featuredPostSummary, featuredPostBody, featuredPostUrl) so a relevant piece of content can be surfaced inside the digest alongside the customer's own ranking data.

    Tracking and events

    Email engagement is tracked through two lightweight endpoints under /api/public/:

    • e.o — an open-tracking pixel endpoint.
    • e.c — a click-tracking/redirect endpoint, recording the link_label clicked.

    Both write into Supabase tables that back the admin dashboard: email_send_log records each send attempt (message_id, template_name, status, timestamp), and email_events records subsequent opens/clicks (message_id, template_name, event_type, link_label, timestamp). The /admin/email-engagement page joins these to show send counts alongside open/click rates over a selectable 7/30/90-day window; it currently focuses on the winback template family as the primary tracked sequence.

    Lovable-side webhooks (src/routes/lovable/email/auth/webhook.ts) also feed into this pipeline, handling delivery/auth-related callbacks from the managed email service.

    Mailchimp integration

    Mailchimp is used purely for marketing segmentation and campaign sends — it is separate from the transactional Lovable pipeline described above and does not go through the template registry.

    • Lifecycle sync (mailchimp-plan-sync-daily, src/lib/mailchimpPlanSync.server): keeps each customer's Mailchimp tags aligned with their real trial/plan status, so marketing segments (e.g. "on trial", "active customer", "churned") stay accurate without sending any email itself. It runs nightly as a bounded sweep, immediately after trial/plan changes for specific users, and can be run as a full backfill.
    • Weekly blog campaign (mailchimp-blog-campaign-weekly, src/lib/mailchimpBlogCampaign.server): sends a Mailchimp campaign promoting the latest blog content to leads captured on the marketing site (visible in /admin/leads), rather than to paying customers. It supports a dryRun mode to preview the campaign before it actually sends.

    Both Mailchimp jobs are triggered by pg_cron calling /api/public/mailchimp-plan-sync and /api/public/mailchimp-blog-campaign respectively, authenticated with the shared CRON_SECRET (checked against the x-cron-secret header). Shared Mailchimp API helper logic lives in supabase/functions/_shared/mailchimp.ts.

    Operational notes

    • The sender subdomain and From-header domain are fixed at scaffold time in send-email.ts; do not send from the root domain.
    • LOVABLE_API_KEY must be present in the server environment for any transactional send to succeed — its absence causes sendTemplateEmail to throw immediately.
    • When investigating a "customer didn't receive an email" report, check in this order: email_send_log for the template and recipient, then email_events for tracking activity, then /admin/cron-status if the email was meant to be triggered by a scheduled job, then whether the recipient may have been suppressed by Lovable.