FEP-34ec: Notification Collection Endpoint

This FEP defines a standardized notification collection for ActivityPub actors. A new notifications property under endpoints (ActivityPub §5.7) provides an OrderedCollection containing Notification objects that inform the actor about relevant activities.

Unlike the inbox, which receives raw activities, the notification collection holds server-generated notification objects. Notifications only exist within the collection — read notifications are removed. Batch removal is supported via FEP-db70 (RemoveAll) with optional FEP-34c1 filtering.

Full text: //codeberg.org/fediverse/fep/src/branch/main/fep/34ec/fep-34ec.md

What is a "notification object"?

Wouldn't this fall outside the scope of AP? Is this a C2S specific route?

Haha, you’re too quick – wait for the FEP :wink:

now it’s merged

Unfortunately I see a lot of problems with this:

  • What is the point of this FEP? It seems to have something to do with read/unread status?
  • All activities are already notifications, so what does it mean to define a new Notification type? The definition is very unclear.
    • “There is no standardized ActivityPub endpoint for notifications and no vocabulary for notification objects” is incorrect; the endpoint is the ldp:inbox and the vocabulary is the AS2 vocabulary.
    • Later you say “A notification is a distinct object that informs the actor about an activity” – is this not already an Announce activity?
  • There is a line in the “motivation” section talking about home vs notification feeds, and this FEP feels like it 1) doesn’t address that motivation, 2) confuses the problem further. Do you create Notifications for only things that appear in the notifications feed, or is it something else entirely? How does this solve anything about the “home feed”?
  • as:endpoints is typically for server-wide endpoints, not actor-specific endpoints. So it doesn’t make sense to put https://example.com/actors/bob/notifications in the endpoint mapping.

The point is C2S: a generic ActivityPub client has no standardized, performant way to retrieve notifications separately from the home feed. Every implementation (Mastodon, Pleroma, GoToSocial, Misskey) has built a proprietary API for this — outside of ActivityPub. This FEP standardizes that missing C2S endpoint.

Activities inform — but the inbox is a delivery channel, not a notification interface. A C2S client connecting to the inbox sees everything mixed: posts from followed accounts, likes on your posts, boosts, mentions. Which of those are “notifications”? Today every server decides that on its own and exposes a proprietary REST API for it. The Notification type makes this server-side decision explicit and machine-readable via notificationType, accessible through a standardized C2S endpoint.

ldp:inbox defines activity delivery — agreed. But how does a C2S client retrieve only notifications from the inbox? There is no standardized, performant mechanism to separate notifications from the home feed. If the existing vocabulary were sufficient for this, every implementation would not have needed to build its own proprietary notification API.

Announce has federation semantics — it gets forwarded to followers. A Notification is purely local, never federated, and carries notificationType for machine-readable categorization. Collection membership serves as read state, managed by the client via C2S. How would you mark an Announce as “read”?

Fair point — the motivation section needs to be clearer about the scope. This FEP solves one specific problem: a standardized C2S endpoint for notifications. It does not define the home feed.

Yes — the server creates Notification objects only for events it considers notification-worthy (likes on your posts, mentions, boosts of your content). Posts from followed accounts go into the home feed, not the notification collection. This is exactly what every implementation already does today — they just all do it behind proprietary APIs.

The spec says “typically” — not “exclusively”. And proxyUrl under endpoints is already actor-specific: each actor has their own proxy endpoint, yet it lives under endpoints.

That said — i’‘m open to alternatives. Would a top-level property on the actor object be preferable? The key requirement is discoverability: a C2S client needs to find the notification collection by looking at the actor object.

Inboxes receive linked data notifications

No, the inbox is the ldp:inbox from Linked Data Notifications.

What is needed to replicate Mastodon/etc behavior is not a way to extract “notifications” from the inbox, it’s a way to extract the “home feed” from the inbox.


Activities doesn’t need to be federated

No, the addressing is separate from the type. If you want to explicitly identify the actor and the audience, you can consider it an Announce from a system actor to some other actor.


Mark as read

Through a mechanism dedicated to read markers, not through reinventing notifications.

I would say there are two ways to do it:

Method 1: Uncollapse notifications

