CLAUDE.md covers patch authoring rules, post-type discovery, the two reply compose paths, ap_timeline insertion timing, fork dependencies, and common debugging entry points. memory/ contains three files: - project_activitypub.md — data flows, syndicator config, all AP patches - project_architecture.md — FreeBSD jails, MongoDB collections, actor URLs - feedback_patches.md — patch pattern, known fragile points, threading gotchas Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.8 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.
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.