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.
Three ways to point it somewhere
Section titled “Three ways to point it somewhere”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.
Announcements
Section titled “Announcements”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
#newsis core’s, and needs the bot below. - A module may add its own. The
uomodule 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.
The Discord bot
Section titled “The Discord bot”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.
Push notifications
Section titled “Push notifications”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.
On-site notifications
Section titled “On-site notifications”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.
Who receives what
Section titled “Who receives what”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.