Idea for a more capable relay server

Very often I hear from users and ex-users of the Fediverse, that they they complain about posts that never reached their accounts. For example they subscribed to a hashtag on two accounts on separate servers. Now they see that both servers deliver different posts.

A solution for that are relay servers. But not every admin wants to receive thousands of unwanted posts just receive the few ones with content that a user wanted.

A solution for that would be a relay server that would be able to deliver content based on some rules that the admin defined. This should be much more than just a list of hashtags. It should be possible to filter on content in any field of a message.

What do you think about this idea? I’m currently in the process of thinking about a way to communicate these filter conditions.

4 Likes

I think this is an excellent idea.

I’m currently in the process of thinking about a way to communicate these filter conditions.

@helge and I had a discussion recently regarding outbox filtering:

https://mitra.social/objects/019ba7e3-dc8d-bffd-2552-df355897970c

ActivityPub needs some kind of query language. Implementations can use it to request a subset of activities when following an actor (incl. relay actors), and to filter collections. I think FEP-6606 is a good starting point.

Would be good to have some unified search pattern structures. Concerning that discussion: When you are only interested in in a low number of posts, then it doesn’t make sense to receive all others as well.

1 Like

An activitypub relay based on rules makes sense. Filtering by hashtags, actors or message fields can reduce unnecessary traffic and improve consistency of delivery. Maybe FEP-6606 is a good basis for that approach: Convenience functions, too. For example.

Here's another interesting FEP from @steve where JSONPath-based syntax was proposed: https://codeberg.org/fediverse/fep/src/branch/main/fep/c195/fep-c195.md

I think this would be able to be accomplished through a protocol extension with the Relay Servers. I forked and updated Activity Relay (GitHub - thystra/Activity-Relay: Yet another powerful customizable ActivityPub relay server written in Go. · GitHub). Once we have an agreed upon protocol, I am willing to implement the protocol in Activity Relay. This would require a corresponding update to your social instance (e.g. Friendica, Mastodon, etc.) to communicate the filter requests.

As a relay operator, I want you (the instance operator) to communicate and manage your own filters, I don’t want to incur extra work managing a greay many individual server’s filter preferences.

With the help of AI, which I understand Codeberg doesn’t want AI, and that’s fine, someone else can post there, I have prepared a draft proposal to implement this filtering. It would require your instance to use RFC 9421 signed messages to (reasonably) validate that the server administrator is directing the filter requests.


Proposal: server-managed language and hashtag filters for ActivityPub relays

The goal is to let an instance subscribe to a relay while requesting only selected categories of public content—for example:

  • posts in one or more declared languages;
  • posts containing one or more hashtags;
  • posts matching either condition;
  • posts matching both conditions; or
  • the relay’s complete eligible feed.

This could be particularly useful for rare-language communities and smaller instances that want better federation coverage without importing everything handled by a large relay.

This is an early protocol proposal, not an existing ActivityPub standard.

The problem

Following the same hashtag from two instances can produce different results because each instance has a different federation graph. Neither instance necessarily learns about every relevant post.

Relays can improve that coverage, but a normal relay sends its entire eligible feed to every subscribing instance. An administrator may not want to receive thousands of unrelated posts just to obtain a small number of posts in a particular language or carrying a particular hashtag.

A filtered relay subscription could help:

  1. Participating servers continue sending eligible public activities to the relay.
  2. Each receiving server tells the relay which subset it wants.
  3. The relay delivers only matching content to that server.
  4. The receiving server remains responsible for applying its normal moderation and federation policies.

Filtering would reduce outbound relay traffic and receiving-server load. It would not make the relay a global crawler, and it could not discover posts that were never delivered to the relay.

Basic design

I propose a two-step process:

  1. Subscription: the server joins the relay through an RFC 9421-authenticated relay subscription.
  2. Configuration: after the subscription has been accepted, the same registered server actor sends a separate RFC 9421-authenticated delivery-policy request.

