← Server architecture · all tasks

Set up Resend for real account emails

needs-input priority 2 estimate S

server-architecture/012 · created 2026-08-12 · updated 2026-08-13

What this is

Task 010 (real accounts) is deployed and live — see land-of-lor/docs/handoff/historic/010-accounts.md. Its mailer (director/src/email.ts) already knows how to send through Resend; until real credentials exist it falls back to LogMailer, which just writes the verify/reset link to the director’s own log instead of emailing it. This is non-blocking — accounts, guest play, and link-code device attach all work today without this. It only matters for two things: a real “verify your email” message, and self-service password reset (right now, a password reset can only be completed by someone reading the director’s server log for the link).

This task is entirely external-repo actions only Kris can do — creating a Resend account, proving domain ownership via DNS, and generating an API key — so it’s filed straight to needs-input rather than backlog. No design decision is open here (see “Why these exact values” below); it’s a checklist.

Step by step

1. Create a Resend account — go to resend.com, sign up (free tier is enough: 3,000 emails/month, 100/day — nowhere near what this game needs any time soon). Use whatever email/login Kris normally uses for project services.

2. Add and verify the sending domain — in the Resend dashboard, go to Domains → Add Domain. Enter exactly:

mail.lor-server.cocreations.com.au

(a dedicated subdomain of the game’s own subdomain — not the bare cocreations.com.au root, which is the CoCreations business domain and already carries its own MX record; keeping the sending domain scoped under lor-server. means this can’t touch or break any existing business email setup, and it’s exactly what Resend itself recommends: a dedicated subdomain for transactional mail).

Resend will display a set of DNS records to add (typically an MX record plus 2-3 TXT/DKIM records, and it’ll suggest a DMARC TXT too — take that suggestion). Add exactly what Resend’s dashboard shows you — the values are generated per-domain at add-time, so there’s nothing to copy from this task file. lor-server.cocreations.com.au’s DNS (and therefore this new mail. subdomain’s) is hosted by iFastNet (nameservers ns101.ifastnet.com / ns201.ifastnet.com — confirmed live 2026-08-12), so add the records there, in whatever iFastNet/cPanel-style DNS zone editor Kris already uses to manage cocreations.com.au.

Resend auto-checks verification in the background; it’s usually minutes, can be a few hours depending on DNS propagation. The domain shows Verified in the dashboard once it’s done — don’t move on to step 4 (dropping the API key onto the droplet) until it does, since Resend will reject sends from an unverified domain.

3. Generate an API keyAPI Keys → Create API Key. Name it something identifiable, e.g. lor-director-prod. Permission: Sending access only (not “Full access” — the director only ever calls the send-email endpoint, per director/src/email.ts’s ResendMailer). Copy the key immediately — Resend only shows it once (re_...).

4. Drop the three env vars onto the droplet — SSH to the droplet (ssh lor@209.38.29.22), then:

sudo nano /etc/lor/game.env

Add these three lines (the file already has DATABASE_URL, DIRECTOR_PORT, SNAPSHOT_INTERVAL_MS, GODOT_BIN — confirmed live 2026-08-12; add these alongside, don’t replace anything):

RESEND_API_KEY=re_your_actual_key_from_step_3
EMAIL_FROM="Legend of Rah <noreply@mail.lor-server.cocreations.com.au>"
PUBLIC_BASE_URL=https://lor-server.cocreations.com.au/director

Save, then restart the director so it picks up the new env:

sudo systemctl restart lor-director.service
sudo systemctl status lor-director.service   # confirm active (running), no crash-loop

5. Verify it’s actually sending — trigger attach_email or request_password_reset against the live director (e.g. npm run verify-accounts-e2e from game/director/, or by hand from a client) and confirm two things: (a) journalctl -u lor-director -f no longer logs a [mailer:log-fallback] line for that request, and (b) the email actually lands in the test inbox used.

Why these exact values

server.ts’s create_link_code handler builds its QR/URL field the same way: ${PUBLIC_BASE_URL}/play?link=<code>. With the /director-suffixed value above, that becomes .../director/play?link=... — not a route Caddy or the director serves, so the QR code / link-code URL will be broken once this env var is set (today, with PUBLIC_BASE_URL unset, it falls back to http://localhost:8081/play?link=..., which is equally unusable off the droplet — so this isn’t a regression, just not fixed by this task either).

This isn’t fixable by choosing a different single value: verify/reset need the director’s own domain+prefix; the play link needs wherever the actual current web client is hosted, which per land-of-lor/docs/handoff/historic/010-accounts.md’s Deploy section is https://lor.kris.ai-task-runner.com/play/ (the workspace site) — not lor-server.cocreations.com.au/play/, which Caddy still points at an old pocs/multiplayer-poc web build that was never updated in the accounts-v2 deploy (confirmed live 2026-08-12: still serving from /opt/lor/game-server/pocs/multiplayer-poc/world-server/client/web-build). One env var can’t correctly serve both purposes under the current route layout. Not blocking — a link code can also be typed in by hand on the account screen (redeem_link_code), so the QR/URL is a convenience, not the only path — but worth a real follow-up: either split into two env vars (e.g. a DIRECTOR_PUBLIC_URL for verify/reset and a separate one for the play link), or update the droplet’s own /play to reverse-proxy or mirror the current build instead of serving the stale pocs/ one. Prioritized verify/reset (the security-relevant, actually-requested piece) over the QR link in the values recommended above.

Progress — 2026-08-13 (steps 1, 3 and 4 done; step 2 is the whole remainder)

Kris created the Resend account and generated the API key (sending-access only — confirmed by the key being rejected for GET /domains, which is exactly the scope this task asked for). The domain mail.lor-server.cocreations.com.au is added but not verified: the dashboard is sitting on “Fill in your DNS Records”, and a live DNS check found no TXT, MX or DKIM record published under that subdomain.

The key is on the droplet but commented out, on purpose. Deploying it live and running verify-accounts-e2e showed why: createMailer only falls back to LogMailer when RESEND_API_KEY is absent, so with the key set and the domain unverified, every send throws and attach_email answers INTERNAL_ERROR — strictly worse for real users than the log fallback. A direct API send confirmed the cause (403 — the … domain is not verified). /etc/lor/game.env therefore has the key present, commented, with a note; the last step of this task is to uncomment it and re-run game/update_game_env.sh. (A 422 Invalid to field seen alongside this was just the e2e’s @example.com test recipient, not a config problem.)

EMAIL_FROM is live. PUBLIC_BASE_URL was superseded the same day by EMAIL_VERIFY_BASE_URL + LINK_CODE_BASE_URL — see task 016, which fixes the broken-QR-link consequence flagged below.

The remaining work is DNS, and it is at iFastNet

The records must be added in iFastNet’s DNS zone editor: cocreations.com.au delegates to ns101.ifastnet.com / ns201.ifastnet.com (re-confirmed 2026-08-13), so whoever registers the domain does not control its records. A Namecheap API key was tried and cannot do this job for two independent reasons: the credentials were rejected outright (1011102 — API Key is invalid or API access has not been enabled; Namecheap also requires API access switched on per-account and the calling IP whitelisted), and more fundamentally Namecheap’s domains.dns.setHosts only edits zones served by Namecheap’s own BasicDNS — it has no effect on a domain delegated elsewhere. Moving the nameservers to Namecheap to make the API usable would mean re-creating the entire existing zone by hand, including the business domain’s live MX and web records, to fix one subdomain: not worth the blast radius.

So step 2 stays a human, in-browser action in iFastNet’s control panel.

Definition of done

View source in repository · also available as raw markdown.