docs: add CLAUDE.md for AI agents and README.md for humans

CLAUDE.md covers architecture, 18 critical gotchas distilled from
bug fixes (Fedify bridge, objectId vs getObject, template collisions,
Express 5 redirect, date handling, author fallback chain, etc.),
MongoDB collections, route table, and publishing workflow.

README.md covers features, installation, configuration, nginx setup,
how syndication/inbox/content negotiation work, Mastodon migration,
admin UI reference, and known limitations.
This commit is contained in:
Ricardo
2026-02-21 17:06:11 +01:00
parent 348a183e46
commit b81ecbcaa4
2 changed files with 570 additions and 0 deletions

303
CLAUDE.md Normal file
View File

@@ -0,0 +1,303 @@
# CLAUDE.md — @rmdes/indiekit-endpoint-activitypub
AI agent instructions for working on this codebase. Read this entire file before making any changes.
## What This Is
An Indiekit plugin that adds full ActivityPub federation via [Fedify](https://fedify.dev). It turns an Indiekit-powered IndieWeb site into a fediverse actor — discoverable, followable, and interactive from Mastodon, Misskey, Pixelfed, Lemmy, etc.
**npm:** `@rmdes/indiekit-endpoint-activitypub`
**Version:** See `package.json`
**Node:** >=22
**Module system:** ESM (`"type": "module"`)
## Architecture Overview
```
index.js ← Plugin entry, route registration, syndicator
├── lib/federation-setup.js ← Fedify Federation instance, dispatchers, collections
├── lib/federation-bridge.js ← Express ↔ Fedify request/response bridge
├── lib/inbox-listeners.js ← Handlers for Follow, Undo, Like, Announce, Create, Delete, etc.
├── lib/jf2-to-as2.js ← JF2 → ActivityStreams conversion (plain JSON + Fedify vocab)
├── lib/kv-store.js ← MongoDB-backed KvStore for Fedify
├── lib/activity-log.js ← Activity logging to ap_activities
├── lib/timeline-store.js ← Timeline item extraction + sanitization
├── lib/timeline-cleanup.js ← Retention-based timeline pruning
├── lib/batch-refollow.js ← Gradual re-follow for imported Mastodon accounts
├── lib/migration.js ← CSV parsing + WebFinger resolution for Mastodon import
├── lib/csrf.js ← CSRF token generation/validation
├── lib/storage/
│ ├── timeline.js ← Timeline CRUD with cursor pagination
│ ├── notifications.js ← Notification CRUD with read/unread tracking
│ └── moderation.js ← Mute/block storage
├── lib/controllers/ ← Express route handlers (admin UI)
│ ├── dashboard.js, reader.js, compose.js, profile.js, profile.remote.js
│ ├── followers.js, following.js, activities.js
│ ├── featured.js, featured-tags.js
│ ├── interactions.js, interactions-like.js, interactions-boost.js
│ ├── moderation.js, migrate.js, refollow.js
├── views/ ← Nunjucks templates
│ ├── activitypub-*.njk ← Page templates
│ ├── layouts/ap-reader.njk ← Reader layout (NOT reader.njk — see gotcha below)
│ └── partials/ ← Shared components
├── assets/
│ ├── reader.css ← Reader UI styles
│ └── icon.svg ← Plugin icon
└── locales/en.json ← i18n strings
```
## Data Flow
```
Outbound: Indiekit post → syndicator.syndicate() → jf2ToAS2Activity() → ctx.sendActivity() → follower inboxes
Inbound: Remote inbox POST → Fedify → inbox-listeners.js → MongoDB collections → admin UI
Reader: Followed account posts → Create inbox → timeline-store → ap_timeline → reader UI
```
## MongoDB Collections
| Collection | Purpose | Key fields |
|---|---|---|
| `ap_followers` | Accounts following us | `actorUrl` (unique), `inbox`, `sharedInbox`, `source` |
| `ap_following` | Accounts we follow | `actorUrl` (unique), `source`, `acceptedAt` |
| `ap_activities` | Activity log (TTL-indexed) | `direction`, `type`, `actorUrl`, `objectUrl`, `receivedAt` |
| `ap_keys` | Cryptographic key pairs | `type` ("rsa" or "ed25519"), key material |
| `ap_kv` | Fedify KvStore + job state | `_id` (key path), `value` |
| `ap_profile` | Actor profile (single doc) | `name`, `summary`, `icon`, `attachments`, `actorType` |
| `ap_featured` | Pinned posts | `postUrl`, `pinnedAt` |
| `ap_featured_tags` | Featured hashtags | `tag`, `addedAt` |
| `ap_timeline` | Reader timeline items | `uid` (unique), `published`, `author`, `content` |
| `ap_notifications` | Likes, boosts, follows, mentions | `uid` (unique), `type`, `read` |
| `ap_muted` | Muted actors/keywords | `url` or `keyword` |
| `ap_blocked` | Blocked actors | `url` |
| `ap_interactions` | Like/boost tracking per post | `objectUrl`, `type` |
## Critical Patterns and Gotchas
### 1. Express ↔ Fedify Bridge (CUSTOM — NOT @fedify/express)
We **cannot** use `@fedify/express`'s `integrateFederation()` because Indiekit mounts plugins at sub-paths. Express strips the mount prefix from `req.url`, breaking Fedify's URI template matching. Instead, `federation-bridge.js` uses `req.originalUrl` to build the full URL.
**If you see path-matching issues with Fedify, check that `req.originalUrl` is being used, not `req.url`.**
### 2. Content Negotiation Route — GET Only
The `contentNegotiationRoutes` router is mounted at `/` (root). It MUST only pass `GET`/`HEAD` requests to Fedify. Passing `POST`/`PUT`/`DELETE` would cause `fromExpressRequest()` to consume the body stream via `Readable.toWeb(req)`, breaking Express body-parsed routes downstream (admin forms, Micropub, etc.).
### 3. Skip Fedify for Admin Routes
In `routesPublic`, the middleware skips paths starting with `/admin`. Without this, Fedify would intercept admin UI requests and return 404/406 responses instead of letting Express serve the authenticated pages.
### 4. Use .objectId/.actorId — NOT .getObject()/.getActor() in Inbox Handlers
Fedify's `.getObject()` and `.getActor()` trigger HTTP fetches to remote servers. This fails silently or retries ~10 times when:
- Remote server has **Authorized Fetch** enabled (returns 401)
- Server is down or unreachable
- Object has been deleted
**Always prefer** `.objectId?.href` and `.actorId?.href` (zero network requests) for Like, Announce, Undo, and Delete handlers. Only use `.getObject()` / `.getActor()` when you need the full object, and **always wrap in try-catch**.
### 5. Accept(Follow) Matching — Don't Check Inner Object Type
Fedify often resolves the inner object of `Accept` to a `Person` (the Follow's target) rather than the `Follow` itself. The Accept handler matches against `ap_following` by actor URL instead of inspecting `inner instanceof Follow`.
### 6. Filter Inbound Likes/Announces to Our Content Only
Without filtering, the inbox logs every Like/Announce from every federated server — including reactions to other people's content that happens to flow through shared inboxes. Check `objectId.startsWith(publicationUrl)` before logging.
### 7. Nunjucks Template Name Collisions
Template names resolve across ALL registered plugin view directories. If two plugins have `views/layouts/reader.njk`, Nunjucks loads whichever it finds first (often wrong). The reader layout is named `ap-reader.njk` to avoid collision with `@rmdes/indiekit-endpoint-microsub`'s `reader.njk`.
**Never name a layout/template with a generic name that another plugin might use.**
### 8. Express 5 — No redirect("back")
Express 5 removed the `"back"` magic keyword from `response.redirect()`. It's treated as a literal URL, causing 404s at paths like `/admin/featured/back`. Always use explicit redirect paths.
### 9. Fedify Endpoints Type Bug (Workaround)
Fedify serializes `endpoints` with `"type": "as:Endpoints"` which is not a real ActivityStreams type. `sendFedifyResponse()` in `federation-bridge.js` strips this from actor JSON responses. Remove the workaround when [fedify#576](https://github.com/fedify-dev/fedify/issues/576) is fixed upstream.
### 10. Profile Links — Express qs Body Parser Key Mismatch
`express.urlencoded({ extended: true })` uses `qs` which strips `[]` from array field names. HTML fields named `link_name[]` arrive as `request.body.link_name` (not `request.body["link_name[]"]`). The profile controller reads `link_name` and `link_value`, NOT `link_name[]`.
### 11. Author Resolution Fallback Chain
`extractObjectData()` in `timeline-store.js` uses a multi-strategy fallback:
1. `object.getAttributedTo()` — async, may fail with Authorized Fetch
2. `options.actorFallback` — the activity's actor (passed from Create handler)
3. `object.attribution` / `object.attributedTo` — plain object properties
4. `object.attributionIds` — non-fetching URL array with username extraction from common patterns (`/@name`, `/users/name`)
Without this chain, many timeline items show "Unknown" as the author.
### 12. Username Extraction from Actor URLs
When extracting usernames from attribution IDs, handle multiple URL patterns:
- `/@username` (Mastodon)
- `/users/username` (Mastodon, Indiekit)
- `/ap/users/12345/` (numeric IDs on some platforms)
The regex was previously matching "users" instead of the actual username from `/users/NatalieDavis`.
### 13. Empty Boost Filtering
Lemmy/PieFed send Announce activities where the boosted object resolves to an activity ID instead of a Note/Article with actual content. Check `object.content || object.name` before storing to avoid empty cards in the timeline.
### 14. Temporal.Instant for Fedify Dates
Fedify uses `@js-temporal/polyfill` for dates. When setting `published` on Fedify objects, use `Temporal.Instant.from(isoString)`. When reading Fedify dates in inbox handlers, use `String(object.published)` to get ISO strings — NOT `new Date(object.published)` which causes `TypeError`.
### 15. LogTape — Configure Once Only
`@logtape/logtape`'s `configure()` can only be called once per process. The module-level `_logtapeConfigured` flag prevents duplicate configuration. If configure fails (e.g., another plugin already configured it), catch the error silently.
### 16. .authorize() Intentionally NOT Chained on Actor Dispatcher
Fedify's `.authorize()` triggers HTTP Signature verification on every GET to the actor endpoint. Servers requiring Authorized Fetch cause infinite loops: Fedify tries to fetch their key → they return 401 → Fedify retries → 500 errors. Re-enable when Fedify supports authenticated document loading for outgoing fetches.
### 17. Delivery Queue Must Be Started
`federation.startQueue()` MUST be called after setup. Without it, `ctx.sendActivity()` enqueues tasks but the message queue never processes them — activities are never delivered.
### 18. Shared Key Dispatcher for Shared Inbox
`inboxChain.setSharedKeyDispatcher()` tells Fedify to use our actor's key pair when verifying HTTP Signatures on the shared inbox. Without this, servers like hachyderm.io (which requires Authorized Fetch) have their signatures rejected.
## Date Handling Convention
**All dates MUST be stored as ISO 8601 strings.** This is mandatory across all Indiekit plugins.
```javascript
// CORRECT
followedAt: new Date().toISOString()
published: String(fedifyObject.published) // Temporal → string
// WRONG — crashes Nunjucks | date filter
followedAt: new Date()
published: new Date(fedifyObject.published)
```
The Nunjucks `| date` filter calls `date-fns parseISO()` which only accepts ISO strings. `Date` objects cause `"dateString.split is not a function"` crashes.
## Batch Re-follow State Machine
```
import → refollow:pending → refollow:sent → federation (happy path: Accept received)
import → refollow:pending → refollow:sent → refollow:failed (after 3 retries)
```
- `import`: Imported from Mastodon CSV, no Follow sent yet
- `refollow:pending`: Claimed by batch processor, being processed
- `refollow:sent`: Follow activity sent, awaiting Accept
- `federation`: Accept received, fully federated
- `refollow:failed`: Max retries exceeded
On restart, `refollow:pending` entries are reset to `import` to prevent stale claims.
## Plugin Lifecycle
1. `constructor()` — Merges options with defaults
2. `init(Indiekit)` — Called by Indiekit during startup:
- Stores `publication.me` as `_publicationUrl`
- Registers 13 MongoDB collections with indexes
- Seeds actor profile from config (first run only)
- Calls `setupFederation()` which creates Fedify instance + starts queue
- Registers endpoint (mounts routes) and syndicator
- Starts batch re-follow processor (10s delay)
- Schedules timeline cleanup (on startup + every 24h)
## Route Structure
| Method | Path | Handler | Auth |
|---|---|---|---|
| `*` | `/.well-known/*` | Fedify (WebFinger, NodeInfo) | No |
| `*` | `{mount}/users/*`, `{mount}/inbox` | Fedify (actor, inbox, outbox, collections) | No (HTTP Sig) |
| `GET` | `{mount}/` | Dashboard | Yes (IndieAuth) |
| `GET` | `{mount}/admin/reader` | Timeline reader | Yes |
| `GET` | `{mount}/admin/reader/notifications` | Notifications | Yes |
| `POST` | `{mount}/admin/reader/compose` | Compose reply | Yes |
| `POST` | `{mount}/admin/reader/like,unlike,boost,unboost` | Interactions | Yes |
| `POST` | `{mount}/admin/reader/follow,unfollow` | Follow/unfollow | Yes |
| `GET` | `{mount}/admin/reader/profile` | Remote profile view | Yes |
| `GET` | `{mount}/admin/reader/moderation` | Moderation dashboard | Yes |
| `POST` | `{mount}/admin/reader/mute,unmute,block,unblock` | Moderation actions | Yes |
| `GET` | `{mount}/admin/followers,following,activities` | Lists | Yes |
| `GET/POST` | `{mount}/admin/profile` | Actor profile editor | Yes |
| `GET/POST` | `{mount}/admin/featured` | Pinned posts | Yes |
| `GET/POST` | `{mount}/admin/tags` | Featured tags | Yes |
| `GET/POST` | `{mount}/admin/migrate` | Mastodon migration | Yes |
| `*` | `{mount}/admin/refollow/*` | Batch refollow control | Yes |
| `GET` | `/*` (root) | Content negotiation (AP clients only) | No |
## Dependencies
| Package | Purpose |
|---|---|
| `@fedify/fedify` | ActivityPub federation framework |
| `@fedify/express` | Express integration utilities (types only — bridge is custom) |
| `@fedify/redis` | Redis message queue for delivery |
| `@js-temporal/polyfill` | Temporal API for Fedify date handling |
| `ioredis` | Redis client |
| `sanitize-html` | XSS prevention for timeline/notification content |
| `express` | Route handling (peer: Indiekit provides it) |
## Configuration Options
```javascript
{
mountPath: "/activitypub", // URL prefix for all routes
actor: {
handle: "rick", // Fediverse username
name: "Ricardo Mendes", // Display name (seeds profile)
summary: "", // Bio (seeds profile)
icon: "", // Avatar URL (seeds profile)
},
checked: true, // Syndicator checked by default
alsoKnownAs: "", // Mastodon migration alias
activityRetentionDays: 90, // TTL for ap_activities (0 = forever)
storeRawActivities: false, // Store full JSON of inbound activities
redisUrl: "", // Redis for delivery queue (empty = in-process)
parallelWorkers: 5, // Parallel delivery workers (with Redis)
actorType: "Person", // Person | Service | Organization | Group
timelineRetention: 1000, // Max timeline items (0 = unlimited)
}
```
## Publishing Workflow
1. Edit code in this repo
2. Bump version in `package.json` (npm rejects duplicate versions)
3. Commit and push
4. **STOP** — user must run `npm publish` manually (requires OTP)
5. After publish confirmation, update Dockerfile version in `indiekit-cloudron/`
6. `cloudron build --no-cache && cloudron update --app rmendes.net --no-backup`
## Testing
No automated test suite. Manual testing against real fediverse servers:
```bash
# WebFinger
curl -s "https://rmendes.net/.well-known/webfinger?resource=acct:rick@rmendes.net" | jq .
# Actor document
curl -s -H "Accept: application/activity+json" "https://rmendes.net/" | jq .
# NodeInfo
curl -s "https://rmendes.net/nodeinfo/2.1" | jq .
# Search from Mastodon for @rick@rmendes.net
```
## CSS Conventions
The reader CSS (`assets/reader.css`) uses Indiekit's theme custom properties for automatic dark mode support:
- `--color-on-background` (not `--color-text`)
- `--color-on-offset` (not `--color-text-muted`)
- `--border-radius-small` (not `--border-radius`)
- `--color-red45`, `--color-green50`, etc. (not hardcoded hex)
Post types are differentiated by left border color: purple (notes), green (articles), yellow (boosts), primary (replies).

267
README.md Normal file
View File

@@ -0,0 +1,267 @@
# @rmdes/indiekit-endpoint-activitypub
ActivityPub federation endpoint for [Indiekit](https://getindiekit.com). Makes your IndieWeb site a full fediverse actor — discoverable, followable, and interactive from Mastodon, Misskey, Pixelfed, and any ActivityPub-compatible platform.
## Features
**Federation**
- Full ActivityPub actor with WebFinger, NodeInfo, HTTP Signatures, and Object Integrity Proofs (Ed25519)
- Outbox syndication — posts created via Micropub are automatically delivered to followers
- Inbox processing — receives follows, likes, boosts, replies, mentions, deletes, and account moves
- Content negotiation — ActivityPub clients requesting your site get JSON-LD; browsers get HTML
- Reply delivery — replies are addressed to and delivered directly to the original post's author
- Shared inbox support with collection sync (FEP-8fcf)
- Configurable actor type (Person, Service, Organization, Group)
**Reader**
- Timeline view showing posts from followed accounts
- Notifications for likes, boosts, follows, mentions, and replies
- Compose form with dual-path posting (quick AP reply or Micropub blog post)
- Native interactions (like, boost, reply, follow/unfollow from the reader)
- Remote actor profile pages
- Content warnings and sensitive content handling
- Media display (images, video, audio)
- Configurable timeline retention
**Moderation**
- Mute actors or keywords
- Block actors (also removes from followers)
- All moderation actions available from the reader UI
**Mastodon Migration**
- Import following/followers lists from Mastodon CSV exports
- Set `alsoKnownAs` alias for account Move verification
- Batch re-follow processor — gradually sends Follow activities to imported accounts
- Progress tracking with pause/resume controls
**Admin UI**
- Dashboard with follower/following counts and recent activity
- Profile editor (name, bio, avatar, header, profile links with rel="me" verification)
- Pinned posts (featured collection)
- Featured tags (hashtag collection)
- Activity log (inbound/outbound)
- Follower and following lists with source tracking
## Requirements
- [Indiekit](https://getindiekit.com) v1.0.0-beta.25+
- Node.js >= 22
- MongoDB (used by Indiekit)
- Redis (recommended for production delivery queue; in-process queue available for development)
## Installation
```bash
npm install @rmdes/indiekit-endpoint-activitypub
```
## Configuration
Add the plugin to your Indiekit config:
```javascript
// indiekit.config.js
export default {
plugins: [
"@rmdes/indiekit-endpoint-activitypub",
],
"@rmdes/indiekit-endpoint-activitypub": {
mountPath: "/activitypub",
actor: {
handle: "yourname",
name: "Your Name",
summary: "A short bio",
icon: "https://example.com/avatar.jpg",
},
},
};
```
### All Options
| Option | Type | Default | Description |
|---|---|---|---|
| `mountPath` | string | `"/activitypub"` | URL prefix for all plugin routes |
| `actor.handle` | string | `"rick"` | Fediverse username (e.g. `@handle@yourdomain.com`) |
| `actor.name` | string | `""` | Display name (used to seed profile on first run) |
| `actor.summary` | string | `""` | Bio text (used to seed profile on first run) |
| `actor.icon` | string | `""` | Avatar URL (used to seed profile on first run) |
| `checked` | boolean | `true` | Whether the syndicator is checked by default in the post editor |
| `alsoKnownAs` | string | `""` | Mastodon migration alias URL |
| `activityRetentionDays` | number | `90` | Days to keep activity log entries (0 = forever) |
| `storeRawActivities` | boolean | `false` | Store full raw JSON of inbound activities |
| `redisUrl` | string | `""` | Redis connection URL for delivery queue |
| `parallelWorkers` | number | `5` | Number of parallel delivery workers (requires Redis) |
| `actorType` | string | `"Person"` | Actor type: `Person`, `Service`, `Organization`, or `Group` |
| `timelineRetention` | number | `1000` | Maximum timeline items to keep (0 = unlimited) |
### Redis (Recommended for Production)
Without Redis, the plugin uses an in-process message queue. This works for development but won't survive restarts and has limited throughput.
```javascript
"@rmdes/indiekit-endpoint-activitypub": {
redisUrl: "redis://localhost:6379",
parallelWorkers: 5,
},
```
### Nginx Configuration (Reverse Proxy)
If you serve a static site alongside Indiekit (e.g. with Eleventy), you need nginx rules to route ActivityPub requests to Indiekit while serving HTML to browsers:
```nginx
# ActivityPub content negotiation — detect AP clients
map $http_accept $is_activitypub {
default 0;
"~*application/activity\+json" 1;
"~*application/ld\+json" 1;
}
# Proxy /activitypub to Indiekit
location /activitypub {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
# Default: static site, but AP clients get proxied
location / {
if ($is_activitypub) {
proxy_pass http://127.0.0.1:8080;
}
try_files $uri $uri/ $uri.html =404;
}
```
## How It Works
### Syndication (Outbound)
When you create a post via Micropub, Indiekit's syndication system calls this plugin's syndicator. The plugin:
1. Converts the JF2 post properties to an ActivityStreams 2.0 `Create(Note)` or `Create(Article)` activity
2. For replies, resolves the original post's author to include them in CC and deliver directly to their inbox
3. Sends the activity to all followers via shared inboxes using Fedify's delivery queue
4. Appends a permalink to the content so fediverse clients link back to your canonical post
### Inbox Processing (Inbound)
When remote servers send activities to your inbox:
- **Follow** → Auto-accepted, stored in `ap_followers`, notification created
- **Undo(Follow)** → Removed from `ap_followers`
- **Like** → Logged in activity log, notification created (only for reactions to your own posts)
- **Announce (Boost)** → Logged + notification (your content) or stored in timeline (followed account)
- **Create (Note/Article)** → Stored in timeline if from a followed account; notification if it's a reply or mention
- **Update** → Updates timeline item content or refreshes follower profile data
- **Delete** → Removes from activity log and timeline
- **Move** → Updates follower's actor URL
- **Accept(Follow)** → Marks our follow as accepted
- **Reject(Follow)** → Marks our follow as rejected
- **Block** → Removes actor from our followers
### Content Negotiation
The plugin mounts a root-level router that intercepts requests from ActivityPub clients (detected by `Accept: application/activity+json` or `application/ld+json`):
- Root URL (`/`) → Redirects to the Fedify actor document
- Post URLs → Looks up the post in MongoDB, converts to AS2 JSON
- NodeInfo (`/nodeinfo/2.1`) → Delegated to Fedify
Regular browser requests pass through unmodified.
### Mastodon Migration
The plugin supports migrating from a Mastodon account:
1. **Set alias** — Configure `alsoKnownAs` with your old Mastodon profile URL. This is verified by Mastodon before allowing a Move.
2. **Import social graph** — Upload Mastodon's `following_accounts.csv` and `followers.csv` exports. Following entries are resolved via WebFinger and stored locally.
3. **Trigger Move** — From Mastodon's settings, initiate a Move to `@handle@yourdomain.com`. Mastodon notifies your followers, and compatible servers auto-refollow.
4. **Batch re-follow** — The plugin gradually sends Follow activities to all imported accounts (10 per batch, 30s between batches) so remote servers start delivering content to your inbox.
## Verification
After deployment, verify federation is working:
```bash
# WebFinger discovery
curl -s "https://yourdomain.com/.well-known/webfinger?resource=acct:handle@yourdomain.com" | jq .
# Actor document
curl -s -H "Accept: application/activity+json" "https://yourdomain.com/" | jq .
# NodeInfo
curl -s "https://yourdomain.com/nodeinfo/2.1" | jq .
```
Then search for `@handle@yourdomain.com` from any Mastodon instance — your profile should appear.
## Admin UI Pages
All admin pages are behind IndieAuth authentication:
| Page | Path | Description |
|---|---|---|
| Dashboard | `/activitypub` | Overview with follower/following counts, recent activity |
| Reader | `/activitypub/admin/reader` | Timeline from followed accounts |
| Notifications | `/activitypub/admin/reader/notifications` | Likes, boosts, follows, mentions, replies |
| Compose | `/activitypub/admin/reader/compose` | Reply composer (quick AP or Micropub) |
| Moderation | `/activitypub/admin/reader/moderation` | Muted/blocked accounts and keywords |
| Profile | `/activitypub/admin/profile` | Edit actor display name, bio, avatar, links |
| Followers | `/activitypub/admin/followers` | List of accounts following you |
| Following | `/activitypub/admin/following` | List of accounts you follow |
| Activity Log | `/activitypub/admin/activities` | Inbound/outbound activity history |
| Pinned Posts | `/activitypub/admin/featured` | Pin/unpin posts to your featured collection |
| Featured Tags | `/activitypub/admin/tags` | Add/remove featured hashtags |
| Migration | `/activitypub/admin/migrate` | Mastodon import wizard |
## MongoDB Collections
The plugin creates these collections automatically:
| Collection | Description |
|---|---|
| `ap_followers` | Accounts following your actor |
| `ap_following` | Accounts you follow |
| `ap_activities` | Activity log with automatic TTL cleanup |
| `ap_keys` | RSA and Ed25519 key pairs for HTTP Signatures |
| `ap_kv` | Fedify key-value store and batch job state |
| `ap_profile` | Actor profile (single document) |
| `ap_featured` | Pinned/featured posts |
| `ap_featured_tags` | Featured hashtags |
| `ap_timeline` | Reader timeline items from followed accounts |
| `ap_notifications` | Interaction notifications |
| `ap_muted` | Muted actors and keywords |
| `ap_blocked` | Blocked actors |
| `ap_interactions` | Per-post like/boost tracking |
## Supported Post Types
The JF2-to-ActivityStreams converter handles these Indiekit post types:
| Post Type | ActivityStreams |
|---|---|
| note, reply, bookmark, jam, rsvp, checkin | `Create(Note)` |
| article | `Create(Article)` |
| like | `Like` |
| repost | `Announce` |
| photo, video, audio | Attachments on Note/Article |
Categories are converted to `Hashtag` tags. Bookmarks include a bookmark emoji and link.
## Known Limitations
- **No automated tests** — Manual testing against real fediverse servers
- **Single actor** — One fediverse identity per Indiekit instance
- **No Authorized Fetch enforcement** — Disabled due to Fedify's current limitation with authenticated outgoing fetches (causes infinite loops with servers that require it)
- **No image upload in reader** — Compose form is text-only
- **In-process queue without Redis** — Activities may be lost on restart
## License
MIT
## Author
[Ricardo Mendes](https://rmendes.net) ([@rick@rmendes.net](https://rmendes.net))