Expose the notification payload of the inbox, as already happens in Linked Data Notifications (although ActivityPub collapses every notification into an Activity). This seems to be the closest to what you’re trying to do:

{
  "@id": "https://inbox.example/item/1",
  "@type": [
    "http://www.w3.org/2004/03/trix/rdfg-1/Graph",
    "_:LinkedDataNotification"
  ],
  "@graph": [
    {
      "@id": "https://activity.example/",
      "@type": "https://www.w3.org/ns/activitystreams#Create",
      "https://www.w3.org/ns/activitystreams#actor": {"@id": "https://bob.example/"},
      "https://www.w3.org/ns/activitystreams#object": {"@id": "https://thing.example/"},
      "_:published": "8 seconds ago"
    }
  ],
  "_:received": "3 seconds ago"
  "_:readReceipts": [
    {"@type": "_:Read", "_:by": {"@id": "https://client-1.example/"}},
    {"@type": "_:Read", "_:by": {"@id": "https://client-3.example/"}},
  ],
  "http://purl.org/dc/terms/subject": {"@id": "https://activity.example/"}
}

The distinction between the published date on the activity and the received date on the linked data notification allows us to chronologically sort a timeline without being misled by the date set by the publisher. When you view a timeline, the sorting is NOT done by as:published (or rather, SHOULD NOT be) – it is done by whenever the linked data notification was _:received in the ldp:inbox (which can usually be inferred from the Date on the HTTP POST, or the system datetime when the POST is processed).

There is an alternative which might be less desirable, which is: instead of saying that the LDN is a named graph describing the activity, we can say that the LDN refers to the activity. This allows us to avoid using graphs/quads, if we want to stick with the simpler triples. But working with graphs/quads is itself a way to avoid using triple terms.

Method 2: Track read state separately

You could have a graph or collection or whatever keep track of which activities you’ve seen or read.

A “view tracking collection” might look like this:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "orderedItems": [
    {"actor": "https://client-1.example/", "type": "View", "object": "https://activity.example/"}
  ]
}

You could even define an inbox protocol that accepts POSTing View and Undo View activities, if you really wanted to?


No, the same proxyUrl can be used for multiple actors. It’s just an indirection between actor → endpoint mapping → endpoint, instead of actor → endpoint. Consider how the ldp:inbox and as:outbox are directly properties of the actor and not properties of the endpoint mapping, despite functioning as endpoints. But the as:sharedInbox isn’t directly a property of the actor, even though it could be.


Alternatives

I think the fundamental issue here is that ActivityPub describes the ldp:inbox as an as:OrderedCollection whose direct as:items are each an as:Activity. It sounds like what you want is a collection of notifications that were received in the inbox (possibly filtered for relevance/spam/etc), right? This is a bit of a conflict between LDN and AP in how they handle the notification already having its own @id or not.

LDN

Issue URI for notification · Issue #19 · w3c/ldn · GitHub leads to issue Inbox and notification persistence, permanence, integrity · Issue #17 · w3c/ldn · GitHub which discusses this in more depth.

BigBlueHat writes:

[…] allow the originator of the message to provide an identity before sending the notification, for the message to get a new identity upon receipt, and for the message to even be passed along to other inboxes (relayed) with the via chain of identities accumulated along the way […]

rhiaro writes:

If you post objects with @id set, they are preserved. You can use this for the notification itself (as in AP) or any other resources you’re including […] If you post objects with @id relative ("") or missing then the new URI generated can be inferred to identify these objects […] A receiver can forward any incoming notifications, and add new properties to them (like via), but mentioning this is out of scope of the spec.

BigBlueHat writes:

Does that then mean that the notification itself does not get an @id (as originally proposed) unless one was not previously provided?

csarven writes:

The relation (ldp:contains) on the Inbox URL points at the notification URI (what’s deferenceable in the end), not the @id value. It may be that the notification URI (what was created by the server) and the @id end up being the same if the sender leaves @id empty.

BigBlueHat writes:

That would then mean that the Request-URI and the @id do not match […] I don’t suppose that’s a problem technically, but it may be prudent to note that somewhere or other to avoid confusion by consuming clients–as they’ll likely then need to store both those identifiers

So what LDN ended up with is a model like this:

