Admins can email a new user a welcome with a one-time link to choose their password, and send a reset link from the users list. The sign-in page gets "Forgot password?". Mail goes out through Resend; the emails show the person's sign-in address. - password_links collection (migration applied): only the SHA-256 of each token is stored; invites last 7 days, resets 1 hour, one use each - /set-password and /forgot-password pages and their /api/auth routes - forgot-password never reveals whether an account exists, throttles repeat mails, and builds links only from APP_URL - env: RESEND_API_KEY, EMAIL_FROM, EMAIL_REPLY_TO, APP_URL (docs/setup/email.md) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3.7 KiB
Email from the dashboard
The dashboard sends two mails: a welcome invite when an admin adds a user or presses "Send welcome email", and a password reset when someone uses "Forgot password?" on the sign-in page.
Both go out through Resend — one HTTP POST to https://api.resend.com/emails,
no SDK and no new dependency. The key stays on the server; the browser never
sees it.
Environment variables
Set these in the Next.js server environment (Coolify env vars, .env.local,
or whatever your host uses):
| Variable | Required | What it does |
|---|---|---|
RESEND_API_KEY |
Yes | The Resend API key (re_…). Without it the mail feature is off and the admin button answers "Email is not set up yet". |
EMAIL_FROM |
No | The sender shown on the mail. Defaults to MEGA Dashboard <support@apphub.co.za>. |
EMAIL_REPLY_TO |
No | The address a reply goes to. Falls back to the sender. |
APP_URL |
No | The dashboard's public URL, used to build the link in the mail and to point the logo at your own server. Falls back to the request's own origin. |
RESEND_API_KEY is the same key you already use as the PocketBase SMTP
password. Resend's "API key" and its "SMTP password" are different strings —
use the API key here (re_…), not the SMTP password.
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx
EMAIL_FROM=MEGA Dashboard <support@apphub.co.za>
EMAIL_REPLY_TO=support@apphub.co.za
APP_URL=https://wfdash.mega.infra.apphub.co.za
APP_URL must be the address people actually open. If it is wrong, every
"Choose your password" link points somewhere nobody can reach.
Run the migration
The links live in a PocketBase collection. Run this once against the environment you are deploying to:
PB_URL=https://pb.mega.infra.apphub.co.za \
PB_ADMIN_EMAIL=admin@mega.co.za \
PB_ADMIN_PASSWORD=... \
python3 scripts/migrations/2026-11-02-password-links.py
The script is idempotent — it creates password_links when it is missing and
verifies it when it is there. Every API rule on the collection stays null, so
only the server's admin client can read or write these rows.
The collection holds no passwords and no live links: only the SHA-256 hash of each token. A row lifted from the database cannot be turned back into a working link.
Moving to megainfra.co.za later
Until the domain is verified, mail from support@apphub.co.za may land in
spam or be rejected. To send as your own domain:
- Verify the domain in Resend. Resend → Domains → Add Domain. Use
megainfra.co.za, or a sub-domain such asmail.megainfra.co.zaif the root domain is used for something else. - Add the DNS records Resend shows you (SPF, DKIM, and a DMARC record if offered) to the DNS zone for the domain. Wait until Resend marks the domain as verified — usually minutes, sometimes longer if the zone is slow.
- Change
EMAIL_FROMto the verified sender, for exampleMEGA Dashboard <no-reply@megainfra.co.za>, and restart the app.
Keep EMAIL_FROM on a domain you have actually verified. Sending from an
unverified domain is the usual reason mail disappears.
What the user sees
- Welcome — a greeting, what the dashboard (or the clock app, for a clock user) is for, a "Choose your password" button, three short getting-started steps, and the plain link for mail clients that will not show a button. The link is valid for 7 days.
- Reset — a "Reset password" button, valid for 1 hour, and a line telling them to ignore the mail if they did not ask for it.
Both pages also work by hand: /forgot-password asks for the email and
/set-password?token=… sets the new password. The forgot-password route
always answers the same way, so it never reveals whether an address has an
account.