- patch-ap-inbox-delivery-debug: two fixes for diagnosing missing inbound AP interactions (likes, boosts, replies not appearing in notifications) Fix A (federation-setup.js): change ["fedify","federation","inbox"] log category from lowestLevel "fatal" → "error" so HTTP Signature verification failures are now visible in server logs instead of being silently swallowed. The original "fatal" level was hiding real delivery rejections (401s) that cause remote servers to stop retrying. Fix B (federation-bridge.js): add a pre-signature-check console.info for every inbox POST when AP_DEBUG=1 or AP_LOG_LEVEL=debug. Confirms whether remote servers are reaching our inbox at all (nginx/routing check). - memory/project_activitypub.md: document full inbound activity pipeline, _publicationUrl dependency, body buffering, and how to use new diagnostics Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8.7 KiB
ActivityPub Federation — Architecture & Patch Chain
Key Files
| File | Role |
|---|---|
node_modules/@rmdes/indiekit-endpoint-activitypub/lib/controllers/compose.js |
AP reader compose form: GET builds the form with syndicationTargets, POST submits to Micropub |
node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/statuses.js |
Mastodon-compatible API: POST /api/v1/statuses creates posts via Micropub pipeline |
node_modules/@rmdes/indiekit-endpoint-activitypub/lib/syndicator.js |
AP syndicator: federates posts to followers, adds own posts to ap_timeline |
node_modules/@rmdes/indiekit-endpoint-activitypub/lib/jf2-to-as2.js |
Converts JF2 properties to ActivityPub Note/Create activity |
node_modules/@rmdes/indiekit-endpoint-activitypub/lib/storage/timeline.js |
addTimelineItem() — atomic upsert ($setOnInsert) to ap_timeline |
node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/entities/status.js |
Serializes ap_timeline documents to Mastodon Status JSON |
node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/helpers/pagination.js |
encodeCursor(date) / decodeCursor(id) — ms-since-epoch as status ID |
node_modules/@indiekit/endpoint-micropub/lib/post-type-discovery.js |
getPostType() — returns "reply" if in-reply-to key present, else "note" |
node_modules/@indiekit/endpoint-micropub/lib/post-data.js |
Micropub create pipeline: normalise → getPostType → save |
node_modules/@rmdes/indiekit-endpoint-microsub/lib/controllers/reader.js |
Microsub reader compose: auto-selects syndication targets via detectProtocol() |
node_modules/@rmdes/indiekit-endpoint-activitypub/views/activitypub-compose.njk |
AP reader compose template: uses target.defaultChecked for checkbox state |
node_modules/@rmdes/indiekit-endpoint-activitypub/views/partials/ap-item-card.njk |
Timeline card: reply button passes item.url as replyTo query param |
Data Flow: Reply via AP Reader UI
User clicks reply on timeline item
→ /activitypub/admin/reader/compose?replyTo=<url>
→ composeController() fetches syndicationTargets from Micropub q=config
→ sets target.defaultChecked = target.checked === true [patch: ap-compose-default-checked]
→ renders activitypub-compose.njk with hidden in-reply-to field
→ user submits form
→ submitComposeController() POSTs to Micropub with in-reply-to
→ Micropub: formEncodedToJf2 → normaliseProperties → getPostType("reply") → save to /replies/{slug}/
→ syndication webhook fires → AP syndicator.syndicate(properties)
→ jf2ToAS2Activity creates Note with inReplyTo
→ delivered to followers + original author's inbox
Data Flow: Reply via Mastodon Client API (Phanpy/Elk)
Client sends POST /api/v1/statuses { status, in_reply_to_id }
→ findTimelineItemById(ap_timeline, in_reply_to_id)
→ decodes cursor (ms-since-epoch) → looks up by published date
→ inReplyTo = replyItem.uid || replyItem.url
→ jf2["in-reply-to"] = inReplyTo (if resolved)
→ jf2["mp-syndicate-to"] = [publicationUrl] (always set — AP-only)
→ postData.create → postContent.create
→ addTimelineItem immediately [patch: ap-mastodon-reply-threading]
→ returns minimal status JSON to client
→ build webhook → syndicator → AP delivery
Own Post in ap_timeline
The AP syndicator stores outbound posts with:
uid = url = properties.url(blog URL, e.g.https://blog.giersig.eu/replies/slug/)published = properties.published(ISO 8601 with TZ offset, e.g."2026-03-21T16:33:50+01:00")inReplyTo = properties["in-reply-to"](original Mastodon URL or own blog URL)
The Mastodon API encodes status IDs as encodeCursor(published) = ms-since-epoch string.
findTimelineItemById uses a ±1 s range query with $dateFromString to handle TZ-offset strings.
Syndication Target Config
The AP syndicator's info object:
{
checked: true, // from indiekit.config.mjs: checked: true
name: `@${handle}@${hostname}`, // e.g. "@svemagie@blog.giersig.eu"
uid: publicationUrl, // e.g. "https://blog.giersig.eu/"
service: { name: "ActivityPub (Fediverse)", ... }
}
The Micropub q=config endpoint returns this with checked: true.
Both compose forms read this value:
- Microsub reader: uses
target.checkeddirectly (always pre-checked ifchecked: truein config) - AP reader: uses
target.defaultCheckedset bycomposeController()— was broken, now fixed
Patches Applied (AP threading)
patch-ap-compose-default-checked
File: lib/controllers/compose.js
Problem: target.defaultChecked was hardcoded to name === "@rick@rmendes.net" — never matches.
Fix: target.defaultChecked = target.checked === true
Effect: AP syndication checkbox is pre-checked in the AP reader compose form, matching
the checked: true config — replies via the AP reader UI are now federated.
patch-ap-mastodon-reply-threading
File: lib/mastodon/routes/statuses.js
Problem: After POST /api/v1/statuses, own post was NOT inserted into ap_timeline until
the Eleventy build webhook fired (30–120 s later). Follow-up replies during that window
would fail findTimelineItemById → inReplyTo = null → no in-reply-to in JF2 →
getPostType returned "note" → reply saved at /notes/ with no inReplyTo in the AP activity.
Fix: Eagerly insert a provisional timeline item via addTimelineItem() immediately after
postContent.create(). Uses $setOnInsert (idempotent); syndicator's later upsert is a no-op.
Effect: in_reply_to_id can be resolved immediately → correct "reply" post type → proper
inReplyTo in the AP Note → thread displays correctly on Mastodon.
Post Type Discovery
getPostType(postTypes, properties) in post-type-discovery.js:
- Checks
propertiesMap.has("in-reply-to")→"reply" - Checks
propertiesMap.has("like-of")→"like" - Checks
propertiesMap.has("repost-of")→"repost" - Falls through to
"note"if none match
The "reply" post type in indiekit.config.mjs has no discovery field — standard PTD spec applies.
Inbound AP Activity Pipeline
Activities from remote servers follow this path:
Remote server → nginx → Express (body buffered in createFedifyMiddleware)
→ Fedify signature check (uses req._rawBody for digest)
→ Fedify Redis message queue (if Redis configured)
→ Fedify queue worker → inbox listener (inbox-listeners.js)
→ enqueueActivity() → ap_inbox_queue (MongoDB)
→ startInboxProcessor() (1s poll) → routeToHandler()
→ handleLike / handleAnnounce / handleCreate
→ addNotification() → ap_notifications
Critical: collections._publicationUrl is set in index.js (_publicationUrl: this._publicationUrl)
AND by patch-ap-inbox-publication-url in federation-setup.js. Both set "https://blog.giersig.eu/".
Notification conditions gate on pubUrl && objectId.startsWith(pubUrl):
handleLike: only notifies for likes of our own contenthandleAnnouncePATH 1: only notifies for boosts of our contenthandleCreate: only notifies for replies to our posts (inReplyTo.startsWith(pubUrl))
Body buffering (createFedifyMiddleware): application/activity+json bodies are buffered
into req._rawBody before express.json() (which only handles application/json) touches them.
fromExpressRequest passes req._rawBody verbatim to the Fedify Request object so the
HTTP Signature Digest check passes.
Fedify inbox log suppression: ["fedify","federation","inbox"] was hardcoded to "fatal"
(patch-ap-inbox-delivery-debug fixes this to "error" so real failures are visible).
Diagnosing inbox delivery issues:
- Set
AP_DEBUG=1→ logs[AP-inbox] POST /activitypub/users/svemagie/inbox ct=... body=...BBEFORE Fedify's signature check. If this doesn't appear, activities aren't reaching our server. - With inbox log level now
"error": signature failures show as Fedify error logs. - Queue processing failures:
[inbox-queue] Failed processing ...— always logged.
detectProtocol() in Microsub Reader
detectProtocol(url) in reader.js classifies URLs for syndication auto-selection:
"atmosphere"— bsky.app / bluesky"fediverse"— mastodon., mstdn., fosstodon., troet., social., misskey., pixelfed., hachyderm., infosec.exchange, chaos.social (extended bypatch-microsub-reader-ap-dispatch)"web"— everything else, including own blog URLs
Own blog URLs return "web", so auto-selection doesn't trigger for reply-to-own-reply in the
microsub reader. This is harmless because checked: true in the config already pre-checks the
AP target in the microsub reader's target.checked field.