GET /inbox/ HTTP/1.1
Host: foo.example
Accept: application/ld+json

HTTP/1.1 200 OK
Content-Type: application/ld+json

{
  "@id": "https://foo.example/inbox/",
  "http://www.w3.org/ns/ldp#contains": [
    "https://foo.example/inbox/item1",
    "https://foo.example/inbox/item2"
  ]
}
POST /inbox/ HTTP/1.1
Host: foo.example
Content-Type: application/ld+json

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://activity.example/",
  "actor": "https://actor.example/"
  "type": "Announce",
  "object": "https://thing.example/"
}

HTTP/1.1 201 Created
Location: https://foo.example/inbox/item3
GET /inbox/ HTTP/1.1
Host: foo.example
Accept: application/ld+json

HTTP/1.1 200 OK
Content-Type: application/ld+json

{
  "@id": "https://foo.example/inbox/",
  "http://www.w3.org/ns/ldp#contains": [
    "https://foo.example/inbox/item1",
    "https://foo.example/inbox/item2",
    "https://foo.example/inbox/item3"
  ]
}
GET /inbox/item3 HTTP/1.1
Host: foo.example
Accept: application/ld+json

HTTP/1.1 200 OK
Content-Type: application/ld+json

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://activity.example/",
  "actor": "https://actor.example/"
  "type": "Announce",
  "object": "https://thing.example/"
}

Note that we fetched https://foo.example/inbox/item3 and received a description of https://activity.example/ in the content. But we say that the inbox contains https://foo.example/inbox/item3 and does not contain https://activity.example/.

AP

Where AP differs from LDN is that it redefines the ldp:inbox somewhat – it isn’t just an ldp:Container, it is also an as:OrderedCollection.

However, the biggest divergence is that the container contains the notifications, while the inbox collection’s items are activities.

So you end up with this kind of confusing situation:

{
  "@id": "https://foo.example/inbox/",
  "http://www.w3.org/ns/ldp#contains": [
    "https://foo.example/inbox/item3"
  ],
  "https://www.w3.org/ns/activitystreams#items": {
    "@list": [{"@id": "https://activity.example/"}]
  }
}

The inbox simultaneously contains notifications and activities, depending on whether you access it as a Container or a Collection.

I believe uncollapsing the notification might be necessary to enable a lot of use cases, but it would run counter to ActivityPub. I guess this can be raised as an issue against ActivityPub… I’ll do that later today.

In that regard, what is needed is a way to wrap activities in notifications, or a way to associate notifications with activities. Maybe as:alsoKnownAs can help, but this requires a lot of thought about which identifiers should be used in which contexts.

@stevebate Have you given it any thought yet?

Ok, I honestly didn’t plan to invest this much effort, but your comment made me dig deeper into LDN and rethink the approach. Here’s where I landed:

The core issue as I understand it: AP collapses the LDN indirection — inbox items reference foreign activity URIs instead of server-owned notification resources.

Instead of working around this with a separate collection (current FEP-34ec), what about solving it at the inbox level? Each inbox item becomes a server-owned resource with dual
types — the Activity type plus Notification. The existing content type distinction controls the view:

Standard AP client (Accept: application/activity+json):

{
    "@context": "https://www.w3.org/ns/activitystreams",
    "type": "as:Like",
    "id": "https://example.com/inbox/notif-42",
    "as:actor": "https://alice.example/actors/alice",
    "as:object": "https://example.com/posts/123",
    "as:published": "2026-02-24T10:00:00Z"
  }

RDF-aware client (Accept: application/ld+json):

{
    "@context": [
      "https://www.w3.org/ns/activitystreams",
      "https://w3id.org/fep/34ec"
    ],
    "type": ["as:Like", "fep34ec:Notification"],
    "id": "https://example.com/inbox/notif-42",
    "as:actor": "https://alice.example/actors/alice",
    "as:object": "https://example.com/posts/123",
    "as:published": "2026-02-24T10:00:00Z",
    "fep34ec:dateReceived": "2026-02-24T10:00:05Z",
    "fep34ec:read": false,
    "fep34ec:readBy": [
      {
        "fep34ec:client": "https://example.com/clients/web",
        "fep34ec:readAt": "2026-02-24T10:05:00Z"
      }
    ]
  }

