FEP-c3ab: Referencing existing objects in client-to-server Create

I have written a draft FEP and would like comment before opening the pull request.

The situation. Client-to-server submission is described for an object that is born with its activity: a client posts a Note, the server wraps it in a Create, and both are given identifiers. Where federation is added to a content store that already exists, the order is reversed, and that is the normal case for a content management system. The store assigns an identifier, an author and an audience when the content is written; a plugin or module adds federation later; so an object can sit there for years before any actor decides to publish it. Publishing older content, publishing selectively, and publishing under one of several actors are then ordinary requests.

For those servers the client has nothing new to submit. It wants to name something the server already hosts:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Create",
  "actor": "https://example.org/actors/alice",
  "object": "https://example.org/articles/123"
}

The syntax is not new — this is the shape of the Minimal Activity example, with both properties as URIs. What is missing is what an outbox should do with it. Section 6.2.1 is written throughout for an object the server is about to create, including the requirement to attach an identifier to the wrapped object, and says nothing about one that already has an identifier. w3c/activitypub#438 asks a neighbouring question and has been open since 2024. MediaUpload reached for the same shape in 2017 and has not been edited since.

What the draft says. Most of it follows from roles the specifications already define: do not give the referenced object a new identifier; take the activity’s addressing from the object rather than from the request; reject a request that carries its own to/cc/bto/bcc/audience rather than ignoring it, because ignoring it lets a client believe it published narrowly when it did not; refuse a reference the server cannot resolve rather than creating something to satisfy it; and check two permissions separately — permission to submit to that outbox, and permission for that actor to publish that object.

The part I think is new. Because such a submission carries no document, actor and object describe it completely, so a server can recognise a repeat as a retry. That is not generally possible in client-to-server: two submissions carrying identical documents may be two genuine posts. It does depend on the server’s own policy — if a server lets the same actor publish the same object more than once, the reference alone does not distinguish a retry from a new publication — but where that policy exists, the submission is idempotent and the server records nothing the second time.

Which produces the one place the draft departs from a MUST. Section 6 requires 201 Created. When the server recorded nothing, 201 asserts a creation that did not happen, so the draft recommends 200 OK with the existing activity’s Location. The same reasoning extends to a Delete naming an object by URI, with the complication that a deletion after an Undo(Delete) is a new transition rather than a retry.

What it deliberately leaves alone. How many Create activities one object may have, and who may publish them. A server may record one and treat its actor as the publishing authority; a server may accept one from each of several actors and call the object jointly published. Both should be able to implement this.

Questions.

  1. Is a referenced Create a reasonable client-to-server submission at all, or should publishing an existing object stay out of band?
  2. Is rejecting request-side addressing right, or does someone have a case for merging it?
  3. Is 200 on a recognised retry acceptable as a documented exception, or is there a better answer I have not found?
  4. Has anyone hit this already and solved it differently? I would rather adopt an existing answer than add one.

We have this implemented in a WordPress plugin set and covered by tests; it is not in a released version yet. Draft text follows in the pull request once there is a discussion link to point at.

1 Like

Is a referenced Create a reasonable client-to-server submission at all, or should publishing an existing object stay out of band?

I think this is a reasonable submission, although a different activity might be preferable. In one of my FEPs I proposed a Notify activity with similar semantics: it notifies recipients about the object. Something between Create and Announce.

Is rejecting request-side addressing right, or does someone have a case for merging it?

This sounds like an implementation detail. Generic servers may allow an invalid activity as long as it doesn't impersonate another local actor. Non-generic servers may need to have stricter validation procedures.

It’s reasonable. But is there any reason why not consider an Announce activity? It’s perfectly fine to have an object be “created” beforehand and then later announce its existence, even much later.

It’s also possible to send the Create notification with any published date, even a different published date than the object.

An Activity can have different addressing than its object. The addressing of the activity controls where the Activity will be delivered. The addressing of the object might control who can fetch the object. The former should probably be a subset of the latter, and the ActivityPub specification suggests merging the audiences, but they don’t have to be the same.