Separating these operations has several benefits:

  • ordinary relay subscription remains distinct from policy configuration;
  • an invalid filter cannot create a partial subscription;
  • filters can be updated without unfollowing and refollowing;
  • filters can be removed without ending the subscription;
  • manual approval of subscriptions remains possible;
  • policy updates can be idempotent;
  • relay implementations can version the filter extension independently.

Standards relationship

This proposal would not require changing ActivityPub or RFC 9421.

Instead, it would be an optional Fediverse relay-protocol extension built from:

  • ActivityPub and ActivityStreams actors and activities;
  • JSON-LD extensibility;
  • RFC 9421 HTTP Message Signatures;
  • RFC 9530 Content-Digest; and
  • a new, versioned relay-filter vocabulary.

If there is sufficient interest and implementation experience, this may be suitable for a Fediverse Enhancement Proposal.

Terminology

In this proposal:

  • Relay: the service receiving eligible public activities and distributing them to registered receivers.
  • Subscriber: a server-level ActivityPub actor registered to receive relay traffic.
  • Subscription actor: the exact Application or Service actor that joins the relay.
  • Delivery policy: the subscriber-controlled language and hashtag restrictions applied to its relay deliveries.
  • Declared language: language metadata supplied by the publishing server or author, rather than a language guessed by the relay.
  • Default policy: the relay’s behavior when a subscriber has not installed a custom policy.

The subscriber is a server actor, not an individual user account.

Why require RFC 9421?

I propose requiring RFC 9421 for both the subscription and all delivery-policy operations.

This is partly a modernization boundary rather than a claim that older signatures are necessarily cryptographically inadequate. A filter-capable subscriber would be explicitly indicating that it implements the modern protocol profile.

RFC 9421 provides a clear way to bind the request to:

  • the HTTP method;
  • request authority;
  • target URI;
  • content type;
  • RFC 9530 content digest;
  • creation and optional expiry times;
  • a nonce;
  • a key ID; and
  • an application-specific signature tag.

The relay would resolve the keyid to the server’s ActivityPub actor document and verify that the key is currently owned by the actor.

A valid signature proves control of the actor key. The subscribing server remains responsible for ensuring that only an authorized local administrator can request or change relay settings.

RFC 9421 support alone would not imply support for filtering. Filter capability and protocol version must also be advertised explicitly.

Application-specific signature profiles

To prevent one signed operation from being confused with another, the extension should use distinct RFC 9421 signature tags.

For example:

tag="activity-relay-subscription-v1"

for the join, and:

tag="activity-relay-preferences-v1"

for policy-management requests.

The exact names are open for discussion.

A state-changing request would cover at least:

"@method"
"@authority"
"@target-uri"
"content-digest"
"content-type"
"date"

It would also require:

  • created;
  • a short-lived expires;
  • a fresh nonce;
  • keyid;
  • the agreed signature algorithm; and
  • the operation-specific tag.

The relay must reject replayed, expired, incorrectly targeted, or incompletely covered requests.

Step 1: relay subscription

A server joins using its server-level ActivityPub actor and an RFC 9421-signed Follow.

Conceptual example:

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://example.org/ns/activity-relay/v1"
  ],
  "id": "https://social.example/activities/relay-follow-123",
  "type": "Follow",
  "actor": "https://social.example/actor",
  "object": "https://relay.example/actor",
  "relayProtocolVersion": 1,
  "capabilities": [
    "delivery-preferences-v1"
  ]
}

The relay would:

  1. verify the RFC 9421 signature and RFC 9530 content digest;
  2. resolve the signing key and its owner;
  3. require the activity actor to be the verified key-owning actor;
  4. validate the actor as a supported server-level actor;
  5. apply the relay’s blocking, approval, and admission policies;
  6. record the actor ID, inbox, domain, and subscription identity;
  7. return or deliver Accept.

A delivery policy cannot create a subscription. Policy operations are authorized only after this join is complete.

Whether the filter extension should support both traditional Public-collection subscriptions and relay-actor subscriptions is open for discussion. The important requirement is that the relay records one exact server actor as the subscription principal.