No new profile needed — application/activity+json already implies the AS2 profile. Properties outside the as: namespace get stripped in that view. application/ld+json delivers everything.

The fep34ec:read property is computed per request context — the server checks whether the requesting client (identified e.g. via OAuth2 azp claim) appears in readBy. This enables filtering unread notifications via FEP-34c1:

{
    "type": "FilterRequest",
    "relation": [
      {
        "type": "EqualToRelation",
        "path": { "@id": "fep34ec:read" },
        "value": false
      }
    ]
  }

This also addresses swicg/activitypub-api#41 — readBy provides multi-client read tracking, read provides a simple filterable boolean.

No separate collection, no wrapping layer, backwards compatible. Does this align with what you meant by “uncollapsing the notification”?

that’s one possible way to approach it, i guess. although it’s complicated by AS2 saying to use application/activity+json and AP saying to use application/ld+json; profile="https://www.w3.org/ns/activitystreams"

another way is to use ldp:contains to point to notifications and as:items to point to activities? this can maybe be retrofitted onto AS2 paging mechanisms, but that part was left open-ended by LDN: https://www.w3.org/TR/ldn/#note-paging

This specification does not define a paging mechanism to serve the list of notifications in an Inbox. Implementations that wish to enable paging may want to use existing mechanisms to allow efficient retrievals e.g., Linked Data Platform Paging 1.0, Activity Streams 2.0 Collection.

so just like how we can say a CollectionPage’s items are included in the collection, we can also maybe say that CollectionPage is also a “container page”?

a related approach is to say that, since an ldp:inbox’s contained notifications are required to be RDF sources per section 3.3.2, they can maybe also be RDF graphs. i’m not sure if this violates any requirements, because i can’t find any requirements for a received LDN to have any specific content when you fetch a resource representation via HTTP – it can be any RDF-bearing representation, which includes both triples and quads. the simplest thing to do would be to provide links to RDF datasets that make claims about the activity using the notification as the quad’s context:

GET /item/1 HTTP/1.1
Host: inbox.example
Accept: application/ld+json

HTTP/1.1 200 OK
Content-Type: application/ld+json
Link: </item/1/graph>; rel="describedby"

{
  "@id": "https://activity.example/",
  "@type": "https://www.w3.org/ns/activitystreams#Create",
  "https://www.w3.org/ns/activitystreams#actor": {"@id": "https://bob.example/"},
  "https://www.w3.org/ns/activitystreams#object": {"@id": "https://thing.example/"}
}
GET /item/1/graph HTTP/1.1
Host: inbox.example
Accept: application/ld+json

HTTP/1.1 200 OK
Content-Type: application/ld+json

{
  "@id": "https://inbox.example/item/1",
  "@graph": [
    {
      "@id": "https://activity.example/",
      "@type": "https://www.w3.org/ns/activitystreams#Create",
      "https://www.w3.org/ns/activitystreams#actor": {"@id": "https://bob.example/"},
      "https://www.w3.org/ns/activitystreams#object": {"@id": "https://thing.example/"}
    }
  ],
  "http://purl.org/dc/terms/subject": {"@id": "https://activity.example/"}
}

some open questions on what the graph id should be, and also whether rel=“describedby” is the appropriate link relation to use here.


other misc responses

this isn’t necessary

you can infer that something is read if it has at least 1 known read receipt, yes.

it’s getting closer, i think! i’m definitely exploring something along the lines of what i’ve described above for an inbox that supports both LDN and AP. i’m considering starting with something like this:

{
  "@context": [
    "http://www.w3.org/ns/ldp",
    "https://www.w3.org/ns/activitystreams"
  ],
  "contains": ["./notifs/1", "./notifs/2"],
  "orderedItems": ["latest-posted-activity", "some-other-activity"]
}

but this is because i want to rely more on a sort of “thin ping” approach where activities are fetched from origin and authenticated by TLS (to avoid the necessity of HTTP signatures). at least, the claims made by a POSTed notification are contextualized by the notification’s graph and not taken as canonical unless they agree with the canonical authoritative origin.

“uncollapsing the notification” basically means serving a collection that has notifications instead of activities. doing this directly would not be compatible with activitypub since activitypub requires collapsing the notification.

