You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The KPI report currently delivers to Discord only. The email option is present in the dialog but disabled, and POST /api/kpi-report rejects medium: "email", because this instance has no mail transport configured.
What's needed
No separate application — the existing Express server can send directly.
An SMTP account. Any mailbox provider works (Gmail app password, Fastmail, self-hosted). A transactional service (Resend, SendGrid, SES) is preferable for deliverability, since a report sent from a Flux node IP is prime spam-filter material — but it isn't a hard requirement.
Two dependencies:nodemailer for transport, exceljs for the XLSX attachment.
Five environment variables:SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM.
Behaviour
Follow the pattern the R2 backup already uses: the feature is a no-op unless every required variable is set. GET /api/kpi/availability should report whether email is configured so the dialog can enable the option automatically rather than hard-coding it.
Scope
The KPI computation is already delivery-agnostic (buildKpiReport() returns pure data; sendToDiscord() is the only delivery path). So this is:
HTML email body with the same section/table structure as the Discord embed, inline CSS, no emoji
XLSX attachment: overview sheet plus one sheet per section, named fluxtracker-kpi-{timeframe}-{YYYY-MM-DD}.xlsx
Server-side email validation (never trust the client); consider a lightweight MX check
Reuse the existing rate limiter, keyed on the email address as the destination
Mask email addresses in logs
README: document the env vars and the enabled/disabled behaviour
Notes
The numbers in the email must come from the same buildKpiReport() call as the Discord path — no second computation, or the two can drift.
See the "KPI Report" section of the README for the current design.
Choosing a relay (added 2026-08-22)
nodemailer and exceljs are both MIT-licensed and free — there is no paid dependency in the
code. The cost question is entirely about the relay we send through, not the library.
Self-hosting a mail server (Postfix) on a Flux node is free and open source but not viable in
practice: a cloud IP with no reverse DNS, SPF, DKIM or sending reputation goes to spam or is
rejected outright. Discounted.
Free-tier relays that speak plain SMTP, so the five SMTP_* variables above work unchanged:
Relay
Free tier
Own domain required
Notes
Brevo
300/day
No
Easiest start — SMTP creds without domain verification
SMTP2GO
1,000/month
No
Similar, lower volume
Resend
3,000/month, 100/day
Yes
Best deliverability of the three; needs a verified sending domain
Gmail app password
~500/day
No
Works, but sends from a personal mailbox and Google keeps tightening app passwords
Recommendation: start on Brevo (no domain needed, nothing to buy). Move to Resend if
FluxTracker gets its own domain, since a verified domain is what actually keeps reports out of
spam folders.
Design decision needed before building: who can be emailed
The Discord path is self-limiting — possessing the webhook URL is itself proof the requester
controls the destination. Email has no equivalent. An open form that mails an arbitrary
user-supplied address turns FluxTracker into a spam relay wearing our sending domain's
reputation, and the 5-minute per-destination rate limit does not protect against that: a
handful of reports mailed to unwitting strangers is enough to get the domain blocklisted, which
is slow and painful to reverse.
Three options, in order of preference:
Email only for scheduled reports (feat: scheduled KPI reports #63) — destinations are configured by the operator, not
typed by a visitor. Removes the problem entirely and is the most useful form of the feature
anyway.
Confirm the address once — first send to a new address delivers a confirmation link
instead of a report; the address is remembered after that.
Operator allowlist — an KPI_EMAIL_ALLOWLIST env var, simplest to build, least flexible.
Keep the open, unauthenticated form Discord-only regardless of which is chosen.
The KPI report currently delivers to Discord only. The email option is present in the dialog but disabled, and
POST /api/kpi-reportrejectsmedium: "email", because this instance has no mail transport configured.What's needed
No separate application — the existing Express server can send directly.
nodemailerfor transport,exceljsfor the XLSX attachment.SMTP_HOST,SMTP_PORT,SMTP_USER,SMTP_PASS,SMTP_FROM.Behaviour
Follow the pattern the R2 backup already uses: the feature is a no-op unless every required variable is set.
GET /api/kpi/availabilityshould report whether email is configured so the dialog can enable the option automatically rather than hard-coding it.Scope
The KPI computation is already delivery-agnostic (
buildKpiReport()returns pure data;sendToDiscord()is the only delivery path). So this is:emailService.js— transport, config detection, sendfluxtracker-kpi-{timeframe}-{YYYY-MM-DD}.xlsxNotes
The numbers in the email must come from the same
buildKpiReport()call as the Discord path — no second computation, or the two can drift.See the "KPI Report" section of the README for the current design.
Choosing a relay (added 2026-08-22)
nodemailerandexceljsare both MIT-licensed and free — there is no paid dependency in thecode. The cost question is entirely about the relay we send through, not the library.
Self-hosting a mail server (Postfix) on a Flux node is free and open source but not viable in
practice: a cloud IP with no reverse DNS, SPF, DKIM or sending reputation goes to spam or is
rejected outright. Discounted.
Free-tier relays that speak plain SMTP, so the five
SMTP_*variables above work unchanged:Recommendation: start on Brevo (no domain needed, nothing to buy). Move to Resend if
FluxTracker gets its own domain, since a verified domain is what actually keeps reports out of
spam folders.
Design decision needed before building: who can be emailed
The Discord path is self-limiting — possessing the webhook URL is itself proof the requester
controls the destination. Email has no equivalent. An open form that mails an arbitrary
user-supplied address turns FluxTracker into a spam relay wearing our sending domain's
reputation, and the 5-minute per-destination rate limit does not protect against that: a
handful of reports mailed to unwitting strangers is enough to get the domain blocklisted, which
is slow and painful to reverse.
Three options, in order of preference:
typed by a visitor. Removes the problem entirely and is the most useful form of the feature
anyway.
instead of a report; the address is remembered after that.
KPI_EMAIL_ALLOWLISTenv var, simplest to build, least flexible.Keep the open, unauthenticated form Discord-only regardless of which is chosen.