Step 2: capability discovery

The relay actor should advertise whether it supports delivery policies.

A conceptual actor extension might look like this:

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://example.org/ns/activity-relay/v1"
  ],
  "id": "https://relay.example/actor",
  "type": "Application",
  "relayProtocol": {
    "versions": [1],
    "subscriptionSignatureProfile": "rfc9421",
    "deliveryPreferences": {
      "versions": [1],
      "endpoint": "https://relay.example/actor/preferences",
      "dimensions": [
        "hashtag",
        "declared-language"
      ],
      "matchModes": [
        "any",
        "all"
      ],
      "maximumHashtags": 100,
      "maximumLanguages": 20,
      "maximumPolicyBytes": 16384
    }
  }
}

The vocabulary URL above is only a placeholder. A final extension should use a stable, implementation-neutral namespace.

Capability discovery prevents a server from assuming that RFC 9421 support automatically means filter support.

Step 3: install or replace a policy

After receiving Accept, the subscribing server sends an RFC 9421-signed PUT to the advertised preference endpoint.

PUT would replace the complete policy. Complete replacement is preferable to a sequence of “add tag” and “remove language” operations because it is easier to make idempotent and safe to retry.

Conceptual request:

PUT /actor/preferences HTTP/1.1
Host: relay.example
Date: Tue, 01 Oct 2026 12:00:00 GMT
Content-Type: application/activity+json
Content-Digest: sha-256=:...:
Signature-Input: relayprefs=("@method" "@authority" "@target-uri" "content-digest" "content-type" "date");created=...;expires=...;nonce="...";keyid="https://social.example/actor#main-key";alg="rsa-v1_5-sha256";tag="activity-relay-preferences-v1"
Signature: relayprefs=:...:

Conceptual body:

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://example.org/ns/activity-relay/v1"
  ],
  "type": "RelayDeliveryPolicy",
  "protocolVersion": 1,
  "id": "https://social.example/relay-policies/relay.example",
  "actor": "https://social.example/actor",
  "relay": "https://relay.example/actor",
  "revision": 3,
  "delivery": {
    "mode": "filtered",
    "match": "any",
    "hashtags": [
      "cymraeg",
      "fediverse"
    ],
    "languages": [
      "cy",
      "br"
    ],
    "unknownLanguage": "exclude"
  }
}

The body’s actor must exactly match the verified signing actor and the actor stored for the active subscription.

The body’s relay must identify the receiving relay actor.

Delivery modes

The first protocol version could support two modes.

Complete feed

{
  "delivery": {
    "mode": "all"
  }
}

This requests all traffic that is otherwise eligible under the relay’s own policies.

It does not bypass:

  • blocked-domain rules;
  • limited-domain rules;
  • public-address rules;
  • supported-activity rules;
  • person-only rules;
  • loop prevention;
  • queue limits;
  • relay-administrator restrictions.

Filtered feed

{
  "delivery": {
    "mode": "filtered",
    "match": "any",
    "hashtags": ["cymraeg"],
    "languages": ["cy"],
    "unknownLanguage": "exclude"
  }
}

An empty filtered policy should be rejected. It should not ambiguously mean either “all traffic” or “no traffic.”

A future protocol version could add an explicit none or paused mode if there is a demonstrated need.

Matching semantics

Matching across filter dimensions

With:

{
  "match": "any",
  "hashtags": ["cymraeg"],
  "languages": ["cy"]
}

the post would be eligible if:

  • it contains the #cymraeg hashtag; or
  • its declared language matches cy.

With:

{
  "match": "all",
  "hashtags": ["cymraeg"],
  "languages": ["cy"]
}

the post would be eligible only if:

  • it contains the #cymraeg hashtag; and
  • its declared language matches cy.

Only nonempty configured dimensions participate in the all calculation.

Within a list, matching would initially use “any listed value.” For example:

{
  "languages": ["cy", "br"]
}

means Welsh or Breton, not Welsh and Breton simultaneously.