collapsed:

{
  "name": "Some Inbox",
  "orderedItems": [
    {"id": "some-activity", "type": "Activity"}
  ]
}

uncollapsed:

{
  "name": "Some Inbox",
  "orderedItems": [
    {"type": "_:LinkedDataNotification", "@graph": [{"id": "some-activity", "type": "Activity"}]}
  ]
}

alternate uncollapsing using references to activities:

{
  "name": "Some Inbox",
  "orderedItems": [
    {"type": "_:LinkedDataNotification", "_:activity": "some-activity"}
  ],
  "@included": [
    {
      "id": "some-activity",
      "type": "Activity"
    }
  ]
}

you can think of this as a sort of “CollectionItem” similar to how HTML uses <li> for list items and doesn’t just directly iterate over direct children of <ul> or <ol>. AP is basically doing this:

<ol>
  <article></article>
  <article></article>
  <article></article>
</ol>

when it should be doing this:

<ol>
  <li><article></article></li>
  <li><article></article></li>
  <li><article></article></li>
</ol>

I appreciate the deep dive into LDN and the architectural analysis — this is exactly the kind of discussion that helps shape a solid proposal.

That said, I need something implementable now, not in months. I’m actively building C2S support and need a working notification mechanism.

So let me ask directly: If I rework FEP-34ec along the lines we discussed — uncollapsing at the inbox level with ldp:contains for server-owned notification resources alongside as:orderedItems for activities — would you consider that a viable direction? Or do you see fundamental blockers that require an AP issue to be resolved first?

I’d rather iterate on a concrete spec than wait for upstream changes that may take a long time.

One practical constraint: the “thin ping” approach (fetching activities from origin) won’t work in the real Fediverse — Mastodon for example don’t persist some activities after delivery. The inbox must hold the full activity payload.

Another practical constraint: most Fediverse developers don’t know what RDF is — let alone quads or named graphs. A solution that relies on named graphs won’t get implemented in practice. Whatever we specify needs to work with simple JSON-LD that looks and feels like regular JSON.

On read state: agreed that read is derivable from readBy. The reason for the explicit boolean is purely practical — it enables simple filtering with TREE (FEP-34c1). A client can request unread notifications with a single EqualToRelation on a boolean property, instead of requiring the server to support existence checks on nested structures. If there’s a clean way to express “readBy is empty” as a TREE filter, I’m happy to drop it.

To move forward concretely, I’d like to clarify two points:

  1. What should a notification resource look like? You sketched several approaches. I see named graphs and quads as unrealistic for the Fediverse community — which approach works with simple JSON-LD?
  2. How does a client mark something as read? POST View/Undo View?

what can be done now

The inbox is already a working notification mechanism. Is what you really need a way to filter for specific notifications ahead-of-time?

Would you consider it viable? What is the UX you’re trying to implement here? The home-vs-notifications split that Mastodon et al present to their users? If that’s all you want, then the least disruptive thing you could do would be to write the FEP so that you have some properties pointing to specific collections that you define. Say something like this:

{
  "id": "https://actor.example/",
  "inbox": "https://inbox.example/",
  "outbox": "https://outbox.example/",
  "https://w3id.org/fep/xxxx/home": {"id": "https://home-timeline.example/"},
  "https://w3id.org/fep/xxxx/notifications": {"id": "https://notifications-timeline.example/"}
}

Then in the FEP, define the criteria for when an Activity arriving in the inbox gets added to home or gets added to notifications.

If these collections are being managed by the server, then the client can Add activities to them as they arrive in the inbox (and this client might be an internal client integrated into the server).

If these collections are being managed by some client, then the client can listen to inbox activities and generate the collections on its own.


re: “practical constraints”

this isn’t a blocker if you store the payloads received but contextualize the statements as being from the received payload and not from the origin. i’m not saying to discard the body content of the notifications – i’m saying to be aware when claims are authoritative or not. the “thin ping” comment is for a personal project of mine and you can ignore that part for the purposes of this discussion.

the quad-based approach only matters if you want to work with notifications as separate from activities. even so, there’s still a way to keep everything at the level of triples – notifications can be graphs or not graphs, that’s an implementation detail.

the json can look like this:

