Outbox listeners
Fedify can route POST requests to an actor's outbox through typed listeners. This is useful when you want to accept ActivityPub client-to-server activities from your own clients without exposing a separate non-standard API.
This guide covers POST /outbox. To serve GET /outbox, use the Collections guide.
Registering an outbox listener
With Fedify, you can register outbox listeners per activity type, just like inbox listeners. The following shows how to register a listener for Create activities:
federation
.setOutboxListeners("/users/{identifier}/outbox")
.on(Create, async (ctx, activity) => {
await savePostedActivity(ctx.identifier, activity);
await ctx.sendActivity(
{ identifier: ctx.identifier },
myKnownRecipients,
activity,
);
})
.authorize(async (ctx, identifier) => {
const session = await verifyAccessToken(
ctx.request.headers.get("authorization"),
);
return session?.identifier === identifier;
});The setOutboxListeners() method registers the outbox path, and the on() method registers a listener for a specific activity type. The authorize() hook runs before the listener and can reject unauthorized requests with 401 Unauthorized.
Fedify also rejects a posted activity if its actor does not match the local actor who owns the addressed outbox.
TIP
If you need to handle every activity type, register a listener for the Activity class. Unsupported activity types can also be left unhandled, in which case Fedify responds with 202 Accepted without dispatching a listener.
NOTE
The URI Template syntax supports different expansion types like {identifier} (simple expansion) and {+identifier} (reserved expansion). If your identifiers contain URIs or special characters, you may need to use {+identifier} to avoid double-encoding issues. See the URI Template guide for details.
Looking at OutboxContext.identifier
The identifier property contains the identifier from the matched outbox route. Fedify does not infer anything more specific than that.
.on(Create, async (ctx, activity) => {
console.log(ctx.identifier);
console.log(activity.id?.href);
});Federating posted activities
Fedify does not federate client-posted activities automatically. If you want to deliver a posted activity, call sendActivity() or forwardActivity() explicitly inside your outbox listener.
federation
.setOutboxListeners("/users/{identifier}/outbox")
.on(Create, async (ctx, activity) => {
await ctx.sendActivity(
{ identifier: ctx.identifier },
recipients,
activity,
);
});If the client already signed the posted JSON-LD with Linked Data Signatures or Object Integrity Proofs and you want to preserve that payload verbatim, use forwardActivity() instead of round-tripping through Fedify's vocabulary objects:
federation
.setOutboxListeners("/users/{identifier}/outbox")
.on(Activity, async (ctx) => {
await ctx.forwardActivity(
{ identifier: ctx.identifier },
recipients,
{ skipIfUnsigned: true },
);
});If a listener returns without calling one of these delivery methods, Fedify logs a runtime warning. The @fedify/lint package also provides a lint rule for the same mistake; see Linting for details.
TIP
Explicit delivery keeps outbox listeners symmetric with inbox listeners: Fedify never guesses the recipient list for you, so applications can reuse their own caches and delivery policies.
Handling errors
You can attach an error handler to outbox listeners. It receives the outbox context along with the thrown error:
federation
.setOutboxListeners("/users/{identifier}/outbox")
.on(Activity, async () => {
throw new Error("Something went wrong.");
})
.onError(async (ctx, error) => {
console.error(ctx.identifier, error);
});Current scope
Outbox listeners currently provide the routing and authorization surface for client-to-server posting, but the rest of the server-side behavior remains application-defined.
In particular, Fedify does not currently do the following for you:
- Persist the posted activity in your outbox collection
- Generate IDs or
Locationheaders for newly posted activities - Wrap non-
Activityobjects inCreateautomatically - Federate anything unless your listener calls
ctx.sendActivity()orctx.forwardActivity()
If you need full GET /outbox support as well, combine this guide with the Collections guide.