Wire production email secrets through Forgejo deploy.
All checks were successful
CI / Lint, type check, unit tests, coverage (push) Successful in 2m2s
CI / E2E browser tests (push) Successful in 1m16s

Deploy workflow now writes MAIL_* and APP_PUBLIC_BASE_URL from Actions
secrets into the server .env so Resend SMTP works after domain verify.
Document Resend-only setup, Forgejo secret names, and prod expose-token off.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Joakim Mörling 2026-05-21 18:39:00 +02:00
parent 86fb946e33
commit ad195fd890
6 changed files with 70 additions and 51 deletions

View file

@ -32,11 +32,11 @@ APP_PUBLIC_BASE_URL=http://localhost:3000
# ---------- SMTP (local Docker uses Mailpit via docker-compose.yml) ----------
# docker compose up → view mail at http://localhost:8025
# Leave MAIL_HOST unset in .env to use compose defaults (mailpit).
# Production: use Resend/Brevo SMTP — see README "Email (password reset)"
# Production (Resend SMTP) — see docs/production-email-checklist.md
# MAIL_HOST=smtp.resend.com
# MAIL_PORT=587
# MAIL_USERNAME=
# MAIL_PASSWORD=
# MAIL_USERNAME=resend
# MAIL_PASSWORD=re_... # API key; never commit a real value
# MAIL_FROM=noreply@bilhej.se
# ---------- Production admin (prod profile only) ----------

View file

@ -39,6 +39,12 @@ jobs:
SWISH_NUMBER: ${{ secrets.SWISH_NUMBER }}
ADMIN_EMAIL: ${{ secrets.ADMIN_EMAIL }}
ADMIN_PASSWORD: ${{ secrets.ADMIN_PASSWORD }}
APP_PUBLIC_BASE_URL: ${{ secrets.APP_PUBLIC_BASE_URL }}
MAIL_HOST: ${{ secrets.MAIL_HOST }}
MAIL_PORT: ${{ secrets.MAIL_PORT }}
MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }}
MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }}
MAIL_FROM: ${{ secrets.MAIL_FROM }}
run: |
# Docker Compose treats $ as variable interpolation in .env files.
# Escape literal dollar signs (e.g. in passwords) as $$.
@ -54,6 +60,12 @@ jobs:
printf 'SWISH_NUMBER=%s\n' "$(escape "$SWISH_NUMBER")"
printf 'ADMIN_EMAIL=%s\n' "$(escape "$ADMIN_EMAIL")"
printf 'ADMIN_PASSWORD=%s\n' "$(escape "$ADMIN_PASSWORD")"
printf 'APP_PUBLIC_BASE_URL=%s\n' "$(escape "${APP_PUBLIC_BASE_URL:-https://bilhej.se}")"
printf 'MAIL_HOST=%s\n' "$(escape "$MAIL_HOST")"
printf 'MAIL_PORT=%s\n' "$(escape "${MAIL_PORT:-587}")"
printf 'MAIL_USERNAME=%s\n' "$(escape "$MAIL_USERNAME")"
printf 'MAIL_PASSWORD=%s\n' "$(escape "$MAIL_PASSWORD")"
printf 'MAIL_FROM=%s\n' "$(escape "${MAIL_FROM:-noreply@bilhej.se}")"
} > .env
- name: Build and start production stack

View file

@ -188,7 +188,7 @@ entity must NOT have an address field. The address lookup and mailing are
external/human processes in Phase 0.
### Local email (Mailpit)
`docker compose up` includes Mailpit (`ghcr.io/axllent/mailpit:v1.28`); password-reset mail appears at http://localhost:8025. E2E verifies SMTP via Mailpit API (`frontend/e2e/helpers/mailpit.ts`). Production uses transactional SMTP (Resend/Brevo)—see README.
`docker compose up` includes Mailpit (`ghcr.io/axllent/mailpit:v1.28`); password-reset mail appears at http://localhost:8025. E2E verifies SMTP via Mailpit API (`frontend/e2e/helpers/mailpit.ts`). Production uses Resend SMTP—see docs/production-email-checklist.md.
### Password reset test token (never in production)
`app.password-reset.expose-token` must stay **false** in prod/default; it is only enabled in `application-docker.yml` for CI E2E so Playwright can read `testToken` from the forgot-password response.

View file