GET /inbox/ HTTP/1.1
Accept: application/activity+json

HTTP/1.1 200 OK
Content-Type: application/activity+json

{
  "orderedItems": ["activity-2", "activity-1"],
  "http://www.w3.org/ns/ldp#contains": [{"id": "notification-1"}, {"id": "notification-2"}]
}

and then when you GET notification-1 or notification-2 it can return either triples or quads. the triples can be contextualized to quads anyway since LDN requires notifications to be RDF sources (but doesn’t specify whether they must be graphs or datasets).

for embedded activities, the json could look like this:

{
  "orderedItems": [{"id": "activity-2", "type": "Announce"}, {"id": "activity-1", "type": "Create"}],
  "http://www.w3.org/ns/ldp#contains": [{"id": "notification-1"}, {"id": "notification-2"}]
}

or it could look like this:

{
  "orderedItems": ["activity-2", "activity-1"],
  "http://www.w3.org/ns/ldp#contains": [{"id": "notification-1"}, {"id": "notification-2"}],
  "@included": [{"id": "activity-2", "type": "Announce"}, {"id": "activity-1", "type": "Create"}]
}

arbitrary nesting depth in JSON documents vs flattening or included statements

this is what could be dealt with upstream in AP, since currently there are no schematic constraints on AS2 documents beyond the top-level node being a JSON object. this creates the infamously inconvenient issue of values of properties having many possible representations – strings that are semantically an @id reference may or may not be in object form at any arbitrary depth in the JSON document, so AS2 processors that don’t use LD need to account for numerous possibilities:

{
  "orderedItems": ["activity-1"]
}
{
  "orderedItems": [
    {
      "id": "activity-1",
      "object": "object-1"
    }
  ]
}
{
  "orderedItems": [
    {
      "id": "activity-1",
      "object": {
        "id": "object-1",
        "attributedTo": "actor-1"
      }
    }
  ]
}
{
  "orderedItems": [
    {
      "id": "activity-1",
      "object": {
        "id": "object-1",
        "attributedTo": {
          "id": "actor-1",
          "outbox": "outbox-1"
        }
      }
    }
  ]
}
{
  "orderedItems": [
    {
      "id": "activity-1",
      "object": {
        "id": "object-1",
        "attributedTo": {
          "id": "actor-1",
          "outbox": {
            "id": "outbox-1",
            "totalItems": 34576
          }
        }
      }
    }
  ]
}

if you wanted to know “how many activities are in the outbox of the author of the post that was just interacted with?” (“the author of the interacted-with post has made 34576 posts”) then you can’t just use your JSON processor to access .orderedItems[*].object.attributedTo.outbox.totalItems in all cases, because the nested/embedded graph may cut off at any arbitrary point. you will likely have to fetch information from additional sources to be able to answer that question. so the JSON processor has no guarantees anyway already. you don’t know how deep the nesting will be for any given JSON document. this is why flattening exists.

footnote on notifications

for the notifications that the ldp:inbox ldp:contains, there could also be other statements @included or you could GET them individually. that’s out of scope here, but it could look like this:

{
  "orderedItems": ["activity-2", "activity-1"],
  "http://www.w3.org/ns/ldp#contains": [{"id": "notification-1"}, {"id": "notification-2"}],
  "@included": [
    {"id": "activity-2", "type": "Announce", "published": "17 seconds ago"},
    {"id": "activity-1", "type": "Like", "published": "13 seconds ago"},
    {
      "id": "notification-1",
      "http://purl.org/dc/terms/subject": {"id": "activity-1"},
      "_:received": "3 seconds ago"
    },
    {
      "id": "notification-2",
      "http://purl.org/dc/terms/subject": {"id": "activity-2"},
      "_:received": "2 seconds ago"
    }
  ]
}

note that the activities have out-of-order published timestamps, but the notifications reflect order of delivery instead of order of sending. what happened here is that the sender sent an Announce, then 4 seconds later sent a Like. however, the Like took 10 seconds to be delivered, while the Announce took 15 seconds to be delivered. you get different orderings if you sort “chronologically”, depending on whether you sort by “activity published” or “notification received” timestamps.


read state

you’re probably looking for a tree:shape with sh:minCount on some sh:path

