| name | fedify-activitypub |
| description | Guide for building and maintaining ActivityPub/fediverse applications using Fedify. This skill should be used when users want to create federated social applications, implement ActivityPub protocols, build new features, test and debug federation, deploy to production, look up FEPs, or maintain existing Fedify applications. |
Fedify ActivityPub Development Guide
This skill provides guidance for building, maintaining, and extending ActivityPub-based federated applications using the Fedify framework.
Documentation Resources
For detailed and up-to-date API documentation:
- LLM-optimized docs: https://fedify.dev/llms.txt (overview) and https://fedify.dev/llms-full.txt (complete)
- Official docs: https://fedify.dev
- API Reference: https://jsr.io/@fedify/fedify
When answering detailed API questions, fetch the llms-full.txt for comprehensive documentation.
When to Use This Skill
- Creating a new ActivityPub-compatible server application
- Building new features (posts, likes, follows, boosts, replies)
- Setting up actors, inboxes, and collections
- Testing and debugging federation
- Deploying to production environments
- Migrating between Fedify versions
- Looking up FEPs (Fediverse Enhancement Proposals)
Quick Start
# Install CLI
deno install -A jsr:@fedify/cli # or: npm install -g @fedify/cli
# Create new project (interactive)
fedify init my-app
Core Architecture
Federation Instance
import { createFederation, MemoryKvStore, InProcessMessageQueue } from "@fedify/fedify";
const federation = createFederation<AppContext>({
kv: new MemoryKvStore(), // Required: cache and state
queue: new InProcessMessageQueue(), // Recommended: async activity processing
});
Production stores: @fedify/redis, @fedify/postgres, DenoKvStore
Actor Dispatcher (Required)
federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
const user = await db.getUserById(identifier);
if (!user) return null;
return new Person({
id: ctx.getActorUri(identifier),
preferredUsername: user.username,
inbox: ctx.getInboxUri(identifier),
publicKey: (await ctx.getActorKeyPairs(identifier))[0].cryptographicKey,
});
});
Key Pairs (Required for sending)
federation.setKeyPairsDispatcher(async (ctx, identifier) => [{
publicKey: await importJwk(user.publicKeyJwk, "public"),
privateKey: await importJwk(user.privateKeyJwk, "private"),
}]);
Inbox Listeners
federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
.on(Follow, handleFollow)
.on(Create, handleCreate)
.on(Like, handleLike)
.on(Undo, handleUndo);
Building Features
Implementing Follow/Unfollow
// Handle incoming Follow
.on(Follow, async (ctx, follow) => {
const follower = await follow.getActor();
const followeeId = ctx.recipient;
await db.createFollow(follower.id.href, followeeId);
// Send Accept back
await ctx.sendActivity(
{ identifier: followeeId },
follower,
new Accept({ actor: ctx.getActorUri(followeeId), object: follow })
);
})
// Handle Undo (unfollow)
.on(Undo, async (ctx, undo) => {
const object = await undo.getObject();
if (object instanceof Follow) {
const follower = await undo.getActor();
await db.removeFollow(follower.id.href, ctx.recipient);
}
});
Creating and Sending Posts
async function createPost(ctx: Context, authorId: string, content: string) {
const post = await db.createPost(authorId, content);
const note = new Note({
id: ctx.getObjectUri(Note, { id: post.id }),
attributedTo: ctx.getActorUri(authorId),
content: post.content,
published: Temporal.Now.instant(),
to: new URL("https://www.w3.org/ns/activitystreams#Public"),
});
await ctx.sendActivity(
{ identifier: authorId },
"followers", // Send to all followers
new Create({ actor: ctx.getActorUri(authorId), object: note })
);
}
Implementing Likes
// Send a Like
await ctx.sendActivity(
{ identifier: "alice" },
targetActor,
new Like({
actor: ctx.getActorUri("alice"),
object: new URL("https://remote.example/posts/123"),
})
);
// Handle incoming Like
.on(Like, async (ctx, like) => {
const objectUri = like.objectId;
const liker = await like.getActor();
await db.addLike(objectUri.href, liker.id.href);
});
Object Dispatchers
Expose posts and other objects:
federation.setObjectDispatcher(Note, "/posts/{id}", async (ctx, { id }) => {
const post = await db.getPost(id);
if (!post) return null;
return new Note({
id: ctx.getObjectUri(Note, { id }),
attributedTo: ctx.getActorUri(post.authorId),
content: post.content,
published: Temporal.Instant.from(post.createdAt),
});
});
Collections with Pagination
federation.setFollowersDispatcher("/users/{identifier}/followers",
async (ctx, identifier, cursor) => {
if (cursor == null) return null;
const { items, nextCursor } = await db.getFollowers(identifier, cursor, 20);
return {
items: items.map(f => new URL(f.actorUri)),
nextCursor,
};
}
)
.setFirstCursor(async () => "")
.setCounter(async (ctx, id) => db.countFollowers(id));
Testing
Mock Federation (@fedify/testing)
import { createFederation } from "@fedify/testing";
const federation = createFederation({ contextData: { userId: "test" } });
// Test inbox handlers
await federation.receiveActivity(followActivity);
// Inspect sent activities
console.log(federation.sentActivities);
// Reset between tests
federation.reset();
CLI Testing Tools
fedify inbox # Ephemeral server to receive activities
fedify tunnel 3000 # Expose local server with HTTPS
fedify lookup @user@host # Debug actor resolution
fedify webfinger user@host
Enable Private Network (Testing Only)
const federation = createFederation({
allowPrivateAddress: true, // SSRF risk - disable in production
});
Logging & Debugging
import { configure, getConsoleSink } from "@logtape/logtape";
await configure({
sinks: { console: getConsoleSink() },
filters: {},
loggers: [
{ category: ["fedify"], sinks: ["console"], lowestLevel: "debug" },
],
});
Key log categories:
["fedify", "federation", "inbox"]- Incoming activities["fedify", "federation", "outbox"]- Outgoing activities["fedify", "sig", "http"]- HTTP signature verification
Framework Integration
| Framework | Package | Integration |
|---|---|---|
| Hono | @fedify/hono |
app.use(federation()) |
| Express | @fedify/express |
integrateFederation() |
| Fastify | @fedify/fastify |
fedifyPlugin |
| Next.js | @fedify/next |
fedifyWith() in middleware |
| SvelteKit | @fedify/sveltekit |
fedifyHook() |
| NestJS | @fedify/nestjs |
Middleware |
Production Deployment
Key considerations:
- Use production KV store (
@fedify/redis,@fedify/postgres,DenoKvStore) - Use persistent message queue for activity delivery
- Store key pairs in database (never regenerate on restart)
- Separate web and queue workers with
manuallyStartQueue: true - Set up logging with appropriate sinks
- Configure
originoption for proper URL generation
FEP Resources
FEPs (Fediverse Enhancement Proposals) define interoperability standards:
- Repository: https://codeberg.org/fediverse/fep
- Discussion: https://socialhub.activitypub.rocks/c/standards/fep/54
Look up FEP: https://codeberg.org/fediverse/fep/src/branch/main/fep/{id}/fep-{id}.md
Important FEPs: FEP-8b32 (Object Integrity Proofs), FEP-fe34 (Origin security), FEP-1b12 (Groups)
Detailed Reference
For comprehensive API documentation, advanced patterns, and migration guides, see references/fedify-manual.md.