If there is demand to require multiple hashtags on one post, that could be added explicitly in a later version rather than overloading the initial semantics.

Hashtag matching

The first version should match structured ActivityStreams hashtag metadata.

Conceptual object:

{
  "type": "Note",
  "tag": [
    {
      "type": "Hashtag",
      "name": "#Cymraeg",
      "href": "https://social.example/tags/cymraeg"
    }
  ]
}

The relay should match the structured hashtag name, not scrape hashtags from rendered HTML.

Suggested normalization:

  1. trim surrounding whitespace;
  2. remove one optional leading #;
  3. apply a specified Unicode normalization;
  4. perform Unicode-aware case-insensitive comparison;
  5. reject empty or excessively long names;
  6. sort and deduplicate stored values.

The href should not define hashtag identity because different servers may generate different tag URLs for the same textual hashtag.

The first version should not support:

  • regular expressions;
  • substring matching;
  • arbitrary HTML inspection;
  • user-provided executable expressions;
  • remote taxonomy lookups.

Language matching

The first version should use declared language metadata rather than automatic language detection.

Possible sources include standard ActivityStreams natural-language maps such as contentMap and explicitly documented compatibility fields.

Language identifiers should be validated and canonicalized as BCP 47 language tags.

A post may contain more than one declared language. It matches when any declared language matches one of the subscriber’s selected languages.

The protocol must specify whether selecting a base language such as:

pt

also matches more specific tags such as:

pt-BR
pt-PT

My preference is documented basic-language matching, but this needs interoperability discussion.

Unknown language

Language metadata is not universally supplied. A filtered policy therefore needs explicit unknown-language behavior.

{
  "unknownLanguage": "exclude"
}

would mean that a post with no usable declared language does not satisfy the language dimension.

{
  "unknownLanguage": "include"
}

would allow an unknown-language post to satisfy that dimension.

For a rare-language relay subscription, exclude will usually be the intended choice, but it can produce false negatives when publishing software omits language metadata.

Automatic language detection should not be silently substituted for declared language. If detection is ever standardized, it should be a separately advertised capability with defined model, confidence, resource, and compatibility behavior.

Authorization rules

A valid signature alone is not sufficient. A policy request should be accepted only when:

  1. the RFC 9421 signature is valid;
  2. all required components are covered;
  3. the RFC 9530 digest matches the exact body;
  4. the signature is fresh and unexpired;
  5. the nonce has not previously been used;
  6. the key is currently published by the resolved ActivityPub actor;
  7. the body’s actor equals the verified signing actor;
  8. that exact actor has an active, complete relay subscription;
  9. the actor is authorized to modify that subscription;
  10. the requested relay ID is the local relay actor;
  11. the protocol version is supported;
  12. the revision is newer than the stored revision;
  13. the policy satisfies all size and vocabulary limits.

Authorization should be bound to the exact actor ID, not merely to a matching domain.

A normal user actor on the same domain must not be able to modify a server actor’s shared-inbox subscription.

Key rotation should be permitted when the actor ID remains unchanged and the actor’s current authenticated document publishes the new key.

Policy responses

A successful response should return the effective normalized policy.

Conceptual response:

HTTP/1.1 200 OK
Content-Type: application/json
ETag: "policy-3-7a8b..."
{
  "protocolVersion": 1,
  "outcome": "updated",
  "actor": "https://social.example/actor",
  "relay": "https://relay.example/actor",
  "revision": 3,
  "effectivePolicy": {
    "mode": "filtered",
    "match": "any",
    "hashtags": [
      "cymraeg",
      "fediverse"
    ],
    "languages": [
      "br",
      "cy"
    ],
    "unknownLanguage": "exclude"
  }
}

Returning the effective policy lets the subscriber confirm the relay’s normalization, sorting, deduplication, and language-tag canonicalization.

Reading and deleting a policy

The advertised endpoint could support:

Method Meaning
GET Return the signing actor’s effective policy
PUT Create or completely replace the signing actor’s policy
DELETE Remove the custom policy and return to relay-default behavior
OPTIONS Return protocol and endpoint capabilities

All operations should require RFC 9421 authentication, including GET. Subscriber filter choices are operational information and should not be exposed publicly.

PATCH need not be supported in version 1.

Policy revisions and concurrency

Each replacement should include a monotonically increasing revision:

{
  "revision": 4
}

The relay should reject:

  • a lower revision;
  • a duplicate revision containing different policy bytes;
  • an update racing with subscription removal.

A repeated request with the same revision and same normalized policy may return an unchanged outcome. This makes retries safe after a lost response.

An HTTP ETag and If-Match mechanism could additionally prevent concurrent local administrators from overwriting each other, although the exact concurrency mechanism remains open for discussion.

Lifecycle consistency

Filtering a Create is easy. Correct handling of later activity is more difficult.

Suppose a subscriber requests Welsh posts:

  1. A matching Welsh Create is delivered.
  2. The author edits the post.
  3. The Update omits language metadata or removes the matching hashtag.
  4. The author later sends Delete.

The receiver must still receive the necessary Update and Delete. Otherwise it may retain stale or deleted content.

A conforming relay therefore needs a bounded routing ledger recording which subscribers received a canonical object.

Suggested behavior:

  • A matching Create establishes the object’s destination set.
  • Necessary Update, Delete, and Move activities are delivered to prior destinations even when the later activity no longer independently matches.
  • Policy changes apply to future admission decisions and do not rewrite already queued deliveries.
  • A newly matching Update should not be sent by itself unless the protocol defines how the receiver obtains a complete importable object state.
  • Historical posts should not be replayed merely because a subscriber changes its policy.
  • Ledger entries should expire after a documented bounded period.
  • The ledger should avoid storing post content unnecessarily.

The exact retention period and edit behavior require implementation experience, but lifecycle consistency should be part of the protocol rather than left undefined.

Subscription removal

When the server sends a valid Undo(Follow) or otherwise ends the subscription:

  • the receiver must be removed;
  • its active custom policy must be removed or durably disabled;
  • a racing policy update must not recreate the receiver;
  • the old actor must not be able to modify a later subscription belonging to a different actor;
  • queued-work and lifecycle-cleanup behavior must be defined.

A delivery policy must never create, restore, or implicitly approve a subscription.

Relay-operator authority

Subscriber policies only narrow the traffic a subscriber receives.

They must never override the relay operator’s:

  • blocked-domain policy;
  • limited-domain policy;
  • public-address policy;
  • actor-type restrictions;
  • supported activity types;
  • moderation rules;
  • reflection prevention;
  • queue limits;
  • fan-out limits;
  • emergency suspension.

A relay operator may need an administrative override to suspend or further restrict an instance. That override should take precedence over the subscriber’s requested policy.

The objective is to avoid requiring the relay operator to manually maintain normal per-instance hashtag and language selections. It is not to remove the relay operator’s ability to respond to abuse or operational problems.

Privacy and observability

Subscriber policies should not be publicly listed by default.

Relays should not place any of the following into unbounded logs or metric labels:

  • complete policy documents;
  • arbitrary hashtags;
  • actor IDs;
  • inbox URLs;
  • signature values;
  • nonces;
  • request bodies;
  • arbitrary language strings.

Metrics should use bounded result categories such as:

accepted
rejected_authentication
rejected_authorization
rejected_version
rejected_validation
stale_revision
subscription_missing

The relay may expose aggregate numbers of filtered and unfiltered subscriptions without revealing individual policies.

Error responses

The protocol should define a small, closed error vocabulary.

Examples:

{
  "protocolVersion": 1,
  "error": {
    "code": "subscription_missing",
    "message": "The signing actor does not have an active relay subscription."
  }
}
{
  "protocolVersion": 1,
  "error": {
    "code": "unsupported_policy_version",
    "supportedVersions": [1]
  }
}

Possible error codes include:

authentication_failed
actor_mismatch
subscription_missing
subscription_pending
unsupported_policy_version
unsupported_filter_dimension
invalid_policy
policy_too_large
too_many_hashtags
too_many_languages
stale_revision
revision_conflict
rate_limited
temporarily_unavailable

Error messages should be bounded and must not disclose keys, signatures, nonces, or private subscription state to an unauthenticated caller.

Backward compatibility

This should be an additive extension.

Existing relay subscriptions would continue to receive the relay’s default feed unless:

  • they adopt the new filter protocol; or
  • the relay operator changes local policy.

A possible compatibility matrix is:

Subscription Authentication Subscriber policy
Existing traditional subscription Legacy or RFC 9421 Relay default
Existing follower-style subscription Legacy or RFC 9421 Relay default
Filter-capable subscription v1 RFC 9421 required Server-managed language/hashtag policy

A relay could optionally require all new subscriptions to use the modern profile, but that would be a local operator decision rather than an inherent requirement for existing relay protocols.

Why not a relay-side control panel?

A relay-side control panel would make the relay operator responsible for maintaining every subscribing instance’s preferences.

It would also require:

  • user accounts or another administrator identity system;
  • sessions;
  • CSRF protection;
  • account recovery;
  • authorization and auditing;
  • proof that a human controls the affected instance;
  • ongoing UI maintenance;
  • another public attack surface.

The subscribing server already knows which local users are administrators. It is therefore a better place to present the configuration interface.

An ideal deployment would be:

  1. The server administrator enters languages and hashtags in their own server’s administration UI.
  2. The server stores the desired policy locally.
  3. The server signs the corresponding request using its registered server actor key.
  4. The relay authenticates the server actor and stores the resulting policy.
  5. The server periodically verifies that the relay’s effective policy still matches its desired state.

No human account on the relay is required.

Questions for implementers

I would especially appreciate feedback on the following:

  1. Should the extension support both Public-collection and relay-actor Follow styles, or define one modern join form?
  2. Should an RFC 9421 join be mandatory, or only RFC 9421 policy management?
  3. What stable JSON-LD vocabulary namespace should be used?
  4. Should match: "all" apply only across dimensions, or should requiring all listed hashtags also be supported?
  5. Should a base language such as pt match pt-BR and pt-PT?
  6. Which declared-language representations are sufficiently interoperable?
  7. What should happen when an Update newly matches a policy but the original Create was not delivered?
  8. How long should routing-ledger entries be retained?
  9. Should deleting a custom policy restore the relay default or temporarily suppress delivery?
  10. Should policy reads and responses also be signed by the relay?
  11. How should a subscriber prove continuity during server actor migration?
  12. Which existing server project would be interested in implementing the subscribing side?

Suggested implementation path

A cautious path would be:

  1. Document and collect real hashtag and language fixtures from several ActivityPub implementations.
  2. Implement bounded structured metadata extraction.
  3. Implement relay-wide filters first to validate matching behavior.
  4. Add lifecycle-consistent routing records.
  5. Publish an experimental protocol document and interoperability fixtures.
  6. Implement RFC 9421 subscription capability discovery.
  7. Implement signed GET, PUT, and DELETE policy operations.
  8. Implement the subscribing side in at least one server or plugin.
  9. Perform real server-to-relay interoperability testing.
  10. Revise the protocol based on two independent implementations.
  11. Submit a FEP once the wire format and semantics are demonstrated.

Summary

The central proposal is:

A server joins a relay using an RFC 9421-authenticated server actor. After the relay accepts the subscription, that same actor may install or replace a versioned delivery policy through a separate RFC 9421-authenticated request. The policy may request all eligible traffic or filter it by structured hashtags and author-declared languages. Subscriber policies may narrow relay delivery but may never override relay security or moderation policy.

This distributes filter administration to the subscribing instance, avoids creating relay-side user accounts, and gives the relay a well-defined authenticated server actor to which the policy belongs.

Feedback on the wire format, matching rules, lifecycle semantics, and interest from server implementers would be very welcome.