I would still use a 201 Created if you are talking about a POST request to the outbox. There, the 201 status indicates that the activity was published, not that the object was created.

This might be clarified upstream within the ActivityPub specification soon, if that is a suitable answer to you. https://github.com/w3c/activitypub/issues/438 is on the 1.1 milestone. The idea of what exactly it means to “create” an object within ActivityPub is something that we need to clarify for the next revision of the specification. See also https://github.com/w3c/activitypub/issues/662 for tracking this issue.

@trwnh I agree that Announce seems like the best option for this scenario. /cc @jiwoon

Thank you — and thank you for the pointer to FEP-aef6, which I had not read.

If I understand it, Notify answers “deliver this object to a requester without a GET” — a reply to a Request, for cases where the origin cannot serve the object itself, such as an offline node or end-to-end encrypted content. That is a different question from recording a first publication, even though the shape resembles Create. I will cite it as adjacent work rather than as an alternative, but tell me if you meant it to cover publication too.

On rejecting request-side addressing being an implementation detail: I think you are right about the strictness, and I was treating a validation choice as though it were the rule. What I would still like written down is the invariant rather than the check — that a client cannot end up having published to a wider audience than it asked for. How strictly any given server validates against that, and how much an intentionally generic server tolerates, seems like exactly the local decision you describe.

The harder half is that I am no longer sure what the invariant should be. trwnh’s framing in the next post — the activity’s addressing as a subset of the object’s — is better than what the draft says, but the comparison is not a subset of URI lists: an object addressed to Public and a request naming one actor is narrower in effect while sharing no addresses at all. I would rather leave that open for now than write a test I cannot defend.

1 Like

Thank you — this is more useful than I expected, and two of the points change the draft.

On 201. Agreed, and I should have been clearer: 201 refers to the activity being published, not to the object being created. A Delete makes it sharpest — the 201 there says a Delete activity was recorded, and says nothing about whether the object now answers with a Tombstone, a 404 or a 410. Running that sequence out:

POST Delete        → 201, Location: .../activities/1
POST Delete again  → no new activity recorded
Undo(Delete)       → 201, Location: .../activities/2
POST Delete again  → 201, a new transition, Location: .../activities/3

The second line is the only one I am asking about. The first submission records an activity and gets 201; the question is what to return for a POST the server has recognised as a retry, where it recorded nothing.

Section 6 requires 201 Created for a submission without qualification, which reads naturally as long as an accepted submission records an activity. RFC 9110 defines 201 as meaning the request “has resulted in one or more new resources being created”, and defines the content of a 200 for a POST as “the status of, or results obtained from, the action”. So where a server accepted the submission and recorded nothing, reading Section 6 as covering that case obliges it to say something about its own request that is not true, while the honest answer is a status Section 6 does not mention. That is less a deviation than a case the requirement was not written for, and leaving it undocumented means every server that needs it invents its own answer quietly.

It is also not special to a referenced Create, now that I look at it. Whenever an outbox recognises a submission as a retry and records no new activity, it can answer 200 and identify the existing one — Follow, Like and Block are the familiar cases. Which submissions count as retries is a separate question, and a contested one, as w3c/activitypub#381 and #485 show; the draft proposes the response for a referenced Create without defining deduplication rules for anything else.

There is a practical reason to identify the existing activity rather than say nothing about it: that id is what a client needs later to address an Undo, which is close to what #485 describes senders losing. A C2S response obviously cannot settle how a remote server deduplicates on its side, but it can at least tell its own client which activity is the authoritative one.

So: do you mean replaying the original 201 and Location for that second POST, or something else? I would rather understand the proposal before revising.