{
  "@context": {
    "sh": "http://www.w3.org/ns/shacl#"
  },
  "sh:description": "This shape matches any read object.",
  "sh:path": {"@id": "https://w3id.org/fep/xxxx/readBy"},
  "sh:minCount": 1
}

Although now that you mention it, is TREE the right vocabulary to be using for filtering? I can see it being maybe useful for describing the results, but filtering/querying is usually done with SPARQL, while TREE can be used to describe the results as a tree:Collection (so you can describe how an ordered collection is ordered, or describe what a “next” page contains…)


clarification

re: (1)

The approach that uses triples is something like this:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "some-notification",
  "https://some-relation.example/": {"id": "some-activity"}
}

I sketched out a triple using dcterms:subject as the relation, which seems appropriate but I’m not 100% sure. This is claiming that the activity is the main subject or topic of the notification:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "some-notification",
  "http://purl.org/dc/terms/subject": {"id": "some-activity"}
}

Note that this still has the problem with arbitrary embedding/nesting depth that is unsolved in AP, so additional included statements may look like this:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "some-notification",
  "http://purl.org/dc/terms/subject": {"id": "some-activity", "actor": {"id": "some-actor", "name": "Some Actor"}}
}

or they may look like this:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "some-notification",
  "http://purl.org/dc/terms/subject": {"id": "some-activity"},
  "@included": [
    {
      "id": "some-activity",
      "actor": "some-object"
    },
    {
      "id": "some-actor",
      "name": "Some Actor"
    }
  ]
}

depending on whether there are any schematic constraints in effect. For example, a schematic constraint might say “The actor of an Activity MUST be a JSON string, i.e. it MUST NOT be a JSON object with embedded statements”. (Right now, AP has no schematic constraints, so JSON processors have an explosion of complexity.)

re: (2)

If you mean what I described in earlier posts, that would be an endpoint that has an associated protocol:

{
  "endpoints": {
    "https://view-tracker.example/": {"id": "https://endpoint.example/"}
  }
}
POST / HTTP/1.1
Content-Type: application/activity+json; profile="https://view-tracker.example/"

{
  "actor": "client-1",
  "object": "notification-1",
}

HTTP/1.1 201 Created
Location: https://views.example/1
GET /some-inbox HTTP/1.1
Accept: application/activity+json

HTTP/1.1 200 OK
Content-Type: application/activity+json

{
  "name": "Some Inbox",
  "orderedItems": ["some-activity"]
  "http://www.w3.org/ns/ldp#contains": {"id": "notification-1", "https://vocab.example/readReceipts": {"actor": "client-1"}}
}

This requires the server to be aware of the endpoint and its POSTed receipts, then inject them into the representation of the inbox.

If this is an unacceptable ask of the server, then the client can fetch two graphs and merge the information client-side instead:

{
  "name": "Some Actor",
  "inbox": "/my-inbox",
  "https://view-tracker.example/viewsCollection": {"id": "/my-views"}
}
GET /my-inbox HTTP/1.1
Accept: application/activity+json

HTTP/1.1 200 OK
Content-Type: application/activity+json

{
  "orderedItems": ["..."]
}
GET /my-views HTTP/1.1
Accept: application/activity+json

HTTP/1.1 200 OK
Content-Type: application/activity+json

{
  "orderedItems": [
    {"id": "/view-1", "actor": "/client-1", "object": "..."}
  ]
}

I think you may need a more precise definition of “notification” in the FEP.

(In my opinion, the whole LDN discussion is an unproductive tangent. There’s one paragraph in the spec that says there’s an overlap and a claim that the two specs are interoperable, but AFAIK, that’s never been proven in practice.)

ActivityPub inbox activities are sometimes referred to as “notifications”, but probably not the kind you are describing in the FEP. The ActivityStreams 2.0 Vocabulary spec also uses the word “notifications”. This is clearly not an LDN term since that spec was published before ActivityPub and LDN is not otherwise mentioned. I think the AS2 notion of “notification” may be closer to what you mean by it.

All that said, the kind of “notification” you’re describing in the FEP isn’t completely clear to me. To me, a Like(for example) is a notification. I’m not seeing why a separate Notification type is needed. Why not just reference the activity from the notification collection and remove the reference (not delete the referenced activity) when it has been acknowledged. The active collection management is what makes this seem different to me than just showing a filtered set of activities from the AP inbox.

