mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-05-09 08:26:00 +02:00
[GH-ISSUE #303] Update/delete notifications #237
Labels
No labels
ai-generated
android-app
android-app
android-app
🪲 bug
build
build
dependencies
docs
enhancement
enhancement
🔥 HOT
in-progress 🏃
ios
prio:low
prio:low
pull-request
question
🔒 security
server
server
unified-push
web-app
website
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
starred/ntfy#237
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @blacklight on GitHub (Jun 1, 2022).
Original GitHub issue: https://github.com/binwiederhier/ntfy/issues/303
A similar issue was open a while ago, but it didn't get traction for some reason.
The ability to update notifications is so far the only feature that forces me to still use AutoNotification and the Tasker environment instead of ntfy.
I have a multi-host and multi-room music setup, and whenever a new track is playing I receive an actionable notification that allows me to easily control the playback.
AutoNotification allows editing a notification given its ID (even a total overwrite/replace is fine, so we don't have to mess with the complexity of partial updates). And that's exactly the feature I need: if every music device created a new notification for each track that is played the notification bar will quickly get crammed, especially if there is no programmatic way to dismiss stale notifications. Notifications that report a progress status are another quite interesting use case that isn't possible right now.
@binwiederhier commented on GitHub (Jun 1, 2022):
I absolutely love the idea of updating notifications, and I've attempted to implement it twice. It's a little tricky to implement but doable.
The reason it didn't get traction was because I hated the API I came up with, and on Discord nobody had any better ideas. I want the API to be simple and ideally the request to update a message is the same that it is to initially publish it. That's what's tricky about it.
Related:
https://github.com/binwiederhier/ntfy/pull/259
#254
#187
#43
@binwiederhier commented on GitHub (Jun 1, 2022):
There's also #44 for progress. That is trivial to implement once messages can be updated.
@blacklight commented on GitHub (Jun 2, 2022):
Something like
POST/PUTwithout anidto create a notification andPUTwith anidto upsert?idcould then be passed either on the query string or the JSON payload.@KawaiiZapic commented on GitHub (Jun 25, 2022):
It's great to have this feature.
we can set an id(or type) for a message, and replace the old message when new message come if id is same.
Or can we support conversation type message on Android?

It is useful to push IRC message to phone in a website only IRC channel.
@mrherman commented on GitHub (Sep 13, 2022):
For the API, we could consider something similar to Home Assistants companion app's notification replace/clear system. https://companion.home-assistant.io/docs/notifications/notifications-basic/#replacing For that, the user/client provides a tag/id name for the notifications. On the receiver side, if no notification with that tag exists then it creates it, otherwise it deletes/replaces it. It is up to the user to make the tag super unique or generic depending on their use case.
Just to add, I would really like this feature too. I pop up an image of the outdoor cam when doorbell is rang, but it is only relevant until no motion is detected (for me to decide to answer it or not), after that I would like to clear the notification. Also, sticky notification (if we get them) that has some data that is periodically updated, I can update the notification when the data is changed.
@julianlam commented on GitHub (Jan 25, 2023):
Another +1 for this suggestion, if I may 😄
We would use this in NodeBB for replacing notifications with updated text or to remove stale notifications. For example, a notification that a post needs review is no longer relevant if another admin has approved/denied it, and so the notification automatically gets rescinded in NodeBB.
We also replace notification text saying "X has posted a reply" to some variants like "X and 2 others have posted a reply", etc.
We use
nid, similar to @BlackLight's suggestion. We also usemergeIdto combine notifications, but I wouldn't expect that one to land in ntfy any time soon, if ever 😉@courtarro commented on GitHub (Feb 24, 2023):
+1
As discussed in previous comments, it seems like the standard in other notification architectures is to use an optional ID when creating a notification. If the ID of a new notification matches a previous notification, the previous notification is replaced with the new one. If we support an integer ID, the clients should be able to handle them also. If there is an errant client that can't handle it, it would just show all of the notifications without replacement.
Is there a reason we can't use an integer
idfield for this? What are the API concerns you have, @binwiederhier?Edit: I see that there's an internal
idfield already that is a random string. So then let's call oursexternal_idand it's an int.@spraot commented on GitHub (Mar 30, 2023):
If this feature is implemented I would happily switch over from Pushover.
@chovyy commented on GitHub (Sep 22, 2023):
I would realy love to have this. I often send status updates from my home automation, and it is quite annoying to have this stack of messages on the same issue.
@pnxs commented on GitHub (Nov 9, 2023):
I also really like this feature, I tried to understand what you hate about your API for updating messages but I had trouble to find what the API really looks like (I looked into #259 and #187).
You said, you want the API "to be simple and ideally the request to update a message is the same that it is to initially publish it.". This makes sense to me, because removing the message is more like adding the information, that the message is remove to an existing message. In some cases one would like to add an information or timestamp why this message was deleted.
Naively spoken: I just expect the message id to be returned from the POST, which enabled the publisher to update it's message if it want to.
To update the message it just do the same as publishing it originally, with the difference, that the message id is added to the POST.
E.g.:
First POST:
The Post will return the message Id "cdssFSfwt4fw"
Update of the message:
Remove the message:
The presenting application has not to be aware of updating messages. In the simplest case, every message is shown consecutively. But if the presenting application is aware it could replace the message on it's display, change colors or do whatever. Maybe also show a log of recently remove messages.
What do you think @binwiederhier ?
@genofire commented on GitHub (Nov 17, 2023):
i am for POST for create, PUT for update and DELETE for delete an message as http method (and not everything POST)
i also believe everything should be equal like before the POST (just the path contains the id and topic like
DELETE /topic/id), i think it is an bad idea to put any logic into the BODY of an request.@pnxs commented on GitHub (Nov 20, 2023):
@genofire
How to you handle the information about the time when the reason for generating the original message was omitted (e.g. the real deletion date)?
What do you mean with " i think it is an bad idea to put any logic into the BODY of an request"? I do not see any logic...
And sure you can use different Verbs, but I tried to make the API very simple, as wished by the author...
@mikebakke commented on GitHub (Nov 20, 2023):
+1 - driven by the use case where I have set a scheduled message but need to delete it before reaching the scheduled delivery time, to cancel it as not required or correct my speeiling mistakes...
@julianlam commented on GitHub (Nov 20, 2023):
@pnxs, @genofire means you should not just have a single POST endpoint and pass in an
actionparameter to denote whether to create, delete, or update... because those are already available if you use the proper http methods.@smashah commented on GitHub (Mar 31, 2024):
i thought this + progress bar notification would already be built in to ntfy. I think it should be implemented even if the api is awkward
@wunter8 commented on GitHub (Jun 28, 2024):
Copying my thoughts from Discord to here so they don't get lost:
(a) I propose adding a new field to the message object called
pid(persistent id). It will be a user-defined string ([A-Za-z0-9-_]+). You can include thepidin a header (-H "pid: garage") or in the JSON body ({"topic":"home","message":"The garage has been open for 10 minutes","pid":"garage"}).(b) Each revision to a notification (e.g., sending another message with "pid: garage") will be its own, individual message. It will have a different message id, db row id, timestamp, etc.. I think this will help maintain backwards compatibility since old clients can just receive each individual message, ignore the
pidfield, and show a notification for each revision. All the messages/revisions can be sorted based on timestamp, and?since=will work just as before. Updated clients can choose to combine/update notifications that have the samepidin their respective UIs.(c) Having each revision be its own message (with its own message id, etc.) makes it easy for a client to show the history of a message over time. And you don't run into issues with reinserting a modified message into the db to get a new id.
(d) I prefer this approach over updating a message based on its message id (e.g.,
POST /topic/$id) because this way you do not need to remember/keep track of a randomly generated message id; it would be nice to update a message using a key I define.(e) One of the use cases for this feature that was discussed in a Github issue was implementing a "progress" feature:
(e) Imagine this being replaced with actual progress updates of any long running command (e.g., file downloads, machine updates, machine learning model training, etc.). I could send progress updates right now using the current API (using the above example code), but I'd receive 10 different notifications. It'd be nice to be able to simply include a
pidto the above code and receive 1 updating notification instead of 10 individual notifications. (And old clients that haven't been updated yet (e.g., iOS 😬) would simply "fallback" to receiving 10 individual notifications).(f) Since every message update is its own message, rate limiting would be applied as if we were using the current/"old" system: every message or revision counts as 1 publish toward your limit; you cannot circumvent the limits by sending 1 message to a topic and "updating" the contents of that message 10 (20, 30, 100, etc.) times.
g) You mentioned that you weren't sure about anonymous users being able to update the text of any previous message. I think we can either add a new user permission to control "update" permissions (not my preference), or we can let any user that can publish to a topic update a notification with a "pid" in that topic. If we go with my approach of having each revision as its own message (and leaving it to the client to combine/update the messages in the UI), the history of edits will be tracked, so no information could be lost/overwritten. And it continues to "fallback" gracefully, since old clients will just depend on normal publish permissions instead of trying to act based on a new "update" permission. And any old, already cached notifications cannot be updated using the new feature since the old messages don't have a
pid(h) In general, this is mostly a proposal to update the clients. A new field will be added to the message object on the server, but that's it there. We don't need to add new endpoints (e.g., /topic/$id), rate-limiting code, permissions, etc.. We just add a user-specified identifier (
pid) to messages (on the backend), and then update each client to deal with the new field and use it to make their respective UI/notification systems a little cleaner/more convenient.(i) I know Android, iOS, and web allow updating the contents of a notification/replacing an existing notification with a new notification with different contents.
@goulf-3m commented on GitHub (Nov 24, 2024):
Please add this feature!
@ElectricTea commented on GitHub (Apr 2, 2025):
Would love to see the unpublishing scheduled messages PR https://github.com/binwiederhier/ntfy/pull/1142 merged if possible ❤
@binwiederhier commented on GitHub (May 26, 2025):
@wunter8 Your approach is sound and is very similar to what I would have suggested. We will likely need a
versionorrevisionfield to keep track of which is the newest message in the group.pid. We should another namemessage_group_idorrevision_id(rid).@wunter8 commented on GitHub (May 26, 2025):
versionorrevisionfield would just be an auto-incrementing number on the backend, right? So the server would increment it when a new message is received and then send the incremented field in the server response, but it'd never be provided or incremented by the user?revision_idcould be confused with therevisionfield. Are you okay withmessage_group_idand abbreviating it tomgid? If we usemessage_group_id, I think usingrevisionwould be fine.@mrherman commented on GitHub (May 26, 2025):
THe message group ID would be user defined right, what are the constraints,
any ASCII or alphanumeric field? Length? What happens if it is not set?
Just blank/null and that's treated as no mgid? I think that would give the
backwards compatibility desired.
This feature also requested delete notifications. I think deep down that
means remove them from the users current notifications, or atleast unread
notifications. I don't think it needs to mean delete from the database,
especially as the discussion above is just a series of updates/rows in the
database. I know I would want to remove a notification from my phone's
pull down notifications. Would there be a way to do that? I might propose
just an update with a blank/empty message? A client would need to know
that means don't create a new message, but remove the current. BUT, I
imagine some users might use blank messages with just a topic already, if
so this would be bad for them. An alternative would be an additional field
that could be set: dont_show, or remove or something similar? The client
would know to update the current mgid group by removing the notice of the
last message.
The use case for being able to remove a notification:
My system alerts me if the garage door is open for over 10 minutes, a
reminder so I could close it if needed.. Sometimes, it's just my kids and
they close it after 20 min. When the garage door finally closes I don't
need to pop up a notification on my phone anymore, the issue is resolved.
I pop up a ntfy when my door bell rings with a picture of who it is. I can
decide to answer the door with that info. But after 5 min it doesn't
matter if I saw it or not, they already left and I can remove the ntfy. (I
don't want a ntfy on my phone of all the people that have been at my door,
just who is there right now).
On Mon, May 26, 2025 at 12:20 PM wunter8 @.***> wrote:
@binwiederhier commented on GitHub (May 26, 2025):
Let's see what the proposal looks like. It's much easier to discuss when we have an actual proposal.
Use cases we want to support:
@wunter8 commented on GitHub (May 26, 2025):
For "logically deleting an existing message", are you hoping to support deleting messages that don't have a message_group_id (e.g., deleting a message based on its
idalone instead of based on itsmgid)?@binwiederhier commented on GitHub (May 26, 2025):
I am hoping for all the things :-D We could just assign an group id (uhhh,
update_id?) to every message, that can then be used to update it. Because it would be a bummer if you'd have to remember to pass the group id to update it later.@wunter8 commented on GitHub (May 26, 2025):
So allow a user to specify an
update_id(renaming frommessage_group_id), but if noupdate_id(uid) is provided, then we randomly generate one and send it to the user so they can delete the message later if they store that randomly generateduid?@binwiederhier commented on GitHub (May 26, 2025):
Something like that?! Or we just implicitly use the message id as the update id if you don't specify one. Idk :)
@mrherman commented on GitHub (May 26, 2025):
I don't think it is unreasonable to expect the user to be able to set and
remember a message group ID if they want to update and "logically remove" a
message group. If not set, something random if it's needed for the
database to work, or just null (and you can't send updates/remove).
On Mon, May 26, 2025 at 1:42 PM wunter8 @.***> wrote:
@p1gp1g commented on GitHub (May 28, 2025):
Web push has this exact feature: https://www.rfc-editor.org/rfc/rfc8030#section-5.4
It uses the
Topicheader. If you implement the feature, it would be great to use this header when?up=1, to bring it to UnifiedPush as well@wunter8 commented on GitHub (May 29, 2025):
Here's a
wall of textcarefully prepared proposal!DB Changes
getMostRecentRevisionByUid:SELECT revision FROM messages WHERE uid = ? ORDER BY revision DESC LIMIT 1;(used when inserting new message/revision into DB so we can add 1 to the revision)HTTP Request Examples
Initial message without a
uid.uidmade equal to randomly generatedidUpdate a message using
uidDeleting a message using
uid(revision is incremented, message body is cleared/set to empty string). The ntfy server does not let a user send an empty message body right now (if a user tries to do that, ntfy replaces the message with "triggered"), so I think it's safe to use an empty message body as an indication that the notification should be cleared.Updating a deleted message (same as previous update)
A message with a custom
uid(these all work with either POST or PUT)A message via GET with query params
Update via GET with query params
Scheduled Messages
I think this structure could also be used to support the "deadmanssnitch" feature #254 with the existing
at/delay/inheaders.Schedule a message with a delay
Postpone the message (same request, new message id in response, server removes previous message id from "scheduled" queue (by marking the message as already published? or by changing its
timefield tonow-1so the message would have already been sent?))Other Fields
Other fields (e.g., title, priority, tags, actions, attachments, etc.) can be added during any request (except for a DELETE). The new message will include the additional fields, like any other message would. If a first message includes a tag and a second message doesn't include the tag, the second message will appear on the client device without the tag from the first message.
Subscribe API
We probably need a new value for the subscribe API to only return the latest message for each
uidfrom the cache vs. returning all messages with the sameuidthat are in the cacheGet only latest revisions
Get all revisions (default for backwards compatibility. clients may only create a push notification for the latest message of each
uid, though)Questions
timeandexpiresfields instead of creating a new message with a unique id each time? This might be easier/nicer, but we would lose the history of messages (just in the server cache), if the messages happened to change over time. This might be fine@binwiederhier commented on GitHub (May 29, 2025):
(I just saw this; I will review tonight or tmr! Looks promising.)
@wunter8 commented on GitHub (Jun 1, 2025):
Thinking a bit more about the scheduled messages, I think we should only keep 1 scheduled message for a particular
uidat a time. So a first message can be scheduled withuid: reminder. Then, if we want to update that (push the reminder out in the future, change the reminder message, etc.), we can POST a new message withuid: reminderand whatever fields we want (message, delay, attachments, etc.). The previous revision would be removed from the DB (I'm thinking deleted entirely), and the new revision would be added/scheduled to be sent in the future.I think deleting entirely might be better than modifying it's timestamp field or published field because if we change the DB to show the message was published and don't send anything to the client (since it wasn't actually sent), if the client polls the server for all messages in the cache for a topic, they'd get a bunch of scheduled revisions that were never supposed to be sent.
If we delete the revisions, using the subscriber API, you could still check for scheduled messages on a particular topic, but you wouldn't be able to see a history of revisions for the scheduled message, only the most recent.
To delete/unschedule a future message, you'd
DELETE /topic?uid=reminder(or something likePOST /topic?uid=reminder&deleteif we decide to support that). The previously scheduled message would be removed from the DB and a new message with empty content would be put in its place. Or, when deleting scheduled messages, we could remove them from the DB and not replace it with a message with empty content since messages with empty content are specifically for clearing notifications on a client device. With the scheduled message, since it hasn't been sent yet, it won't be on the client device at all.However, I'm now considering the scenario when you send (assuming all these have the same
uid) messageAthen immediately send messageBwith a 10 minute delay. Then there are two messages in the cache for thatuid:Awhich has been published andBwhich hasn't been published. If you then sendDELETE /topic?uid=..., what should happen?Option A: the scheduled message is deleted/unscheduled
Option B: the published message A is cleared from the notifications on client devices
Option C: both A and B
Option D: we add a parameter to allow the user to choose when they send the DELETE request??
@wunter8 commented on GitHub (Jun 1, 2025):
I guess a similar problem would arise if they send
AthenBwith a delay, and then they send aPOST /topic?uid=...to update the message. Does that request update the contents of messageAon client devicea? The contents of (scheduled) messageBin the DB? Both? Option D from above?@gbansaghi commented on GitHub (Jun 1, 2025):
I think it makes the most sense to use DELETE and provide a webhook that can be used with GET (say,
/delete). This would mirror the current methods for sending messages (POST or PUT, or the/publishwebhook via GET) and could be extended to updating messages too. Something like:/publish(or/sendor/trigger)/update(or maybe the same as above?)/delete@wunter8 commented on GitHub (Jun 1, 2025):
@gbansaghi I like that idea. I don't think we'll distinguish between "send" and "update." I think they'll both just use the /publish endpoint, but we can add a /delete endpoint that requires a
uidas either a header or query param.@wunter8 commented on GitHub (Jun 1, 2025):
In response to my questions about scheduled messages, how about this:
To update/delete a sent message, you need to include
uidand CANNOT include a delay.To update/delete a scheduled message, you need to include
uidand MUST include a delay.For example:
Send message A and B with a delay:
Update message A on client devices:
Update scheduled message B in DB:
The 4th curl command above would postpone the scheduled message another 10 minutes. So the message would arrive 10 minutes after the 4th curl command and not 10 minutes after the 2nd curl command.
To avoid further delaying the scheduled message (e.g., to only update it's message contents), I think we need a new
delayvalue. We could do 0? Or any negative number? Or, since thedelayparam is a string that we parse into a duration, we could use a string like "keep"?@gbansaghi commented on GitHub (Jun 1, 2025):
Disclaimer: I am very new to ntfy, so it's likely that there's implementation details I'm not understanding fully. That being said, my approach would be:
If:
uiduiddoes not include a delaythen assume the user meant "I changed my mind, I don't want to show this until later" and
Essentially, a given
uidcan be either currently displayed to clients, or scheduled for later, but not both.There's probably terminology I'm misusing, but hopefully I've made the basic idea clear. Does this seem technically feasible?
@wunter8 commented on GitHub (Jun 1, 2025):
That would mean that you can never update a scheduled message after it's received, right? (or at least not until it has expired from the cache, which some people set to very long period (e.g., 1+ years)). I don't think we want to restrict
uidto "currently displayed or scheduled but not both."@gbansaghi commented on GitHub (Jun 1, 2025):
If my understanding is correct (and it may well not be), updating a scheduled message without a delay would add a revision that's not scheduled, and is therefore delivered immediately. If the message was already received, the client would see the matching
uidand change the existing notification. Otherwise, it would show up as a new message.@wunter8 commented on GitHub (Jun 1, 2025):
That part of your understanding is correct. Here's a bit more about the current ntfy architecture.
All messages are stored in the same cache table. Messages have a
timestampand apublishedboolean. When a no-delay message is received by the server, it puts it in the cache withtimestamp=now(), sends the message, and setspublished=1.When a delayed message is received by the server, it puts it in the cache with
timestamp=now()+delay. Then the server periodically (e.g., every second), checks the cache for any messages wheretimestamp<=now() && published==0and sends them.So, we cannot tell (with the current DB structure), which already-sent messages had delays and which were sent immediately upon receipt.
(I originally thought differentiating between past messages that had delays and those that didn't was necessary for your implementation, but I don't think it is anymore. I think your column headers could be changed to "message in cache scheduled for future" and "no message in cache scheduled for future." So even though you weren't proposing differentiating past messages, I think explaining the ntfy cache and scheduled message architecture is beneficial for this discussion, so I left the explanation above).
However, I'm focusing on the upper-right cell of your table. Under your implementation, if I send a message now, since it will be in the cache for 12 hours, during that time, I cannot schedule a future message (with the same
uid) since it would clear the current message. I don't think we want that. I think it should be possible to schedule a future message and keep the current revision visible. Especially since some people have a very long cache expiration time (e.g., 1+ year).That's what I was trying to convey before.
@wunter8 commented on GitHub (Jun 2, 2025):
While I was preparing the previous response, I realized that as soon as a message (e.g., revision 3 of
uid:reminder) expires from the cache, when the next message withuid:reminderis received, we won't have any idea what revision this is/should be. Should we impose a rule that messages can only be updated while at least one revision is in the cache?I think the alternative is to create a new DB table that forever tracks the revision counts for each
topic/uidpairing (e.g.,CREATE TABLE revisions(id primary key, topic TEXT, uid TEXT, revision INTEGER).The revision is intended to help the client know which message is the most recent for a particular
uid. Can we just rely on timestamps? I guess that wouldn't work if multiple messages/updates are sent in the same second.If we don't know the current revision number, I think we'd have to default to 1 (or 0?). When the client receives that new message, it wouldn't know to overwrite revision 3 (for example) with the new message (version 1) which was received after version 3 expired from the server cache.
I guess we could inform all the clients about the server's cache duration and making them calculate "this was received after revision 3 timestamp+cache duration, so this new message is actually supposed to be revision 4 instead of revision 1." But I don't love that idea
@gbansaghi commented on GitHub (Jun 2, 2025):
Thanks for the explanation, that was very helpful! I'm seeing the problem now with my proposal, and I do agree that keeping the current revision visible would be useful. In this case, I think updates should affect the latest revision, regardless if it's scheduled or delivered. So a scheduled message can be updated while the last delivered revision remains visible. If the user wishes to update the currently displayed message, they can send an update with
Delay: 0which would cause the message to be delivered immediately. Sending another message with a nonzero delay would create a new scheduled revision, which could be update without affecting the currently displayed message (at least until the current time reaches the delivery timestamp, or the update changes the timestamp to<=now()).Note: I'm assuming here that any headers missing from an update would mean "don't modify those values". I.e. if a server receives a message with, say,
tagandactions, for a messageuidthat hastag,delayandactions, then it will changetagandactions, but leavedelayas-is. If there's no revision for thatuidon the server, it would create a message withtagandactionsonly. Is that the idea?I think that's an implementation detail that users should not have to worry about. If an update is posted for an
uidfor which all revisions expired, ntfy creates the message as if it were new, resetting revision to 1 (or 0). Clients can disregard messages where the revision is less than what they currently have for thatuid, except if it's the initial value. In that case, assume the message being updated expired from the server, and update it.The only issue I see with this approach is this: let's say the client has revision 5 for a message that expired from the server. The server receives two updates in quick succession, so revision (let's say) 1 and 2 are sent to the client. If those arrive out of order (can then?), the client will not update the message to revision 2 (because it's less than 5, but not the initial value), then update it to revision 1 (because it is the initial value).
@wunter8 commented on GitHub (Jun 2, 2025):
In general, I have been imagining each message standing alone, especially for normal (non-scheduled) updates. Each message will be its own entry in the database (for backwards compatibility), so we won't worry about merging fields or anything.
I think we'll do the same thing for scheduled messages. If the currently scheduled message has tags (or no tags) and you want to change that, you need to send a new message that has a delay with no tags (or with tags, respectively).
I think the only exception/nuance for that is with the
delayfield. I could imagine times when I set the delay with a first request, want to change the message content with a second request, but I want the time to "keep counting down" based on the first request. I don't want to have to calculate the new delay myself (as the user sending requests) based on the difference between the first request and the update request. That's why I proposed something likedelay: 0,delay: -1, ordelay: keep(or maybedelay: continue?).In that case, I'm thinking the new scheduled message would be added to the DB, the delay field (
timestamp) from the old scheduled message would be copied, and the old scheduled message would be deleted, without merging any more of their fields.@binwiederhier commented on GitHub (Jun 3, 2025):
I was reading through the conversation above and I was able to follow much of it, but not all. I noticed though that the conversation almost entirely revolves around schedules messages to support the "dead man's snitch" ticket, and less about the original "i wanna update/delete a message" ticket. It may be worth excluding scheduled messages for now and handle only non-scheduled message updates.
I will spend 10min crafting some sample requests in the ticket. I ultimately do want to include scheduled messages, but it's likely a <1% case, and it would benefit so many more people to just support the non-scheduled messages for now.
So here it goes:
New database schema
⭐ = new
🍪 = changed
(a) Publish new message
Request:
Alternative request:
Alternative request:
Response:
Database after request:
(b) Update this message:
Request:
Alternative request:
Response:
Database after request:
(c) Delete this message:
Request:
Alternative request:
Response:
Database after request:
Additional thoughts:
uidgot pruned from the cache? Do we start over at 1?timeas revision 🤔@wunter8 commented on GitHub (Jun 3, 2025):
a. If it does have a value, it'd be the value of the most recent revision (revision 2 in this case) and not the value of revision 1 like you example above, right?
GET /topic/uid/deleteendpoint to accommodate services that can only issue GET/webhook requests?deletedfield vs my proposed empty message field on its own?a. If we do add a deleted column to the message cache table, I think it'd be better to call it
clearedsince we're just clearing it from the active notifications on the client device vs deleting it from the client device or deleting it from the server.mtimetimestamp field that includes milliseconds@wunter8 commented on GitHub (Jun 3, 2025):
Would the GET request for publishing a message with a uid have the following format?
@binwiederhier commented on GitHub (Jun 3, 2025):
Just an empty string. I will update the example above.
I am not opposed, but I would not conflate optimizations or convenience methods like this with the original feature. Baby steps. The impl won't be easy anyway, and it must be supported on Android, web app and iOS (eventually).
I think an explicit
deletedflag is better. I still like the namedeletedmore thancleared.👍
--
I will update the example above.
I also do not like the name
uid. Maybesequence_id?! Idk. We can change the name later.@wunter8 commented on GitHub (Jun 3, 2025):
Updating/deleting a message needs to be possible from those devices? Or just displaying a message update/deleting an existing notification needs to be supported?
@binwiederhier commented on GitHub (Jun 3, 2025):
Just displaying I meant
@pnxs commented on GitHub (Jun 3, 2025):
It's nice to see the progress here.
As far as I can see, a client (displaying device) would subscribe to a topic.
For every received message, it stores the latest
mtimeof theuid.If a connection disruption occurs, the client uses the
mtime(which acts like a version for theuid) to receive all newer updates (including deletions).It's also possible to track the history of message-updates on the client - for example, in case of short-lived notifications or missed notifications that are deleted because they are no longer relevant. This way the user would still be able to check whether some of these had occurred. I like this.
As for the question about
uid. To me, the existing concept ofnotificationsforms the foundation for the new concept ofchangeable notifications. As I understand it - for backward compatibility - all updates tochangeable notificationswill be transmitted as a sequence ofnotifications. On one handsequence_idmay be a fitting name, as it describes the idea ofchangeable notificationsas a sequence of notifications. However, perhaps we could find a name, that better describes this new concept. I must admit (English isn't my main language), it's difficult. I thought about the german words "Nachricht" and "Meldung". Where the former has the character of a single notification and the latter conveys more the meaning of the reporting or presenting part. Yeah, butsequence_idorthread_idsounds not to bad.@wunter8 commented on GitHub (Jun 3, 2025):
@binwiederhier Do you want to support this format and therefore exclude
(ws|json|sse|raw|publish|send|trigger)as possibleuids?@binwiederhier commented on GitHub (Jun 4, 2025):
I think it is the RESTy way to do it, so yeah, let's do that. We may want to require a minimum length for the update_id anyway, but blacklisting the above is not a bad idea
@grexe commented on GitHub (Jun 9, 2025):
discovered this wonderful project only now and already love it!
Stumbled across the need for this feature as well (need to keep track of changes of files on my NAS and only keep the latest state/path).
Talking about the RESTy way, I would really prefer to use
PUTfor updating messages instead of anotherPOST, which is usually only used for creating new resources. This was suggested by @blacklight here back then:https://github.com/binwiederhier/ntfy/issues/303#issuecomment-1144286256
Can't wait to test this! Running on my TrueNAS, setup was a breeze and everything worked out of the box!
@wcypierre commented on GitHub (Sep 2, 2025):
Looking forward for this to be implemented :D
@wunter8 commented on GitHub (Oct 18, 2025):
This is being worked on here: https://github.com/binwiederhier/ntfy/pull/1466
@binwiederhier commented on GitHub (Jan 8, 2026):
This is the current state of affairs for this:
https://github.com/user-attachments/assets/d5a758fd-dea5-44e3-a54b-c15347ac826c
The API is like this:
Create message
Update message
Delete message
Subscribers/pollers
Storage
Server:
sequence_idsetdeleted: trueWeb app:
Android app:
@binwiederhier commented on GitHub (Jan 8, 2026):
Questions:
DELETE /<topic>/<sequence-id>delete the notification on the client or mark it as readDELETE /<topic>/<sequence-id>deletes it andPUT /<topic>/sequence-id>/readmarks it as read?@wunter8 commented on GitHub (Jan 8, 2026):
Why the difference between hard delete and soft delete in the web app and Android app?
The example you showed doesn't have a sequence_id (
{"id":"VXsqkDO57Gaz","time":1767910451,"expires":1773094451,"event":"message","topic":"test6","message":"created"})I always imagined DELETE would dismiss an existing/visible notification (so remove a notification from my notification shade, for example). I don't think I want it to be removed inside the app because I'd like to be able to see it there for history/logging reasons.
So, if by "mark as read" you mean "remove a notification from the notification shade or no-op if I've already dismissed that notification from the shade", that's what I would prefer.
As far as the API, having both options (delete and "mark as read") would probably be nice so those that don't care about keeping a history can delete it entirely.
I'm not sure what introducing those new event types would do. It'd probably be fine. And I think it's a more accurate representation of what's happening vs sending a new message. Especially if we have "mark as read" (or "cleared"?) and "deleted". I think it'd be difficult to represent both of those inside our current "message" event
@binwiederhier commented on GitHub (Jan 9, 2026):
The Android app already does soft deletes. Originally this was so it can clean up the attachments separately. I am not sure if it ever hard deletes the entries, which is a problem. But I didn't want to solve too many problems at once.
Yeah it seems marginally more work to do both, so I'll probably do both.
I don't want to change the original message structure, so only message 2, 3, 4, ... of a sequence will show the sequence_id.
@wunter8 commented on GitHub (Jan 9, 2026):
I was thinking soft deletes would be preferred in both clients. So you can see the revision history. But that's probably not necessary. Just my paranoid "save all the data because I might need it at some point!" 😅
@binwiederhier commented on GitHub (Jan 9, 2026):
I think it may be cool to see the history of a message at one point, but not even Slack or Discord do that. How the clients handle the protocol can be adjusted later. For now it's important to get the protocol right
@binwiederhier commented on GitHub (Jan 9, 2026):
Okay, I've hit a pickle / dead end: Marking a notification as "read" aka "clearing it", or even "deleting it" (swiping in the Android app) can now be triggered via the API with DELETE/PUT as described above, but it does not sync state between clients.
A swipe in the Android app should trigger a
DELETE /topic/sid, which would delete it in the web app. That sounds fine until you think of bulk actions or the fact that the deleteAfter in the Android app and web app are different. I guess I could allow bulk actions for deleting and mark as read. Thinking of the rate limits and such, people would surely not like that deleting and marking as read would double/triple the message count.ntfy doesn't keep the entire state on the server. Not all messages are there after 12h, so the state can differ between clients...
Ahh
@capi commented on GitHub (Jan 9, 2026):
I think your latest point adds basically a new use-case, which is state synchronization between the clients. For what I personally am looking forward to this feature for, is that I want to cancel/remove pending notifications from the sender side, since the notification/pending request has already been handled by a different client or the state became obsolete on the sender side. This seems to be very well covered with your current proposal and the current state.
Maybe just consider the client synchronization a separate feature to be solved independently and later.
@wunter8 commented on GitHub (Jan 10, 2026):
I think that's fine for now. It could be added later. And the comment about the server not keeping all the state, I think that's also fine.
ntfy doesn't work well in general if devices are disconnected for longer than the cache time. So it's expected that state and sync and stuff might be negatively impacted if devices are offline longer than the cache duration
@binwiederhier commented on GitHub (Jan 14, 2026):
I am still polishing, but here's the state of affairs:
https://github.com/user-attachments/assets/597e8dae-81a0-4706-b22b-b001d717a089
Create / update / delete / clear message
sequence_id(delete old messages with same sequence IDs from clients)Subscribers/pollers
Apps
Server:
sequence_idsetevent=message_clear⭐event=message_delete⭐Web app:
event=message_deletemessage, no message is added to the local storage. The browser notification cannot be deleted.event=message_clearmessage, the message is marked as read in the local storage. The icon badge number is decreased. The browser popup notification cannot be reliably canceled. ⭐Android app:
event=message_deletemessage, no message is added to the local storage. The Android popup notification is canceled (cleared) ⭐event=message_clearmessage, the message is marked as read in the local storage. The icon badge number is decreased.⭐@p1gp1g commented on GitHub (Jan 14, 2026):
Once implemented, it shouldn't be hard to send the "sequence id" using a header right? Then it can be possible to comply with web push
Topicheader:@binwiederhier commented on GitHub (Jan 14, 2026):
You can pass
X-Sequence-IDheader though that doesn't comply with the RFC. Adding aTopic:header would be super confusing, sadly, because "topic" is what ntfy uses, so I don't quite know what do to there. How would that be helpful?@p1gp1g commented on GitHub (Jan 14, 2026):
What about using
Topic:header only when?upGET parameter is given ?For example, mollysocket, sends push notifications as pings for Molly, a soft-fork of Signal. The push notifications are sent with
topic: mollysocketto avoid storing more than one ping on the server.That way, if a device is offline for an hour and receive like 20 messages, the device only get 1 push notification when back online
@binwiederhier commented on GitHub (Jan 14, 2026):
I am open to the idea, but we should move this discussion to a different ticket. Also, that would mean that ntfy would have to only forward one notification to UP instead of all intermediate ones. Right now, each message still there (see above).
@p1gp1g commented on GitHub (Jan 14, 2026):
I thought the deletion was done on the server. Here it is: https://github.com/binwiederhier/ntfy/issues/1548
@binwiederhier commented on GitHub (Jan 15, 2026):
❓ Here's a question:
Right now, an UPDATED message is still
event:message. When we CLEAR or DELETE a message, it's anevent:message_clearorevent:message_delete. Should an updated message beevent:message_update?So instead of:
It would be:
@wunter8 commented on GitHub (Jan 15, 2026):
It might be nice for clients to know explicitly which messages are new vs updates (especially if the previous/first message(s) aren't in the cache anymore). And it'd be harder to change this in the future if we don't implement it now. So I'd probably lean toward yes?
@wunter8 commented on GitHub (Jan 15, 2026):
Although, if the message isn't in the server cache anymore, how will the server know if something is a first message or an update?
Updates will always have a sequence_id, but a first message might have a manually defined sequence_id
@binwiederhier commented on GitHub (Jan 15, 2026):
The
message_clearandmessage_deleteevents are also in the database. So would this one.This would never change. Backwards compatibility FTW
@binwiederhier commented on GitHub (Jan 15, 2026):
I didn't realize there is a problem here until I was about to implement this. With this new
message_updateevent, the server now needs to figure out the type of the message, which implies a read from the DB for every publish, or at least every publish to /topic/sequence-id. Not sure if that's great.Example 1: Normal case, no DB lookup, no ambiguity.
Example 2.1: Additional DB lookup, ambiguity depending on the message timing. Here: Second message is a
message_update.Example 2.2: Additional DB lookup, ambiguity depending on the message timing. Here: Second message is a
messageI don't like this ambiguity.
@wunter8 commented on GitHub (Jan 15, 2026):
Yeah, your example 2.2 is what I meant by "how will the server know if something is a first message or an update?"
@binwiederhier commented on GitHub (Jan 15, 2026):
I need to learn how to read :-) I just re-read your comments and it's very clear that you saw this problem before me haha. Soooo.... leave it like it is then?
@wunter8 commented on GitHub (Jan 15, 2026):
Yeah, probably leave it as is. Both because of the additional DB query on each publish and because of the ambiguity.
I don't see the same issue with
message_clearandmessage_deletebecause the endpoint will indicate whether it's a clear or delete (without any additional DB query), and the request will always include the relevant sequence_id, so it doesn't matter if the previous message(s) are still in the DB or not@binwiederhier commented on GitHub (Jan 19, 2026):
This is done and merged. It'll be in the next release: https://docs.ntfy.sh/publish/#updating-deleting-notifications
I am testing it right now, and will likely cut a testing Android release either today or tomorrow.
@wcypierre commented on GitHub (Jan 19, 2026):
Any love for iOS clients?
@binwiederhier commented on GitHub (Jan 19, 2026):
No love from me, though there has been a user on Discord that made a new ntfy iOS app. They seem competent. If that doesn't work out, I'll likely eventually pick that up myself again.
@arminus commented on GitHub (Jan 22, 2026):
Highly appreciate this! 🥇 Works like a charm with the web client, is there an updated android version already somewhere?
@binwiederhier commented on GitHub (Jan 22, 2026):
The Android version is rolling out on Play over the next week, or you can sign up for betas there and get it now.
.
@shale-aspect commented on GitHub (Jan 25, 2026):
Is it possible to add
eventto the supported fields of Publish as json? I see thatsequence_idis supported (and updating messages works nicely), but my attempts to clear or delete messages haven't been successful so far.@wunter8 commented on GitHub (Jan 25, 2026):
@shale-aspect I think it'd be best to open a new issue for that. Something like "allow clearing and deleting a message via "publish as JSON""
@shale-aspect commented on GitHub (Jan 25, 2026):
@wunter8 Thanks for the heads-up, I opened #1573.