Skip to content

MSC4306: Thread Subscriptions #4306

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 207 additions & 0 deletions proposals/4306-thread-subscriptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
MSC4306: Thread Subscriptions
=====

## Background and Summary

Threads were introduced in [version 1.4 of the Matrix Specification](https://spec.matrix.org/v1.13/changelog/v1.4/) as a way to isolate conversations in a room, making it easier for users to track specific conversations that they care about (and ignore those that they do not).

Thus far, there has been no good way for users to ensure that they see new messages in only the threads that they care about. The current rules only allow expressing one of two choices: users and their clients can either watch all threads, or ignore all threads.
As a result, the user is either being presented with unwanted information (misusing their attention), or they risk missing important messages.

This proposal introduces a new mechanism, Thread Subscriptions, as a first-class way for users to signal which threads they care about.

We then add a new push rule so that users can receive push notifications only for threads that they care about.


## Proposal

This proposal consists of three main parts:
- 3 simple endpoints for subscribing to and unsubscribing from threads;
- new prescribed client behaviour when the user is mentioned in a thread; and
- a new push rule (including a new push rule condition) that prevents notifying about threads that the user has not subscribed to.


### New Endpoints
Copy link
Contributor

@MadLittleMods MadLittleMods Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't there an endpoint to paginate all thread subscriptions in a room? (my expectation)

Similar to how clients back-paginate with messages in a room by starting with /sync and then using /messages to get the rest as necessary.


#### Subscribing to a thread

```
PUT /_matrix/client/v1/rooms/{roomId}/thread/{eventId}/subscription
```

with request body:

```jsonc
{
// Whether the subscription was made automatically
// by a client, not by manual user choice.
"automatic": true
}
```

Returns 200 with empty body `{}`.

If the thread does not exist, or if the user does not have access to it, returns 404/`M_NOT_FOUND`.

If the thread is already subscribed, then the subscription remains and:

- if `automatic` is `false`, the thread is marked as manually-subscribed, even if the existing subscription has it marked as automatically subscribed. (In other words, manual subscriptions take precedence over automatic subscriptions.)


#### Unsubscribing from a thread
```
DELETE /_matrix/client/v1/rooms/{roomId}/thread/{eventId}/subscription
```

Returns 200 with empty body `{}`.

If the thread was not subscribed, returns 200 with empty body `{}` for idempotence.

If the thread does not exist, or is inaccessible, returns 404/`M_NOT_FOUND`.


#### Retrieving thread subscription

```
GET /_matrix/client/v1/rooms/{roomId}/thread/{eventId}/subscription
{"automatic": true}
```

Retrieves the thread subscription for a given thread.

On success (200), returns:

```jsonc
{
// Whether the subscription was automatically made or not
"automatic": true
}
```

If there is no subscription to that thread, or the thread does not exist, or the thread is inaccessible, returns 404/`M_NOT_FOUND`.


### New Client Behaviour: subscribe on mention

When a user is mentioned in a thread (by another user — the *mentioning user*), the user's client should perform an automatic subscription to that thread using `PUT /subscription` with `{"automatic": true}`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think some interactions can lead to "stuck" subscriptions now 🙃

Imagine the following scenario.

  • On device 1, I have a thread containing events E1, E2 (which mentions me), and E3.
  • Because of E2, my client subscribed me automatically to the thread.
  • Now, I decide that I don't want to be subscribed to the thread, so I manually unsubscribe from it.
  • I open device 2, and back-paginate the thread there as well.

I think that, on device 2, because there's no data related to the thread subscription anymore, when device 2 will see E2 for the first time, it will automatically re-subscribe me to it.

Am I missing something that would prevent this stuck subscription behavior?

Ok, if I'm not mistaken, then. One feature that we'd like to allow, is re-subscription after a thread has been unsubscribed. That is, in my example, if E4 mentions me again, my client should be able to subscribe me again.

I thought we could use some kind of conceptual enum with 3 variants:

  • subscribed
  • unsubscribed
  • (nothing)

Unsubscribing would lead to the unsubscribed state, instead of deleting the subscribed state and replacing it with nothing. Then, we'd need a rule like "if unsubscribed, don't subscribe me again".

But… that doesn't work either 🫠 When E4 comes, which mentions me, my client might want to re-subscribe me to the thread. If we have a rule like "if unsubscribed, don't ever subscribe me ever again", then I can't be re-subscribed. The real information is "I'm unsubscribed until E3 included"; all events prior to it should not lead to a new automatic subscription, and all events after it should automatically resubscribe me. As a matter of fact, I think including the event id in the unsubscribe variant should be sufficient…

…Now, if you're paying attention, I've said "events" and "after" in the same sentence. I'm talking about event ordering here, hence I'm afraid we might run into the "stuck notification" problem, because the ordering may be different if events have been received from sync ("stream" ordering) or from /relations (topological ordering). Here are a few ideas, how to solve it:

  • maybe the "unsubscribe" event id could be… two event ids at most: one in topological ordering, one in sync ordering, and it's the client which determines which is last, in its local state. In this case, the server should accept multiple values at the same time, for the "unsubscribe" endpoint. That's an idea that was mentioned to solve the stuck read receipts dilemma in the past by @MadLittleMods if I recall correctly, so it could apply here too.
  • maybe we should enforce one ordering over the other, and say that this one ordering is the correct one. In that case, it must be the topological ordering, since the sync ordering doesn't match from one device to another. I'm afraid this would lead to the stuck notifications issue, though, unless clients agreed to only use /relations for filling the content of a thread.
  • MSC4033 gives some stable-ish event ordering, and it could be used for determining the order of subscriptions.
  • We could actually use the user's own read receipts as a proxy of "unsubscribed until this event": only event after the read receipt could cause an automatic re-subscription. This is messy though, as it implies a processing ordering dependency in clients: the client would need to process read receipts after it's processed thread subscriptions.

MSC4033 would honestly be the most satisfying, but it's been stuck for a while, there's been valid objections in it, etc. I would be curious to hear other people's ideas or thoughts about the problem. Am I missing something here?

Copy link
Member

@giomfo giomfo Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the need to introduce a new variant unsubscribed to have the possibility to differentiate undefined with unsubscribed.

Then to keep thread subscriptions as simple as possible. I would suggest to disable automatic subscription in a thread which has been manually unsubscribed. I will receive the potential new mentions in this thread but this will not subscribe me again because I clearly unsubscribed myself from this thread

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I would manage differently manual and automatic unsubscription (if automatic is planned)

  • a manual unsubscription -> unsubscribed state (via a PUT)
  • an automatic unsubscription -> undefined state (via a DELETE)


The server does not perform this action on the client's behalf, principally because the server is not able to detect mentions in encrypted rooms.

If the client is already aware of the user being subscribed to the thread, then making a `PUT /subscription` request is not necessary.

If the mentioning user is banned, the automatic thread subscription should not occur.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does "banned" mean the sender is in the recipient's m.ignored_user_list?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant banned from the room, but the mentioning user getting ignored by the mentioned user also sounds like a good case to apply this!


#### Reversal of automatic subscriptions

If an automatic thread subscription occurs and the mentioning user is subsequently banned, then:

- the thread subscription should be reversed,
- provided that there aren't any other mentions by other, non-banned, users that would have caused the same automatic subscription.

When a client becomes aware of a banned user in a room, it may need to backpaginate thread history to determine whether there are any threads whose automatic subscriptions should be reversed, or to determine if there are any other qualifying mentions that would obviate the need to reverse automatic subscriptions.

For efficiency reasons, clients may limit the depth of this backpagination with an implementation-defined recency limit, owing to the observation that abuse is usually cleaned up shortly after it occurs.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds like it might either be expensive or leave dangling subscriptions. If the server exposed an API to list all thread subscriptions in the room, clients could use /relations to only walk the subscribed threads which might be more efficient?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea would be that through MSC4308 (or another mechanism) the client will already know its active subscriptions, so indeed it can just walk the (recently- and automatically-)subscribed threads.

However it's hard to give a general definition of 'time' to describe how much the client should consider, so it's left purposefully flexible here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make optional this Reversal of automatic subscriptions. This add complexity client side for a low gain.

This is relevant to not subscribe if the mentioning user has been banned or ignored before the client takes into account the mention to set up the subscription.

I would let the user reverse manually the automatically subscribed thread if any


### New Push Rules

As motivation, we want threads to have the following notification semantics:

- Messages in unsubscribed threads should not count as activity at all; as a user, I do not want to see the room as unread because there are new messages in an unsubscribed thread.
- Exceptions: if the user is mentioned, this should generate a notification as usual. (The push notification thus generated is also useful for the client to realise it needs to create an automatic thread subscription.)
- Messages in subscribed threads should always count as a notification, and the (effective) room notification settings should not matter at all. E.g. the room can be muted, but if I, as a user, am subscribed to a thread, I still want to get a notification for new messages in that thread. If I do not want that, then I will unsubscribe.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An API listing active subscriptions per room might also make unsubscribing easier?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a user mutes a room, it puts an override rule in for that room. And this override rule is the highest priority

So this is not possible to receive notification from a thread in a muted room.
The manual subscription should be disabled in this room at the UI level until the end user unmutes the room.
When a room is muted, the client should pursue the automatic thread subscriptions. These subscriptions will be ignored until the room is unmuted Keep managing the automatic subscription eases the potential switch mute/unmute of a room. The client UI should let the end user know any potential thread subscription is disabled in a muted room


To achieve this, we propose the addition of two new push rules:

1. an `override` push rule, called `.m.rule.unsubscribed_thread`, at the end of the override list. This rule causes events in unsubscribed threads to skip notification processing without generating a notification.
The rule occurs after mention-specific rules, meaning that mentions continue to generate notifications.
**TODO how do we deal with keyword mentions**
```jsonc
{
"rule_id": ".m.rule.unsubscribed_thread",
"default": true,
"enabled": true,
"conditions": [
{
"kind": "thread_subscription",
"subscribed": false
}
],
"actions": []
}
```
2. an `underride` push rule, called `.m.rule.subscribed_thread`, at the beginning of the underride list. This rule causes events in subscribed threads to generate notifications.
```jsonc
{
"rule_id": ".m.rule.subscribed_thread",
"default": true,
"enabled": true,
"conditions": [
{
"kind": "thread_subscription",
"subscribed": true
}
],
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
}
]
}
```

These push rules use a new push condition `thread_subscription`, which takes an argument `subscribed` (boolean).
The `thread_subscription` push condition is satisfied if and only if all the following hold:
1. the event, which we are running push rules for, is part of a thread.
2. the user is subscribed to the thread (`subscribed` = `true`) or is not subscribed to the thread (`subscribed` = `false`).

## Limitations

- Users will not have enough granularity to subscribe to threads in a way that lets them keep track of threads (being able to 'catch up' through some mechanism in their client) without also getting notifications for them, except by disabling ALL thread subscription notifications altogether.
- There is precedent for this granularity in the popular forum software *Discourse*, but the author is not aware of Instant Messaging software with this granularity.
- With that said, this could be feasibly extended by a later MSC with no apparent issues.

## Alternatives
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm kind of surprised there's no changes to the /threads API?


- Clients could maintain thread subscription settings in Room Account Data, as a map from event ID to the subscription settings.
- This would be inefficient by requiring the entire subscription set for an entire room to be transferred at once, for example in Sliding Sync.
- This would make subscriptions vulnerable to Read-Modify-Write race conditions (though this could be addressed with extensions to the Room Account Data APIs).
Comment on lines +168 to +170
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My hope was you could use (room) account data, but that doesn't have a "state key" or anything. I think it also limits you in size, which probably wouldn't work. 😢

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was also thinking of account data and I believe it isn't subject to the 64KiB limit?

What were you planning to use a state key for with thread subscriptions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using account data in this way may not have the 64 kiB limit, but ideally we should try to avoid using it in more places that will make it hard to add that limit. (Since the lack of a limit is a mistake, I believe.)

The two problems I noted above are the main reasons I don't like using account data here:

  • The read-modify-write issue
  • Clients and servers having to operate on whole lists when they only care about one tiny portion of it.

I think Patrick's 'state key' mention is along the lines of: 'we only have a single key for each piece of room account data: the type'.
You could imagine using having an extra key, so room account data could be keyed on (m.thread.subscription, $abc123def456).

Or we could stretch things further to have 'Event Account Data' or something.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Patrick's 'state key' mention is along the lines of: 'we only have a single key for each piece of room account data: the type'.
You could imagine using having an extra key, so room account data could be keyed on (m.thread.subscription, $abc123def456).

Oh, I see.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the lack of a limit is a mistake, I believe.

Correct.

And your example of a state keys is spot on for what I was thinking.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if expanding this to "relation subscriptions" would be helpful for any other use-cases. So a subscription is parent event+relation type?

Might not be super useful with the current relations though.

- Clients could maintain their thread subscription settings in their global Account Data, but this seems to be strictly worse than doing so in Room Account Data.

## Security considerations

- Abuse of 'subscribe on mention', particularly in public rooms
- Malicious users can be ignored by the user, to stop the subscribe on mention behaviour.
- Users can disable notifications entirely for rooms.
- Room moderators can take action against malicious users abusing the feature.
- If no room moderators are protecting the user, the user can of course also leave the room.

## Notes to Client Implementors

## Notes to Server Implementors

- Since clients will be automatically updating the Thread Subscription Settings when their user is mentioned, server implementations should be ready to handle concurrent updates by multiple of the user's devices/clients when they are online at the same time.

## Unstable prefix

Whilst this proposal is unstable, the following changes should be noted:

- the endpoint is renamed to `/_matrix/client/unstable/io.element.msc4306/rooms/{roomId}/thread/{eventId}/subscription`
- the push rules' IDs are renamed to:
- `.io.element.msc4306.rule.unsubscribed_thread`
- `.io.element.msc4306.rule.subscribed_thread`
- the push rule condition `kind` is renamed to:
- `io.element.msc4306.thread_subscription`


## Dependencies

- no dependencies on other pending MSCs

## Dependents

This proposal is known to be depended upon by the following MSCs:

- [MSC4308: Thread Subscriptions extension to Sliding Sync](https://github.com/matrix-org/matrix-spec-proposals/blob/rei/msc_ssext_threadsubs/proposals/4308-sliding-sync-ext-thread-subscriptions.md)