Skip to content

Proposal: Add higher-level patterns from real-world federation testing #5

@melvincarvalho

Description

@melvincarvalho

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 signatures
  • outbox.send / outbox.createAccept - activity creation
  • webfinger.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+json header
  • 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 undone

4. 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

  1. 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
    }
  2. Storage - Should handlers be stateless (return what to store) or accept a storage interface?

  3. Framework agnostic - Keep everything framework-agnostic (no Fastify/Express/Hono dependency)?

Benefits

  1. Fewer bugs - User-Agent issue alone cost hours of debugging
  2. Better DX - Common patterns just work
  3. Battle-tested - These patterns are proven in production federation
  4. 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

Next Steps

Happy to contribute PRs for any of these if the direction makes sense. Would love feedback on:

  1. Which of these would be most valuable?
  2. API design preferences?
  3. Should these be separate entry points (microfed/fetch) or part of main export?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions