-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Summary
After successfully federating a Solid server (JSS) with Mastodon, I'd like to propose upstreaming some battle-tested patterns to microfed. These address real issues we encountered during federation testing.
Context
JSS (JavaScriptSolidServer) now federates with Mastodon using microfed for the low-level primitives. During testing, we discovered several patterns that every AP implementation needs but aren't currently in microfed.
What worked great:
auth.sign/auth.verify- HTTP signaturesoutbox.send/outbox.createAccept- activity creationwebfinger.createResponse- discovery
What we had to build ourselves:
- Actor fetching with proper headers
- Signature verification flow
- Inbox routing and handlers
- Caching layer
Proposal
1. microfed/fetch - Actor/Object fetching
Problem: Mastodon blocks requests without User-Agent header. Every AP implementation rediscovers this.
import { fetchActor, fetchObject } from 'microfed/fetch'
// Handles: User-Agent, Accept header, error handling, optional caching
const actor = await fetchActor('https://mastodon.social/users/someone', {
userAgent: 'MyApp/1.0 (+https://myapp.com)',
cache: myCache, // optional pluggable cache
timeout: 10000
})
// Also useful for fetching referenced objects
const note = await fetchObject('https://example.com/notes/123')Implementation would include:
- User-Agent header (required by Mastodon)
Accept: application/activity+jsonheader- Timeout handling
- Pluggable caching interface
- Error normalization
2. microfed/inbox - Verification flow
Problem: Signature verification is a multi-step flow (parse header → extract keyId → fetch actor → get public key → verify). Easy to get wrong.
import { verifyRequest } from 'microfed/inbox'
// All-in-one verification
const result = await verifyRequest({
signature: request.headers.signature,
method: request.method,
path: request.url,
headers: request.headers,
body: requestBody
}, {
fetchActor, // use the fetcher above
requireSignature: true,
verifyDigest: true
})
// Returns: { valid: boolean, actor?: object, error?: string }
if (!result.valid) {
console.log(`Verification failed: ${result.error}`)
}3. microfed/handlers - Standard activity handlers
Problem: Follow → Accept is boilerplate everyone writes. Same with Undo/Follow.
import { handleFollow, handleUndo, handleAccept } from 'microfed/handlers'
// Handle incoming Follow - returns Accept activity to send
const accept = handleFollow(activity, {
actorId: 'https://mysite.com/profile#me',
profileUrl: 'https://mysite.com/profile'
})
// Send the Accept back
await outbox.send({
activity: accept,
inbox: followerActor.inbox,
privateKey,
keyId
})
// Handle Undo (e.g., unfollow)
const undone = handleUndo(activity)
// Returns: { type: 'Follow', actor: '...' } so you know what was undone4. microfed/outbox enhancements
Problem: Creating a Note and delivering to followers is a common pattern.
import { createAndDeliver } from 'microfed/outbox'
const result = await createAndDeliver({
type: 'Note',
content: 'Hello Fediverse!',
actor: 'https://mysite.com/profile#me',
to: ['https://www.w3.org/ns/activitystreams#Public'],
cc: ['https://mysite.com/profile/followers'],
inboxes: ['https://mastodon.social/inbox', ...],
privateKey,
keyId: 'https://mysite.com/profile#main-key'
})
// Returns: { activity, delivered: number, failed: number }Architectural Questions
-
Caching interface - Should microfed define a cache interface or leave it to apps?
interface ActorCache { get(url: string): Actor | null set(url: string, actor: Actor, ttl?: number): void }
-
Storage - Should handlers be stateless (return what to store) or accept a storage interface?
-
Framework agnostic - Keep everything framework-agnostic (no Fastify/Express/Hono dependency)?
Benefits
- Fewer bugs - User-Agent issue alone cost hours of debugging
- Better DX - Common patterns just work
- Battle-tested - These patterns are proven in production federation
- Still modular - Keep microfed's minimal philosophy, just add opt-in higher-level modules
Non-goals
These should stay in app code:
- Specific storage implementations (SQLite, Postgres, etc.)
- Framework integration (Fastify plugins, Express middleware)
- Authentication (OAuth, Bearer tokens, etc.)
- Application-specific content negotiation
References
- JSS ActivityPub implementation: https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/tree/gh-pages/src/ap
- Federation milestone: SAND Stack Phase 2: Federation, Bridging & Dashboard JavaScriptSolidServer/JavaScriptSolidServer#50
- Mastodon federation successfully tested Jan 5, 2026
Next Steps
Happy to contribute PRs for any of these if the direction makes sense. Would love feedback on:
- Which of these would be most valuable?
- API design preferences?
- Should these be separate entry points (
microfed/fetch) or part of main export?