@ -202,23 +202,24 @@ docker pull ghcr.io/axllent/mailpit:v1.28
To disable Mailpit and log links only, remove `MAIL_HOST` from the backend service in
`docker-compose.yml` or set `MAIL_HOST=` in `.env`.
**Production (transactional provider):** Use [Resend](https://resend.com) or
[Brevo](https://www.brevo.com)—not a self-hosted mail server on the VPS.
**Production:** [Resend](https://resend.com) via SMTP (no Resend Java SDK required). See
[docs/production-email-checklist.md](docs/production-email-checklist.md).
1. Sign up and add domain **bilhej.se**
2. Add the providers **SPF** and **DKIM** DNS records at your registrar (no MX needed for send-only)
3. Create SMTP credentials in the provider dashboard
4. On the production server `.env` (or Forgejo deploy secrets):
1. Verify domain **bilhej.se** in Resend (SPF + DKIM DNS records)
2. Create an API key (`re_...`)
3. On the production server `.env`:
```bash
APP_PUBLIC_BASE_URL=https://bilhej.se
MAIL_HOST=smtp.resend.com # example; use your providers host
MAIL_HOST=smtp.resend.com
MAIL_PORT=587
MAIL_USERNAME=resend # example
MAIL_USERNAME=resend
MAIL_PASSWORD=re_xxxxxxxx
MAIL_FROM=noreply@bilhej.se
```
`MAIL_USERNAME` is always the literal string `resend`; `MAIL_PASSWORD` is the API key.
5. Deploy via **Deploy to Production**, then test forgot-password on https://bilhej.se
See [docs/production-email-checklist.md](docs/production-email-checklist.md) for a step-by-step operator checklist.
@ -335,6 +336,12 @@ Before the first deploy, complete these steps on the production server (`srvr.nu
| `SWISH_NUMBER` | Swish phone number for payment instructions |
| `ADMIN_EMAIL` | Production admin email (e.g. `admin@bilhej.se`) |
| `ADMIN_PASSWORD` | Strong unique admin password (password manager) |
| `APP_PUBLIC_BASE_URL` | `https://bilhej.se` (password-reset links) |
| `MAIL_HOST` | `smtp.resend.com` |
| `MAIL_PORT` | `587` |
| `MAIL_USERNAME` | `resend` (literal string) |
| `MAIL_PASSWORD` | Resend API key (`re_...`; rotate if ever exposed) |
| `MAIL_FROM` | `noreply@bilhej.se` (must be on verified domain) |
Passwords may contain `$` — the deploy workflow escapes these for Docker Compose.
Production does **not** seed `test@bilhej.se` or demo orders. On first start, the

View file

@ -8,3 +8,5 @@ app:
admin:
email: ${ADMIN_EMAIL}
password: ${ADMIN_PASSWORD}
password-reset:
expose-token: false

View file

@ -1,58 +1,56 @@
# Production email checklist (operator)
# Production email with Resend (operator)
Complete these steps on the server / Forgejo—nothing in this file is applied automatically.
BilHej sends password-reset mail via **SMTP** (Spring `JavaMailSender`). You do **not** need the
Resend Java SDK from their onboarding snippet—only env vars on the server.
## Prerequisites
## Security
- Domain **bilhej.se** DNS managed at your registrar
- BilHej deployed via Forgejo **Deploy to Production**
- Never commit `re_...` keys to git. Put them only in the server `.env`.
- If an API key was pasted in chat or logs, **revoke it** in Resend → API Keys and create a new one.
## 1. Choose a transactional provider
## 1. Verify bilhej.se in Resend
Recommended: [Resend](https://resend.com) or [Brevo](https://www.brevo.com) (EU, free tier).
1. [Resend](https://resend.com) → **Domains** → add `bilhej.se`
2. Add the DNS records Resend shows (SPF, DKIM; DMARC optional) at your domain registrar
3. Wait until status is **Verified**
## 2. Verify the domain
Until the domain is verified, `MAIL_FROM=noreply@bilhej.se` will fail. For a quick API test only,
Resend allows `onboarding@resend.dev` → your own inbox—not for production.
In the provider dashboard, add **bilhej.se** and publish the DNS records they give you:
## 2. Production `.env` (SMTP, not SDK)
- **SPF** (TXT)
- **DKIM** (CNAME or TXT)
- **DMARC** (TXT, optional but recommended)
You do **not** need MX records if the app only sends mail (forgot-password).
Wait until the provider shows the domain as verified.
## 3. Create SMTP credentials
Copy from the provider:
- SMTP host (e.g. `smtp.resend.com`)
- Port (`587`)
- Username / password or API key used as password
## 4. Update production `.env`
On the server (same file used by `docker-compose.prod.yml`):
On the server (file used by `docker-compose.prod.yml`):
```bash
APP_PUBLIC_BASE_URL=https://bilhej.se
MAIL_HOST=<provider-smtp-host>
MAIL_HOST=smtp.resend.com
MAIL_PORT=587
MAIL_USERNAME=<from-provider>
MAIL_PASSWORD=<from-provider>
MAIL_USERNAME=resend
MAIL_PASSWORD=re_your_new_api_key_here
MAIL_FROM=noreply@bilhej.se
```
## 5. Deploy
| Variable | Resend value |
|----------|----------------|
| `MAIL_USERNAME` | Always the literal string `resend` |
| `MAIL_PASSWORD` | Your API key (`re_...`) |
| `MAIL_FROM` | Any address on **verified** domain, e.g. `noreply@bilhej.se` |
Run **Deploy to Production** in Forgejo (do not rsync or manual compose on the server).
## 3. Deploy
## 6. Smoke test
Run **Deploy to Production** in Forgejo (pipeline only—no manual rsync).
1. Open https://bilhej.se/logga-in → **Glömt lösenord?**
2. Enter an email that exists in `users`
3. Check the inbox (and spam) for the reset message
4. If nothing arrives: `docker logs bilhej-backend-prod 2>&1 | grep -i mail`
## 4. Smoke test
Fallback without SMTP: reset links still appear in backend logs (`Password reset link for`).
1. https://bilhej.se/logga-in → **Glömt lösenord?**
2. Email that exists in `users`
3. Check inbox and spam
4. Resend dashboard → **Emails** should show the send
5. On failure: `docker logs bilhej-backend-prod 2>&1 | grep -i mail`
Fallback: reset links still log when `MAIL_HOST` is empty.
## Local dev
Keep using Mailpit (`docker compose up`, http://localhost:8025). Do not point local Docker at
Resend unless you intend to send real mail.