Files
indiekit-server/memory/project_activitypub.md
Sven 5e50d7aceb docs: add CLAUDE.md and memory files for AP threading context
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>
2026-03-30 08:30:32 +02:00

6.8 KiB
Raw Blame History

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.checked directly (always pre-checked if checked: true in config)
  • AP reader: uses target.defaultChecked set by composeController() — 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 (30120 s later). Follow-up replies during that window would fail findTimelineItemByIdinReplyTo = 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 by patch-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.