Skip to content

Notifications and email

Four separate delivery paths, each optional, each off until you configure it. A site that configures none of them still works — it just never reaches anyone who is not looking at it.

This page is about the paths themselves. What decides that a particular message gets sent down one of them is a rule — see Engagement rules.

Admin → Settings → Email delivery. The site sends contact-form messages, invitations, password resets, team notifications and test messages through SMTP. Contact-form mail goes to the Contact email setting.

You pick a mail transport and fill in the fields it asks for. There is no consent flow and no redirect to bounce through — it is a form, and the credentials go straight into the database encrypted at rest, write-only: the panel will tell you a password is set, and will never show it to you again.

Any SMTP server works. Which one you should use depends on how much mail you expect to send.

A relay — the recommended one. Mailgun, SES, Postmark or equivalent: their host, port 587, Implicit TLS off, and your API key as the password. Deliverability is the hard part of sending mail — reputation, DKIM, bounce handling — and this is the option where somebody else owns it. Use this for anything with real volume.

A mailbox provider over SMTP — the simplest. For example smtp.gmail.com, port 587, Implicit TLS off, your address as the username, and an app password — not your account password, and it requires 2-Step Verification to be on. Fine for a small site; subject to the provider’s daily send caps.

Your own MTA. If you already run mail on the same host: its address, port 25, Implicit TLS off, username and password blank. The site treats a username with no password as incomplete, since that authenticates as nobody.

Until a transport is configured, the contact form falls back to a mailto: link to the contact address — which works, and puts the message in the visitor’s own mail client rather than in your logs. Invitations surface a copyable accept link instead, and password resets still answer normally.

Publishing a news post fans it out to every registered delivery leg. The dispatcher is an in-process poller, tuned by ANNOUNCE_POLL_MS (15 seconds by default), and the links in an announcement are built from APP_BASE_URL — so set that in production or the links point at the wrong host.

Which legs exist depends on what has registered one:

  • Discord #news is core’s, and needs the bot below.
  • A module may add its own. The uo module adds an in-game town crier, so a news post is announced to players who are logged into the game and never visit the site.

A leg brings its own settings with it — the town crier’s duration is a module setting, not a core one — which is why they are documented with the module rather than here.

Admin → Discord Bot: enable it, give it the guild (server) ID and the bot token, and save. The token is stored encrypted in the database and is never an environment variable.

The bot is a separate container. On the quickstart deployment from Install the site it is not running at all, and the panel says so — bot unreachable is the honest state of a site that never started one, not a failure. Add the bot service from the project’s shipped Compose file when you want it.

What it does once connected: posts announcements, captures the moderation events on the Moderation screen, serves slash commands, and — if you switch them on — the Team notification bridge and per-Team voice channels from Teams.

Opt-in push to the Android app, over a self-hosted ntfy relay — the ntfy service in the project’s Compose file, plus NTFY_BASE_URL and friends.

Two properties matter for what you have to trust:

  • The relay only ever carries a content-free tickle. The message says something happened; the app then fetches the actual content from the site over its own authenticated connection. So the relay never sees notification text.
  • A device may only register an endpoint on an allowed origin, derived from NTFY_BASE_URL. That is what stops a device pointing your server at somebody else’s.

Without NTFY_PUBLIC_URL / NTFY_ALLOWED_ORIGINS, the app simply shows push as unavailable for your instance — nothing breaks.

A tickle raised by an engagement rule carries a pointer to the matching item in the on-site inbox where there is one, so the app opens on the thing that happened rather than on a list. It is still only a pointer: the content is fetched, not delivered.

The third way to reach somebody, and the only one that needs no relay, no mailbox and no app: an item in their notification inbox on the site itself. A bell in the header carries the unread count; the list lives at Account → Notifications.

Two things are worth knowing before you enable a rule that uses it:

  • It is the one channel that is on by default. Push and email are opt-in — both reach somebody somewhere else, so both have to be asked for. An inbox item is a row on a page the person chose to open, so it is opt-out: they switch it off per notification under Account → Notifications → Settings.
  • The body is plain text, always. The in-app template renders through the same block editor as your mail, but only the text of each block is stored, so nothing an operator writes can become markup on somebody else’s page. Links are site-relative or dropped.

Old, read items are pruned nightly (90 days by default). Unread items are never pruned — an inbox that quietly deleted things nobody had seen would make the unread badge meaningless.

The per-person side of this lives in the player portal, not the admin panel: Account → Notifications → Settings is a grid of every notification against every channel, and each member sets their own. The defaults are not symmetrical, and the asymmetry is deliberate:

  • Email is opt-in.
  • Push is opt-in.
  • On the site is opt-out — see above.
  • Muting a Team silences all three for that Team, whatever the grid says, without touching any of their other Teams.

The operator’s side of the same question — which events exist, and how wide an audience each one may ever be given — is Engagement rules. When a message went nowhere and you want to know why, the send log there is the screen that says.