On Announce. You are right that the draft never says why not, which is a gap rather than a disagreement — and @steve_bate, this is the answer to your /cc as well. Announce is a reasonable choice when the goal is to draw attention to an object. The goal here is narrower: to record that an actor published this object to the fediverse for the first time. That record is what establishes attributedTo and what later Update and Delete activities refer back to — which is also why Update cannot be treated the same way, since the same actor may legitimately update the same object many times.

Your own observation supports the shape, incidentally: an object may be created beforehand and the Create sent later, with its own published date. That is exactly the case the draft is about. I will add a section on why Create here, and on when Announce is the better fit.

On addressing, I was too strict. “The addressing of the activity controls where the activity will be delivered; the addressing of the object might control who can fetch the object; the former should probably be a subset of the latter” states it better than the draft does. What I meant to prevent is a client supplying a narrower audience and the server publishing to the object’s wider one, so the client believes it published narrowly when it did not. Refusing every addressing property also forbids delivering more narrowly, which I did not intend.

What I am less sure of is how a server should check it, because the comparison is not a subset of URI lists. If the object is addressed to Public and the request names a single actor, the delivery is narrower in effect even though that actor may appear nowhere in the object’s to. So I would like to allow narrowing and work out the test, rather than write one into the draft today.

Lastly, w3c/activitypub#662 looks closely related from the other direction: it asks what a server must do for an object that “is created”. The referenced case may be a useful data point there, because what the server owes the object is nothing — no identifier is assigned and it is already persisted — while it still checks authorisation and records a new activity. Separating those two is what makes the response question appear at all.

I will cite it as adjacent work rather than as an alternative, but tell me if you meant it to cover publication too.

I'd like to cover / generalize as much as possible.

The Notify activity in FEP-aef6 means: "there's an object here, but it was created earlier". Whereas my understanding of Create is "this object has been created". That's why I picked a different activity name.

The Notify activity also seems to be a good fit for your use case. If that is true, then we should probably align our FEPs. I am fine with either re-purposed Create or Notify.

Thank you — and yes, I would like to align where the shapes really are the same.

In aef6 as written, Request asks for an object and Notify delivers it. In c3ab, the client asks its own server to publish an object the server already hosts, and the outbox response identifies the publication activity. Even if we chose a different activity type for that publication, the retry question would remain: if the server records no new activity, how should it identify the one it already recorded? The draft proposes 200 OK with the existing activity URI in Location. That is the part I care most about getting agreement on, and it does not depend on what the activity is called.

On the name, your distinction is the crux. Notify means “there is an object here, but it was created earlier”; Create means “this object has been created”. Both are true of my case from different sides — the object was written in a content store years ago, and this server has never published it. What the activity records is the latter: the first federated publication of that object by that actor. The authoring timeline and the publication timeline are different, and Create is accurate on the one the activity is about. There is a practical half too: an activity type receivers do not implement is generally dropped, and the point of this case is getting existing content onto the network.

I should correct something I put badly in my earlier reply, since it belongs here: I wrote that the Create “establishes attributedTo”. That is wrong and contradicts the draft’s own first requirement. A referenced Create must not replace or extend attribution the object already carries; it records the publication, and later Update and Delete activities target the object, not that record.

Where your shape is clearly right is delivering an object a requester cannot fetch — which is not as remote from me as I assumed, since a followers-only object is not retrievable by GET from us either.

So the alignment I would find useful is narrower than the activity name: both of us describe an activity whose object already exists and must keep the identifier it has. That rule should read the same whichever activity carries it, and I would happily take wording from yours.

This discussion helped me see that FEP-c3ab is answering two separate questions:

  1. Referencing an existing object. When Create.object is a URI for an object the server already hosts, how should the outbox handle the object’s identifier, attribution, and addressing?
  2. Responding to a recognised retry. If the outbox has already recorded the activity for this submission and records nothing new, what should it return? The draft proposes 200 OK with the existing activity URI in Location. The first submission still gets 201 Created with the new activity URI.

Create versus Announce or Notify is a question about how to express the publication. It doesn’t settle the retry question. I’ve revised the summary in my working draft to make that distinction explicit.