← Server architecture · all tasks
Set up Resend for real account emails
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 key — API 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
mail.lor-server.cocreations.com.auas the sending domain, not the barecocreations.com.auroot: confirmed 2026-08-12 that the root domain already carries an MX record (self-pointed,0 cocreations.com.au.— no active third-party mail provider visible, but still Kris’s real business domain). Resend’s own guidance is to use a dedicated subdomain for transactional/marketing mail regardless, so the sending domain’s DNS is fully isolated from anything else oncocreations.com.au.PUBLIC_BASE_URL=https://lor-server.cocreations.com.au/director, with the/directorsuffix: confirmed against the droplet’s live Caddyfile (2026-08-12) —lor-server.cocreations.com.auonly reverse-proxies paths under/director/*to the director process; everything else either hits/play/*(a static file server) or falls through to a placeholder"Lor game server is up."response.email.ts’sbuildVerifyUrl/buildResetUrlbuild${PUBLIC_BASE_URL}/verify?token=...and/reset?token=...;http.ts’s route matcher (matchesPath) expects either a bare/verifyor a/director-prefixed/director/verify— so the value here needs the/directorsuffix for the emailed link to actually reach the handler through Caddy instead of 404ing.
Flagged, not fixed here: the link-code /play URL breaks either way
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
- Resend domain
mail.lor-server.cocreations.com.aushows Verified in the Resend dashboard. /etc/lor/game.envon the droplet has all three vars,lor-director.servicerestarted clean.- A live
attach_emailorrequest_password_resetproduces a real email (not a log line) in the test inbox used to check.
View source in repository · also available as raw markdown.