# ActivityPub for a publication: a Fedify provider with WebFinger, NodeInfo, signed inboxes and no unsigned mode

## ActivityPub for a publication: a Fedify provider with WebFinger, NodeInfo, signed inboxes and no unsigned mode

Red Cup Engineering's ActivityPub provider turns a list of publications into a fediverse actor. It uses the Fedify library directly for wire parsing, WebFinger, NodeInfo 2.1, HTTP signatures, Data Integrity proofs and delivery, supports `Organization`, `Service`, `Group`, `Application` and `Person` actors, and has no unsigned-admission mode: an inbound activity that fails signature verification is refused. This article describes the provider's design from its source and from a loopback run we did on 2026-10-22, and it states plainly which parts are not yet live for this site.

### Is the engineering actor live?

Not as of 2026-10-22. This site declares `@engineering@engineering.561.group` in its metadata (the `fediverse:creator` meta tag and a `rel="me"` link on every page), but when we requested `https://engineering.561.group/.well-known/webfinger?resource=acct:engineering@engineering.561.group`, the response was a 404 page, not a JSON resource descriptor. The organization's actors are being launched separately on Cloudflare Workers. We write about the provider's design here, and we will not describe an account as followable until WebFinger answers. You can check the same thing yourself:

```bash
curl -s 'https://engineering.561.group/.well-known/webfinger?resource=acct:engineering@engineering.561.group'
```

