Transactional email

SkillOx sends transactional email only. No marketing, no third-party sharing of your address, no shadow contact lists. This page lists every kind of message we send, what triggers it, and how the opt-out works — critical mail is deliberately not opt-outable, and the page is explicit about which is which.

What we send

Six template names. Four go to creators; two go to the SkillOx admin address only.

creator_welcome

Trigger: first time you sign in via GitHub OAuth and a creator profile is either created from scratch or auto-claimed from a crawler-discovered match. Fires exactly once per account.

Contains: link to your dashboard, creator profile URL, a one-paragraph reminder of which channels are opt-in vs always-on.

grade_drop_alert

Trigger: a re-scan of a SKILL.md claimed by your creator account produces a worse letter grade than the previous one (e.g. B → D). No email when grade stays the same or improves. New skills (no previous scan) don't trigger this.

Contains: old grade, new grade, top five findings by severity, a deep link to the Report Card and to the specific scan ID. Includes an unsubscribe link.

pro_subscription_started

Trigger: Stripe's checkout.session.completed webhook fires successfully. Stripe sends the receipt separately — this is our own confirmation + feature inventory.

pro_subscription_ended

Trigger: Stripe's customer.subscription.deleted webhook (cancellation completes at the end of the current period; we email when the actual end-event lands, not when you click Cancel).

admin_new_creator + admin_critical_finding

These two go to the address in SMTP_ADMIN_TO only — not to any creator. The first fires on every new creator link; the second fires whenever a scan lands at grade F. Useful for spotting outliers fast on a small team.

Opt-out semantics

Two categories, only one of which you can mute.
Critical mail (billing receipts, security notices, sign-in events) ignores the unsubscribe flag entirely. Optional mail (grade-drop alerts, future weekly digests) respects it. The toggle on /account/billing only touches the optional category.

Two ways to opt out of the optional category:

To opt back in, use the same card on /account/billing. The unsubscribe page doesn't have a re-subscribe button because we don't want it to be a one-click reverse — if you unsubscribed, you meant to, and re-subscribing should be a conscious signed-in action.

Unsubscribe-link security model

Unsubscribe links are stateless. The token in /unsubscribe?token=<slug>.<sig> is creator_slug + "." + base64url(HMAC-SHA256(AUTH_SECRET, slug)). No row in any table per token, no expiry table to clean up, no DB write to send a link.

What happens if the mailserver is down

Our outbound stack is a soft-fail singleton — if SMTP_HOST is unset, or the connection fails, the calling flow logs a warning and continues. Sign-in still succeeds, scans still complete, the Stripe webhook still 200s. The trade-off is that you might miss an alert; the alternative (hard-failing on email) was worse.

For production we run a single SMTP transporter per process via Hetzner's webhosting mailserver (port 465, implicit TLS). The transporter is hoisted onto globalThis so dev-time HMR doesn't open a new socket on every code edit.

Admin editor

Template bodies ship as built-in English defaults. Each (name, locale) pair can be overridden from /admin/emails without redeploying. Saving an override writes to the email_templates table; reset to default re-upserts the hardcoded copy so the audit log keeps the edit history.

Each editor card has a Send test to me button that renders the active template with stub vars and ships one copy to the admin's own session email. The subject is prefixed with [TEST]; every attempt — success or failure — lands in the audit log. Useful for sanity-checking the HTML chrome across Gmail, Apple Mail, and Outlook before publishing an override.

Operator config (for self-hosters)

# .env.local — add these and restart the worker + web app
SMTP_HOST=mail.skillox.io
SMTP_PORT=465                   # 465 = implicit TLS, 587 = STARTTLS
SMTP_USER=hello@skillox.io
SMTP_PASS=…                     # from your mailserver console
SMTP_FROM=SkillOx <hello@skillox.io>
SMTP_ADMIN_TO=admin@skillox.io  # admin_* templates land here

Leave any of these unset and outbound mail enters soft-fail mode. Useful in CI; surfaced as a banner on /admin/emails so you don't lose track of it in production.

See also