Federation
Federation (Phase 3)
Every local Actor is now reachable from the Fediverse through the standard
ActivityPub endpoints, all served without session or CSRF
(routes/activitypub.php, loaded outside the web middleware group), as
required by protocols meant to be consumed by other servers rather than
browsers:
- Discovery:
/.well-known/webfinger?resource=(resolvesacct:utente@dominio
or the Actor's canonical URL) and /.well-known/nodeinfo + /nodeinfo/2.1
(instance metadata and aggregate usage statistics, with no personal data). The
NodeInfo document always declares software.name: "openbook", the technical
version (config('openbook.version')) and a link to software.homepage, so
Fediverse tools that read NodeInfo correctly recognize the software behind
the instance (not a fork/derivative of other platforms). For the same reason
the User-Agent of outgoing requests
(config('openbook.federation.user_agent')) reports the real software
version, not a fixed value disconnected from it. These public endpoints (plus
the canonical profile/post/comment below, see content negotiation) also
expose the CORS header Access-Control-Allow-Origin: * (config/cors.php):
they are public documents by definition, and without that header a browser
would block cross-origin reads by client-side federated-software verification
tools.
- Content negotiation: profile (
/@utente), post (/posts/{uuid}), and
comment (/comments/{uuid}) pages return HTML to a browser and an
ActivityPub document (Person/Note/Tombstone) when the Accept header
requests application/activity+json or application/ld+json. Deleted
content is represented as a Tombstone rather than disappearing silently.
- Collections: each user's
outbox,followers, andfollowingare
exposed as paginated OrderedCollection/OrderedCollectionPage; the outbox
wraps public and unlisted posts in Create activities.
- Inbox: each user has a dedicated inbox (
/users/{utente}/inbox) and the
instance has a shared inbox (/inbox). Incoming requests are authenticated
with HTTP Signatures (Cavage draft, rsa-sha256: verification of
Signature, body digest, maximum skew of the Date header, match between
the signing Actor and the activity's actor field, with a key-refresh
attempt in case of rotation), validated in minimal form (content-type, size,
JSON depth) and deduplicated via a unique constraint on
remote_activity_uri. Valid activities are stored raw in inbox_items with
status pending: their semantic processing happens outside the HTTP
cycle (see Social federation below), so a
delivering peer is never blocked waiting for heavy processing.
- Fetching remote Actors:
RemoteActorResolverdownloads, validates, and
caches locally the Person document of a remote Actor (needed to verify
incoming signatures, for remote search, and to resolve actors cited by
activities); every outgoing fetch goes through SafeHttpClient, which
applies SsrfGuard to reject non-public URLs (private, loopback, reserved
IPs), requires HTTPS in production, limits redirects/timeout/response size,
and blocks *DNS rebinding* by pinning the connection to the already-validated
IP (CURLOPT_RESOLVE). The same protection also applies to outgoing
delivery requests (SafeHttpClient::post()), which never follow a redirect
(the HTTP signature is computed on the exact destination URL).
- Mastodon-compatible relays: administrators can configure relay inboxes
from Control panel → Relays, explicitly enable receiving and/or
publishing, and start or stop the subscription. Once accepted, Openbook
accepts public activities transported by that relay and adds its inbox to
the delivery fan-out for local public posts, comments, events, and event
comments. Relay traffic uses the normal ActivityPub queues, signatures,
retry/backoff policy, domain blocks, and duplicate protection; no additional
worker or cron entry is required. The administration page reports the last
successful exchange and the latest terminal delivery error. Relays can
generate a high volume of public content, so enabling one is always an
explicit instance-administrator decision. The normal periodic cron remains
sufficient for small installations; with a busy relay, separate permanent
workers are recommended so that incoming processing and slow remote
deliveries cannot block each other:
```bash
php /path/to/openbook/artisan queue:work --queue=inbox --sleep=1
php /path/to/openbook/artisan queue:work --queue=delivery --sleep=1
```
The workers can safely run alongside openbook:cron, but must be restarted
after each deployment so that they load the updated application code.
- Actor-based instance sources: the same Relay panel can subscribe to a
remote ActivityPub Application/Service Actor, as exposed by software such
as Mobilizon or Gancio. In this mode administrators can enter its federated
identity (for example @relay@instance.example) or its full Actor URI, not
the inbox URL: Openbook discovers, resolves and validates the Actor, follows
it and imports public objects transported through its Announce activities.
Remote applications may likewise follow Openbook's technical /relay Actor to
receive Announce activities for locally produced public posts, comments,
events, and event comments. Unlisted, followers-only, direct, remote, and
private-community content is excluded. Technical subscriptions and
announcements do not create user notifications, social boosts, or reaction
counters. The Actor exposes paginated /relay/outbox, /relay/followers,
and /relay/following collections for discovery and backfill.
- LitePub-compatible relays: relay hubs used by Pleroma/Akkoma and
compatible software can be configured by federated Actor identity or full
ActivityPub URI. Openbook performs the Actor Follow handshake, records the
reciprocal Follow required for outgoing fan-out, and transports local public
content through technical Announce activities. The administration page
distinguishes an accepted subscription that is still waiting for the
reciprocal Follow. Disabling publishing or unsubscribing stops new fan-out
immediately, including deliveries that were already queued; the normal
visibility, domain-block, deduplication and loop-prevention rules continue to
apply. Unlisted, followers-only, direct, remote and private-community content
is never published to a LitePub relay.
Social federation (Phase 4)
Activities accepted in the inbox (Phase 3) are now processed, and relevant
local actions are delivered to the remote servers involved: federation is
finally bidirectional.
- Inbox processing: each
InboxItemwith statuspendingis queued on
ProcessInboxActivityJob (inbox queue) immediately after receipt
(InboxController::receive(), after commit). InboxActivityProcessor
interprets the activity and produces the corresponding domain effect
always reusing the same application services as the local path
(FollowManager, ReactionManager, AnnounceManager), so the two paths
stay consistent:
- Follow toward a local Actor creates the row in follows (pending or
accepted depending on manuallyApprovesFollowers) and, if accepted
immediately, replies with an Accept;
- Accept/Reject complete a Follow originated by this instance toward a
remote Actor;
- Undo (of Follow, Like, or Announce) cancels the corresponding
relation or reaction;
- Like/Announce on a local post or comment update counters and generate a
notification, exactly like a local like/share;
- Create/Update with a postable object (Note, Page, Article,
Video, Image) cache the remote post or comment locally (tables
posts/comments, identified by the uri column), but **only if
relevant** to this instance (the author is followed by a local Actor, the
Note replies to content we already know, or it explicitly mentions a local
Actor): no remote content is stored "at random". HTML content is reduced to
plain text (RemoteContentSanitizer), preserving <a href> as
[label](url); images in attachment remain as remote URLs in the
gallery. Then it goes through the same safe rendering pipeline as local
posts;
- Update with a Person/Group object (another server notifying a change
to one of its users' profiles) updates the local cache of the remote Actor
directly (actors/actor_keys/actor_endpoints) by applying the embedded
document, with no extra HTTP fetch; accepted only if the id declared in the
document matches the signing Actor;
- Delete marks a local post/comment or its remote cached copy as deleted,
exactly like a local deletion (never a physical row delete, to preserve the
id).
- Outgoing activity delivery:
ActivityDeliverycomputes the set of
destination remote inboxes (deduplicated on sharedInbox when several
followers live on the same server) and queues a DeliverActivityJob for each
(delivery queue, afterCommit()). Each job signs the activity with the
sending local Actor's private key and sends it with SafeHttpClient::post();
a temporary failure (network error, 5xx response) is retried with increasing
backoff (1, 5, 15, 60, 360, 1440 minutes, configurable), while a permanent
error (SSRF violation, missing private key) fails immediately without retry.
It is wired into every point where a local Actor performs a federatable
action: FollowManager (Follow/Accept/Reject/Undo),
ReactionManager (Like/Undo), AnnounceManager (Announce/Undo,
delivered both to the sharer's remote followers and to the original author if
distinct), PostComposer/PostController (Create/Delete), and
CommentComposer/CommentController (Create/Delete, always delivered
also to the parent content's author as a direct recipient). Messages with
"direct" visibility are delivered only to explicitly mentioned Actors, never
to all followers.
- Queue and cron: the queue uses Laravel's database driver (
jobsand
failed_jobs tables, already present from the installer), consistent with
shared-hosting constraints (no permanent process, no Redis/RabbitMQ). The
openbook:process-inbox and openbook:deliver commands process the inbox
and delivery queues respectively with --stop-when-empty, so they exit on
their own instead of listening indefinitely; openbook:cron invokes both in
sequence, splitting a configurable maximum time budget, and is the command
meant to be scheduled (see Cron and periodic tasks).
- Search: the Search page (
/cerca?q=..., GET form) has two paths.
If the query is a federated address (utente@dominio, with or without a
leading @, acct:..., or a profile URL), it is resolved locally if the
domain matches this instance, otherwise via WebFinger + fetching the Actor
document (RemoteActorResolver::resolveByHandle()), then redirected to the
profile. If the query has a different shape (keyword, phrase, username
without domain), it runs a *local-only* search (LocalSearchQuery) on people
(username, display name, bio; respects discoverable), posts and comments of
local Actors (visibility and FEP-5feb indexable respected), and hashtags.
No Elasticsearch: case-insensitive LIKE with escaped wildcards, configurable
limits (OPENBOOK_SEARCH_MIN_LENGTH, OPENBOOK_SEARCH_PER_SECTION). A
resolved remote Actor has a convenience profile page (/attori/{id}, never a
canonical ActivityPub identifier) with statistics, optional biography, and a
follow button that starts the real Follow/Accept flow. Across the
interface (post cards, comments, notifications) authors are now shown via
Actor::displayName()/Actor::avatarUrl()/Actor::profileUrl(), which work
identically for local and remote actors. The profile page also fetches
(RemoteOutboxFetcher, cache with a separate TTL in
actors.posts_fetched_at) recent public posts from the Actor's real outbox
(with fallback to the Atom feed if the outbox is a stub, typical of Pixelfed),
so their content can be shown even if no local Actor follows them yet:
without this step a newly discovered profile would often have no posts,
because the inbox only caches content already considered relevant (see
known limitations below).
- Follower/following lists: the "Followers" and "Following" counters on
every profile (local or remote) are links to a paginated page with the real
list (FollowListQuery), shared between local profiles (/@utente/follower,
/@utente/seguiti) and remote Actors (/attori/{id}/follower,
/attori/{id}/seguiti with a redirect to the local profile if the Actor
turns out to belong to this instance). Each row shows a follow/unfollow
button consistent with the visitor's real status
(FollowManager::statusMapFor(), a single query for the whole page). The
"Communities" counter on the profile counts the Groups (local or remote) the
user has joined. Profiles also expose Posts / Photos tabs (roll of
images attached to visible posts).
Community (Phase 5)
Communities are ActivityPub Actors of type Group:
- Local: creation from the UI, slug
/c/{slug}, WebFingernome@dominio,
membership (Follow/Accept), members' post wall, outgoing Group Announce,
private communities with approval, delegated moderators. List at /community
with a Local / Remote switch.
- Remote (Lemmy, Friendica, …): search
nome@dominio, federated join,
profile /attori/{id} with a composer for members, ingestion of
Announce/Page (FEP-1b12). Local Actor URIs use the Mastodon scheme
/users/{username} for compatibility (Lemmy rejects ids with
percent-encoded @).
Interoperability and remote media (Phase 6)
Besides Note and Page, the inbox/outbox accept Article (WordPress
ActivityPub, WriteFreely), Video (PeerTube, including with attributedTo
Person+Group), and Image. Remote image attachments stay as https URLs in
media.remote_url (gallery and profile photo roll) without download onto the
instance. If the outbox is a stub (typical of Pixelfed: only totalItems), the
remote profile falls back to the Atom feed {actor}.atom. For Wafrn (empty
outbox) the public /api/v2/blog API is used. Threads (Meta) does not
expose posts in the ActivityPub outbox: on the remote profile you can only see
content already received in the inbox after a Follow (and only if the account
has enabled sharing to the Fediverse). The World section suggests remote
accounts to discover and, beyond the first 5, the full list at /mondo/scopri
with infinite scroll. SSRF, domain blocks, and HTTP signatures remain the
baseline security constraints.
Federated events
Openbook recognizes ActivityStreams Event objects received through signed
Create and Announce activities and keeps them separate from timeline
posts. The public Events section (/eventi) provides upcoming and past
event lists, local detail pages, location and time-zone information, media,
hashtags, search results, remote counters, and federated comment
threads. Visibility follows the event audience: public and unlisted events can
be opened by link, while followers-only and direct events require a recorded
local recipient. Deleted events remain as tombstones instead of becoming an
ambiguous 404 for users who were allowed to see them.
Authenticated users can create public or unlisted events from the Events
section or from their profile. The composer supports a cover image, Markdown
description, explicit time zone, physical/online/hybrid location, hashtags,
mentions, and open, moderated, or external participation. Local events can be
edited, cancelled, or deleted; Openbook publishes the corresponding
Create(Event), Update(Event), and Delete(Event) activities and exposes
them in both the Actor outbox and its dedicated events collection.
Users can send Like/Undo(Like) as an expression of interest and, where
supported by the origin, Join, Undo(Join), or Leave for an RSVP. Local
organizers receive notifications for new participants and can accept or reject
moderated requests. Event comments and nested replies are federated as Note
objects whose inReplyTo points to the Event or parent comment; comments also
support attachments, likes, notifications, and tombstone deletion. An event
can be shared in a private Openbook conversation only when the recipient
already belongs to its audience; sharing never grants access to restricted
content. Events marked sensitive keep descriptions and media behind an
explicit reveal control. Hashtags used by recent public/unlisted events also
contribute to the trending ranking alongside post hashtags.
No additional worker is required: event activities use the existing inbox and
delivery queues processed by openbook:cron. After deploying support for a
new inbox object type, an administrator may retry retained rows previously
classified as ignored with:
php artisan openbook:reprocess-inbox
The command requeues every retained ignored inbox item and remains safe to run
more than once; unsupported activities simply return to the ignored state.
Not yet part of the mature product: a real recipient system for direct
messages (beyond mentions), and advanced federation-debug tools (beyond the
admin queue panel).