For contrast, the market at `market.561.group` does answer an ActivityPub request today: its `Service` actor at `/actors/market` and WebFinger for `acct:market@market.561.group` are live, and its outbox publishes 130 `Offer` activities ([One catalog, five machine faces](https://engineering.561.group/x402-one-catalog-machine-interfaces/)). That actor is a read-only projection; it is a different composition from the signed-inbox provider described below.

### What does the provider own?

Per the package documentation, `@red-cup-engineering/activitypub-services-section` (version 0.11.0) "owns ActivityPub, ActivityStreams 2.0, WebFinger, NodeInfo 2.1, signature/proof verification, protocol idempotency, and delivery". The customer supplies four things: the actor description, persistent protocol keys, an accepted-arrival callback and an outbox view. The provider "does not own or reinterpret their" domain theory. And, in one sentence worth quoting: "There is no unsigned-admission mode and no umbrella protocol fallback."

Fedify does the protocol work. The house modules around it do the document work:

| Module | Job |
| --- | --- |
| `./as2` | ActivityStreams 2.0 builders for every Vocabulary activity type, notes, articles, actors, tombstones, collections; refuses an actor without `inbox` and `outbox` and a `Block` addressed to Public |
| `./recipients` | Delivery targets per ActivityPub 7.1.1 and 5.6: `to`, `bto`, `cc`, `bcc`, `audience`; Public is never delivered to, the author is excluded, own followers are expanded, shared inboxes are deduplicated, and `bto`/`bcc` are stripped before delivery |
| `./publication` | A small composition seam for a static site: actor keys plus a `listPublications()` function |
| `./inbound` | Reads what was admitted; see [Paraconsistent federation](https://engineering.561.group/activitypub-paraconsistent-inbox/) |
| `./journal-custody`, `./flatfile-kv`, `./flatfile-queue` | Durable local state: verified inbox arrivals, followers, outbox and pending deliveries survive a restart |

### How do articles become ActivityPub objects?

The publication profile takes records, each with a same-origin `url`, a `title`, rendered HTML `content`, and optionally a summary, dates and tags, and projects them into public `Create{Article}` activities in the actor's outbox:

```js
import { createPublicationActivityPubProvider } from "@red-cup-engineering/activitypub-services-section/publication";
import { activityPubApplication } from "@red-cup-engineering/activitypub-services-section/app";

const provider = createPublicationActivityPubProvider({
  origin, identifier, actorName, summary, keyPairs, listPublications,
});
const app = activityPubApplication(provider);
```

(From the package README.) The profile reuses the provider's WebFinger, NodeInfo, actor and outbox routing. `activityPubApplication(provider)` returns an application with one manifest-visible resource per exact route: WebFinger, NodeInfo, inbox, actor, outbox and post objects, plus followers when follower custody is configured. It deliberately does not claim a broad `/actors/*` prefix, so it cannot swallow a neighboring route. For carriers that are not that application, `matchesActivityPubRequest(request, provider)` exposes the same exact ownership decision, and "intentionally does not match neighboring `/.well-known/*`, `/actors/*`, or site artifact routes".

Inbound federation is explicit. Without an `inbox: { onActivity, … }` contract, `POST /inbox` and `POST /actors/{identifier}/inbox` return `405`: "the profile never accepts a remote activity and silently drops it". A publication-only site can therefore stay publication-only, and adds durable inbox custody only when it has a workflow to receive into.

### What does a run look like?

We started a local actor on loopback with a freshly generated key, using the provider's documented local-server code path, and queried it. The keys were throwaway and the origin was `http://127.0.0.1:8031`, so this is a protocol demonstration, not federation with a peer.

WebFinger returns a standard JRD:

```json
{"subject":"acct:alice@127.0.0.1:8031",
 "aliases":["http://127.0.0.1:8031/actors/alice"],
 "links":[{"rel":"self","href":"http://127.0.0.1:8031/actors/alice","type":"application/activity+json"}]}
```

The actor document (abbreviated) has `type: "Service"` by default, its `inbox`, `outbox` and `followers` URLs, `preferredUsername`, a `publicKey` for HTTP signatures (fields `id`, `type`, `owner`, `publicKeyPem`) and an `assertionMethod` entry of type `Multikey` with a `publicKeyMultibase` for Data Integrity proofs. NodeInfo 2.1 reports `"protocols":["activitypub"]`, `"openRegistrations":false` and one user.

Then we sent an **unsigned** `Create` to the actor's inbox:

```bash
curl -s -i -X POST http://127.0.0.1:8031/actors/alice/inbox \
  -H 'content-type: application/activity+json' \
  -d '{"@context":"https://www.w3.org/ns/activitystreams","id":"http://evil.example/a/1","type":"Create","actor":"http://evil.example/u/x","object":{"type":"Note","id":"http://evil.example/n/1","content":"hi"}}'
# HTTP/1.1 401 Unauthorized
# Failed to verify the request signature.
```

That is the "no unsigned mode" rule observed. Fedify verifies the HTTP signature before any house code sees the activity, and the activity's Data Integrity proof is verified before the arrival callback fires; the verified activity is then retained as an immutable, content-addressed record ([Content-addressed names](https://engineering.561.group/content-addressed-names-ni-rfc-6920/)).

### What actor types can it publish?

The accepted set is `Application`, `Group`, `Organization`, `Person` and `Service`. An organization such as an engineering company publishes as `Organization`; the local run above used the default `Service`. The 561.group apex site's documentation takes a conservative stance for its own actor: the provider must be "a separately keyed, custodied" instance that "must accept only `https://561.group` as its canonical origin", and a provider for `science.561.group` "is not substitutable". Static WebFinger JSON is explicitly "not an ActivityPub server".

### How are followers handled?

When the deployment supplies a followers adapter, an inbound signature-verified `Follow` addressed to the actor is retained, the follower is recorded, and a signed `Accept{Follow}` is sent to the follower's inbox. A verified `Undo{Follow}` from the same actor removes the record. The actor advertises a real `followers` collection that returns an `OrderedCollection` with a correct `totalItems` and bare actor URLs, the convention independent implementations follow. Without a followers adapter, none of this is wired and a `Follow` is retained as an ordinary unanswered arrival.

Honest limit: broadcasting new posts to the whole followers set is described in the source as not yet wired. It "needs each follower's inbox available at dispatch time". The only follower-bound delivery today is the `Accept`.

Outbound follows go through an authenticated `POST /actors/{identifier}/outbox`. The activity must name the current actor exactly and address only the same actor it asks to follow. A public, misaddressed, self-targeted or fan-out `Follow` is refused before immutable outbox retention, and the documentation warns that "an outbox handoff is not evidence the peer accepted a follow".

### How are many sites hosted?

A separate package, `@red-cup-engineering/activitypub-server` (0.2.0), is the composition root for hosting. Its `serveActivityPubSites({ sites })` takes already-created providers, one per canonical HTTPS origin, onto one listener. Each provider keeps its own custody, queue, keys, followers, inbox and outbox. "Duplicate or unknown origins refuse rather than falling back to another site's actor." Behind a TLS-terminating proxy, `trustProxy: true` is allowed only when the proxy preserves the public `Host`, supplies the verified scheme, and strips client-supplied forwarded headers; the host refuses to enable it on a non-loopback bind. Several separately keyed actors on one origin is designed but gated: the host refuses to start the mode until a published and installed provider version carries the per-actor credentials.

### What are the honest limits?

The loopback run exercised real Fedify signing and verification but not real DNS, TLS or WebFinger-domain federation against an independent host; the package documentation says the same of its own two-process test. The local flat-file queue and inbox scan are for development; a production deployment supplies its own queue and custody. Nothing here claims that any peer has followed, received or replied to an engineering actor.

If you want to see inbound federation with a twist (activities treated as claims, not commands), continue to [Paraconsistent federation](https://engineering.561.group/activitypub-paraconsistent-inbox/).