The `proxyUrl` endpointisn’t necessarily actor-specific, but I agree that the endpoints, as specified, can be a mix of actor or server level (or some other scope, like a Group or Organization).

I think we need both. The home feed is focused on content (CRUD activities). The notification feed is focused on “notifications” (favorites/Like, boosts/Announce, follows, etc. ) . These are both derived from activities received on the AP inbox endpoint (and activities exchanged between local “actors”).

i don’t think the split is as clear as that. a Create activity may end up in the “home timeline” in some fashion, but it also ends up in the “notification timeline” if you “hit the bell” on someone’s profile in mastodon. similarly, a Like activity can be shown in the “home timeline” by some implementations a la facebook likes showing up in the feed, but other implementations don’t show them (or don’t send them). and an Announce could end up in both, if the Announce.object.attributedTo is yourself. so to me the split is entirely arbitrary and implementation-specific.

i agree this would probably be a more straightforward approach for what seems to be the use case being described (especially as mastodon for example lets you “dismiss” notifications). but it’s hard to tell because there are a lot of separate concerns mixed in here, like read receipts and multi-client use cases. if it were a single client and you didn’t care about tracking read status (i.e. a one-time dismissal is fine), then sure, a notifications collection that adds certain incoming activities then lets you Remove one or RemoveAll might be suitable. the question is more whether these activities need to pass through one’s outbox first (perhaps addressed to a collection, although the outbox delivery algorithm is bugged wrt Collection types), or if they can be POSTed directly to the collection endpoint instead.

Thanks @steve and @trwnh for the feedback — it’s converging nicely.

Yes, that brings us back to the question of whether the server is an actor. This is an issue on which the community clearly cannot agree. In my implementation, the server (i.e. the running AP server instance) is almost certainly an actor.


Here’s where I’m heading with the next revision of FEP-34ec:

Drop the Notification type entirely. The collection holds references to existing activities — a Like IS the notification, no wrapper needed.

Keep it as an OrderedCollection (reverse chronological by server receive time), because stable ordering is needed for pagination and server-side filtering via FEP-34c1.

Dismiss mechanism: Remove to dismiss a single notification, RemoveAll (FEP-db70) for batch dismiss — optionally with a FEP-34c1 filter (e.g. dismiss all Likes). The activity itself is not deleted, only removed from the collection.

Server decides which incoming activities become notifications — just like every implementation does today, but now behind a standardized C2S endpoint instead of proprietary APIs.

That seems to be a simple, pragmatic approach that many people can live with.

sounds fine except maybe this bit

this will not degrade gracefully – a consumer which understands RemoveAll as removing everything from the collection will arrive at wildly different and more destructive results if they don’t understand the filter.

if you flipped it so it was a RemoveMatching activity that simply matches everything, that could maybe work (although you’d still need to signal which filter options are supported).

also one last thing that would help is some conceptual clarity on what a “notification” is, as @stevebate points out:

it’s a really fuzzy term right now, but the definition could look something like this:

  • a notification is an activity interacting with an object attributed to the current actor (?)
  • the notifications collection contains all such activities (depending on the definition above)
  • a notification is added to the collection by a client (which may be internal to the server) checking for matching criteria
  • a notification can be dismissed by removing it from the collection, while remaining in the inbox

the only problem i see having written this down is that the notifications collection and the inbox collection are nearly identical, aside from the fact that activities can be dismissed/removed from the notifications collection but not from the inbox collection. perhaps there’s room to explore this issue within activitypub/ldn as was discussed previously (with Inbox and notification persistence, permanence, integrity · Issue #17 · w3c/ldn · GitHub and Requires receivers store everything, even junk · Issue #22 · w3c/ldn · GitHub having been filed previously, although the conclusion there was “filter spam notifications before persisting them in the inbox” and there wasn’t a specified way to clear or dismiss notifications that were already processed. i guess if the inbox implements LDP you can send an http DELETE for the notification IRI, but in AP you can’t really do that – the nearest analogue would be POSTing a Remove activity targeting your inbox maybe?)