Feature Overview
A "Server Announcements" feature that allows any Synapse admin to send a message to every user on their own homeserver simultaneously. This message would appear in a new room that's added to every user's room list with the new, and any past, homeserver announcements in it. The new room should be visually distinguished in the client to have it appear "official".
The room's attributes (name, avatar, topic) can be configured in homeserver.yaml:
# Allow for multiple server announcements rooms to be defined.
server_announcements:
# Used to identify the announcement room when sending events in it.
- announcement_room_ref: some_unique_string
room_name: "Server Announcements"
room_topic: "Room used by your server admin to notify you of important information"
# The users that may be part of the room and send announcements.
allowed_user_ids:
- "@server:example.org"
# Mutually exclusive options.
room_avatar_url: "mxc://example.com/oumMVlgDnLYFaPVkExemNVVZ"
room_avatar_path: "/home/user/media/server_notices_room_avatar.png"
# Another room, if desired.
- ...
*_path variants are provided for the user and room avatar so that the room can be set up entirely via deterministic config. This makes deploying Synapse in k8s, like ESS, easier. As otherwise you'd need a separate script to run after the homeserver is up, upload some media, edit the homserver.yaml with the new MXC URI, and then restart the homeserver.
The justification for multiple server announcements rooms is that you may want to announce things to users in different contexts. Perhaps "product updates" is one, "server issues/maintenance/etc." is another, and so on.
Sending a message
The above configuration allows a server admin to create and configure a server announcements room. To actually send an announcement, one can simply send an event into the room using the existing PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId} CS API.
PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}
{
/* `content` of the new event */
}
The new event will be sent into the room, and picked up by other users on their next /sync.
Re-using the existing Client-Server API allows one to use existing clients to send events, as well as send any type of event (not just text messages). It may be useful to send a server-wide poll, for instance.
To retrieve the room ID of one of these announcement rooms, a new Admin API is created:
GET /_synapse/admin/v1/server_announcement/get_room/{announcement_room_ref}
{"room_id": "!announcementroomid"}
Existing "Server Notices" feature
Synapse already has a server notices feature which allows a homeserver admin to send a message to a single user on the homeserver from a server account. This is good for sending targeted messages to individual users.
Matrix.org paid plans ("Golden Eagle") has made use of this feature to send targeted messages to users about their usage limits.
Previous work
I took a stab at implementing a proof-of-concept for this feature in Synapse in Element's internal "how hard can it be be?" hackathon in March 2026: https://github.com/element-hq/how-hard-can-it-be-Mar-26/issues/30.
This resulted in a working PoC that had admins create an announcement room via the Admin API:
GET /rooms/announcement_room
{}
{"room_id": "!announcement_room:example.org"}
POST /rooms/announcement_room
{}
// Creates a new announcement room with the appropriate power levels,
// and sets it as the announcement room in the DB.
{"room_id": "!announcement_room:example.org", "created": true}
The room would then appear in that user's room list. From there, sending messages was as simple as sending a message into the room in their matrix client.
While this UX is quite nice, it does result in the events being created by this particular user. That means users see the announcement from "Jane Doe", rather than, say, "Official Example.org Updates".
Technical details
Configuring the room
On startup, Synapse should check whether the configured announcement rooms exist, and their configuration matches their room state. If not, it should adjust them (update room name/avatar) if necessary.
Server announcement rooms must have m.federate set to false.
Syncing events/messages
Rather than a new stream or anything more complicated, we can take advantage of the existing events stream and simply create a real room that announcement messages are sent into.
Then, when a user calls /sync (sliding and legacy), we inject this room ID into the list of rooms that the sync handler should pull messages from for this user. This ensures that only new messages are sent down sync.
This method also has the advantage of only making users aware of the announcements room after the first message has been sent.
Fake join events
For a client to actually show the room in the room list, it will need to receive a m.room.member join event for the user in that room. However, this event won't actually exist (we don't actually want to join users to the room).
Every time an announcement message should be included in a user's /sync stream, we also inject a fake join event for that user. Doing it every time means we don't need to keep track of the first time a user received an event from the announcements room. And clients won't mind receiving multiple join events.
The event ID is computed from a deterministic set of inputs (room ID and user ID), and thus is the same every time. Therefore clients should just ignore subsequent deliveries of the same event.
Injecting room state events
The room's name, avatar, topic etc. are stored as state events, and these need to be delivered to clients as well. These will also have to be injected every time an announcement event is due to be sync'd down to a client. Considering server notices are likely to only be sent very occasionally, this is an acceptable bandwidth trade-off.
Since the event IDs will be static (they're real events), clients will also ignore duplicate deliveries.
Initial sync optimisation?
To save ourselves from needing to inject these events every time a message is sent in the room, we can instead inject them during initial sync. This will preload clients with the state of the announcements room. Then, during incremental /sync, we need only have the new messages injected.
However, this optimisation breaks down for sessions that exist before this feature was enabled. They won't initial sync after the announcements room is created, and thus the announcements room wouldn't have any relevant state once the first message reached their client.
You'd have the same problem if the announcement room were ever upgraded.
Allowing users to view historical announcements
Since most users on the homeserver aren't actually in the room, we need to set the room's history_visibility to world_readable. That will allow any user to call /messages on the room to get historical events.
Power levels
The power levels of the room should be restricted such that only the server notices user may send events. The PoC set the following power levels for the announcement room:
{
"power_level_content_override": {
"ban": 50,
"events_default": 50,
"invite": 50,
"kick": 50,
"redact": 50,
"state_default": 50,
"users_default": 0,
}
}
Push notifications
Announcements may contain important information for users, and thus it'd be ideal to send a push notification every time one is sent out. This can be done by updating the push notification evaluation logic to always assume the user is part of an announcement room.
However, sending a push notification to every user on matrix.org will undoubtedly lead to a meltdown. So we'll need some way to "smear" or "rate-limit" these notifications over a period of time. Perhaps we could implement some sort of rate-limiting for notifications in general? However, we should take care not to block notifications for everyone behind millions that need to be sent for the announcements room. Perhaps we have a per-room lineariser?
Preventing announcement room impersonation
Any announcement room should have the m.server_notice room tag set in each user's room account data.
Unfortunately, we may need to place both a check and set on the hot path (/sync) whenever an announcement is set for each user. Updated since this comment.
Any announcement room should use the m.server_announcement room type (MSC pending). Users should not be able to create these rooms via /createRoom. Instead, these rooms can only be created via the Admin API (when first sending an announcement into that room). This ensures that they are created with the correct permissions, history visibility settings, join rules, etc.
Clients should differentiate announcement rooms from other rooms, to prevent someone from creating a room simply named "Example.org Announcements" and inviting unsuspecting users to it.
Errors
The power levels of the room should prevent users from having the option to send an event into the room. If clients hide other options, such as sending reactions, starting threads, etc. on announcement rooms then there will be few situations where a client will generate an error.
But if a client tries to send a message into the room, they're receive a generic "403 - you are not in this room" or something to that effect. This is considered out of scope, given that clients should try to hide this functionality anyways.
This may be revisited if it turns out that it's hard to block interaction with the room in the client, as otherwise it's not too difficult to have a check in the handler of various APIs to see if a user is trying to write to an announcement room outside of the Admin API.
Spec
The above implementation plan will require a new MSC to define the new m.server_announcement room type.
Open questions
- How to handle room upgrades?
Time-tracking
The internal-to-Element time-tracking code for this work is ER-341. Please track against this when writing or reviewing code for this project.
Feature Overview
A "Server Announcements" feature that allows any Synapse admin to send a message to every user on their own homeserver simultaneously. This message would appear in a new room that's added to every user's room list with the new, and any past, homeserver announcements in it. The new room should be visually distinguished in the client to have it appear "official".
The room's attributes (name, avatar, topic) can be configured in
homeserver.yaml:*_pathvariants are provided for the user and room avatar so that the room can be set up entirely via deterministic config. This makes deploying Synapse in k8s, like ESS, easier. As otherwise you'd need a separate script to run after the homeserver is up, upload some media, edit thehomserver.yamlwith the new MXC URI, and then restart the homeserver.The justification for multiple server announcements rooms is that you may want to announce things to users in different contexts. Perhaps "product updates" is one, "server issues/maintenance/etc." is another, and so on.
Sending a message
The above configuration allows a server admin to create and configure a server announcements room. To actually send an announcement, one can simply send an event into the room using the existing
PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}CS API.The new event will be sent into the room, and picked up by other users on their next
/sync.Re-using the existing Client-Server API allows one to use existing clients to send events, as well as send any type of event (not just text messages). It may be useful to send a server-wide poll, for instance.
To retrieve the room ID of one of these announcement rooms, a new Admin API is created:
Existing "Server Notices" feature
Synapse already has a server notices feature which allows a homeserver admin to send a message to a single user on the homeserver from a server account. This is good for sending targeted messages to individual users.
Matrix.org paid plans ("Golden Eagle") has made use of this feature to send targeted messages to users about their usage limits.
Previous work
I took a stab at implementing a proof-of-concept for this feature in Synapse in Element's internal "how hard can it be be?" hackathon in March 2026: https://github.com/element-hq/how-hard-can-it-be-Mar-26/issues/30.
This resulted in a working PoC that had admins create an announcement room via the Admin API:
The room would then appear in that user's room list. From there, sending messages was as simple as sending a message into the room in their matrix client.
While this UX is quite nice, it does result in the events being created by this particular user. That means users see the announcement from "Jane Doe", rather than, say, "Official Example.org Updates".
Technical details
Configuring the room
On startup, Synapse should check whether the configured announcement rooms exist, and their configuration matches their room state. If not, it should adjust them (update room name/avatar) if necessary.
Server announcement rooms must have
m.federateset tofalse.Syncing events/messages
Rather than a new stream or anything more complicated, we can take advantage of the existing
eventsstream and simply create a real room that announcement messages are sent into.Then, when a user calls
/sync(sliding and legacy), we inject this room ID into the list of rooms that the sync handler should pull messages from for this user. This ensures that only new messages are sent down sync.This method also has the advantage of only making users aware of the announcements room after the first message has been sent.
Fake join events
For a client to actually show the room in the room list, it will need to receive a
m.room.memberjoin event for the user in that room. However, this event won't actually exist (we don't actually want to join users to the room).Every time an announcement message should be included in a user's
/syncstream, we also inject a fake join event for that user. Doing it every time means we don't need to keep track of the first time a user received an event from the announcements room. And clients won't mind receiving multiple join events.The event ID is computed from a deterministic set of inputs (room ID and user ID), and thus is the same every time. Therefore clients should just ignore subsequent deliveries of the same event.
Injecting room state events
The room's name, avatar, topic etc. are stored as state events, and these need to be delivered to clients as well. These will also have to be injected every time an announcement event is due to be sync'd down to a client. Considering server notices are likely to only be sent very occasionally, this is an acceptable bandwidth trade-off.
Since the event IDs will be static (they're real events), clients will also ignore duplicate deliveries.
Initial sync optimisation?
To save ourselves from needing to inject these events every time a message is sent in the room, we can instead inject them during initial sync. This will preload clients with the state of the announcements room. Then, during incremental
/sync, we need only have the new messages injected.However, this optimisation breaks down for sessions that exist before this feature was enabled. They won't initial sync after the announcements room is created, and thus the announcements room wouldn't have any relevant state once the first message reached their client.
You'd have the same problem if the announcement room were ever upgraded.
Allowing users to view historical announcements
Since most users on the homeserver aren't actually in the room, we need to set the room's
history_visibilitytoworld_readable. That will allow any user to call/messageson the room to get historical events.Power levels
The power levels of the room should be restricted such that only the server notices user may send events. The PoC set the following power levels for the announcement room:
{ "power_level_content_override": { "ban": 50, "events_default": 50, "invite": 50, "kick": 50, "redact": 50, "state_default": 50, "users_default": 0, } }Push notifications
Announcements may contain important information for users, and thus it'd be ideal to send a push notification every time one is sent out. This can be done by updating the push notification evaluation logic to always assume the user is part of an announcement room.
However, sending a push notification to every user on matrix.org will undoubtedly lead to a meltdown. So we'll need some way to "smear" or "rate-limit" these notifications over a period of time. Perhaps we could implement some sort of rate-limiting for notifications in general? However, we should take care not to block notifications for everyone behind millions that need to be sent for the announcements room. Perhaps we have a per-room lineariser?
Preventing announcement room impersonation
Any announcement room should have them.server_noticeroom tag set in each user's room account data.Unfortunately, we may need to place both a check and set on the hot path (Updated since this comment./sync) whenever an announcement is set for each user.Any announcement room should use the
m.server_announcementroom type (MSC pending). Users should not be able to create these rooms via/createRoom. Instead, these rooms can only be created via the Admin API (when first sending an announcement into that room). This ensures that they are created with the correct permissions, history visibility settings, join rules, etc.Clients should differentiate announcement rooms from other rooms, to prevent someone from creating a room simply named "Example.org Announcements" and inviting unsuspecting users to it.
Errors
The power levels of the room should prevent users from having the option to send an event into the room. If clients hide other options, such as sending reactions, starting threads, etc. on announcement rooms then there will be few situations where a client will generate an error.
But if a client tries to send a message into the room, they're receive a generic "403 - you are not in this room" or something to that effect. This is considered out of scope, given that clients should try to hide this functionality anyways.
This may be revisited if it turns out that it's hard to block interaction with the room in the client, as otherwise it's not too difficult to have a check in the handler of various APIs to see if a user is trying to write to an announcement room outside of the Admin API.
Spec
The above implementation plan will require a new MSC to define the new
m.server_announcementroom type.Open questions
Time-tracking
The internal-to-Element time-tracking code for this work is
ER-341. Please track against this when writing or reviewing code for this project.