Skip to content

nigini/tinyFediPub

Repository files navigation

tinyFedi

A minimalist ActivityPub server designed for easy integration with personal websites.

Overview

Simple file-based ActivityPub implementation that serves federated content from static JSON files. Perfect for personal blogs and small websites wanting to join the fediverse without a complex infrastructure.

tinyFedi connects content, like blog posts, to the Fediverse

Tech Stack

  • Python 3.11+ - Required for modern datetime handling
  • Flask - Lightweight web framework
  • Jinja2 - Template engine for ActivityPub entities
  • File-based storage - All content served from static JSON files
  • Zero dependencies - Minimal external requirements

If you want to know more about how I implemented this software, and learn a lot about ActivityPub in the process, here are the posts (you can also Follow all updates at @blog@nigini.me - which is using this exact software to Federate):

  1. Building tinyFedi - part 1: Here we explore the basics of AP and build around Actors and its Outbox.
  2. Building tinyFedi - part 2: We finish the basics by building around the Inbox and Activity delivery.
  3. Building tinyFedi - part 3: coming soon HTTP Signatures
  4. Building tinyFedi - part 4: coming soon Update, Like, and Annouce Activities.

Setup

1. Generate Cryptographic Keys

ActivityPub requires public/private key pairs for secure federation:

mkdir keys
openssl genrsa -out keys/private_key.pem 2048
openssl rsa -in keys/private_key.pem -pubout -out keys/public_key.pem

Security Note: Keys are automatically excluded from version control via .gitignore.

2. Configuration

Copy the example configuration file and customize it for your setup:

cp config.json.example config.json

!!! Actor's profile auto-generates from config on startup

3. Take it for a Ride

python app.py

Add posts using the CLI:

./client/new_post.py --title "Post Title" --content "Content" --url "https://yourblog.com/post"

Note: New posts are automatically delivered to followers when created.

Edit existing posts:

./client/edit_post.py --post-id "550e8400-e29b-41d4-a716-446655440000"

Note: Updated posts are automatically delivered to followers when edited.

Follow a remote actor:

./client/follow.py --actor "https://mastodon.social/users/alice"

Note: The Follow is queued in the outbox and delivered by the activity processor. The actor is added to your following.json only once their server sends back an Accept.

Process queued activities (inbox + outbox):

python -m activity_processor  # or set up as a cron job

Note: Activities received in the inbox AND outgoing activities placed in the outbox queue (e.g., follows) are both processed by this single command.

Deployment

Designed to run behind a reverse proxy alongside existing websites:

location /activitypub/ {
    proxy_pass http://localhost:5000/activitypub/;
}

Development

Running Tests

Install dependencies and run the test suite:

pip install -r requirements.txt
python -m pytest tests/ -v

Writing Tests

This project uses a comprehensive test isolation strategy to ensure reliable testing. All test classes should inherit from TestConfigMixin for proper test isolation.

Key principles:

  • Each test gets its own temporary directory and configuration
  • Module reload prevents global variable caching issues
  • Configuration-driven paths (no hardcoded references)
  • Import app modules INSIDE test methods, AFTER setUp() runs

See tests/test_config.py for complete documentation, usage patterns, helper methods, and implementation details of the test configuration strategy.

Template System

ActivityPub entities are generated using Jinja2 templates for maintainability and extensibility:

templates/
├── base/             # Shared base templates
│   ├── activity.json.j2   # Base for all activity types
│   └── post.json.j2       # Base for Article/Note
├── objects/          # ActivityStreams Object types
│   ├── actor.json.j2      # Person/Service actors
│   ├── article.json.j2    # Blog posts, articles
│   └── note.json.j2       # Short messages
├── activities/       # ActivityStreams Activity types
│   ├── create.json.j2     # Create activities
│   ├── update.json.j2     # Update activities
│   └── accept.json.j2     # Accept activities (follow responses)
└── collections/      # ActivityStreams Collections
    ├── outbox.json.j2     # Outbox collection (paginated)
    └── followers.json.j2  # Followers collections

Design Philosophy:

  • Type-specific templates - Each ActivityStreams type has its own template
  • Extensible - Easy to add new object types (Note, Image, Event) and activity types (Like, Follow, Announce)
  • Spec-compliant - Templates ensure proper ActivityPub/ActivityStreams structure
  • Configurable - All values injected from config.json and runtime data

Activity Processing

Incoming and outgoing activities flow through pluggable processors auto-discovered from files under activity_processor/:

activity_processor/
├── follow.py        # FollowProcessor (inbound + outbound) + UndoFollowProcessor
├── accept.py        # AcceptProcessor (dispatcher) + AcceptFollowProcessor
├── create.py        # CreateProcessor (inbound)
├── like.py          # LikeProcessor + UndoLikeProcessor
└── announce.py      # AnnounceProcessor + UndoAnnounceProcessor

Registry convention. Each module defines one or more XxxProcessor classes inheriting from BaseActivityProcessor. The class name maps to a registry key by stripping Processor:

  • FollowProcessorFollow
  • UndoFollowProcessorUndo.Follow
  • AcceptFollowProcessorAccept.Follow

Compound activity types (Undo, Accept, Reject) get auto-split into <prefix>.<innertype> keys via the COMPOUND_PREFIXES tuple in activity_processor/__init__.py. Generic dispatcher classes (UndoActivityProcessor, AcceptProcessor) inspect the inner object's type and delegate to the matching <prefix>.<innertype> processor.

Direction. Each processor may implement process_inbox(activity, filename, config) for incoming activities and/or process_outbox(activity, filename, config) for outgoing ones. The queue dispatcher catches NotImplementedError and skips unsupported directions gracefully.

Queues. data/inbox/queue/ and data/outbox/queue/ are symlink farms pointing at activities awaiting processing. Inbox activities are queued by the HTTP webhook; outbox activities are queued by CLI tools (e.g., client/follow.py). Running python -m activity_processor walks both queues and dispatches to the matching processor.

Federation Features

Implemented:

  • WebFinger Discovery - .well-known/webfinger for actor discovery
  • Actor Profile - Dynamic actor generation from config
  • Outbox Collection - Dynamically serves published activities with pagination
  • Individual Endpoints - Posts and activities accessible via direct URLs
  • Inbox Endpoint - Receives activities from other federated servers with HTTP signature verification
  • Followers Collection - Manages and serves follower list
  • Content Negotiation - Proper ActivityPub headers and validation
  • HTTP Signature Verification - Cryptographic validation of incoming activities (configurable)
  • HTTP Signature Signing - Sign outgoing activities for secure delivery
  • Likes Collection - Per-post likes tracking with collection endpoint at /posts/{id}/likes
  • Shares Collection - Per-post shares tracking with collection endpoint at /posts/{id}/shares
  • Inbound Create - Receive and store posts from trusted actors in posts/remote/, with pristine object storage and provenance metadata
  • Trust Module - Policy-based acceptance of incoming Create activities (block list, following, addressed to us, reply to known post, trusted signer)
  • Inbox Provenance - HTTP signature identity (signed_by) stored alongside inbox activities for trust evaluation
  • C2S Bearer Token Auth - Token-based authentication for client-to-server endpoints
  • C2S Outbox POST - Clients submit AS2 objects, server wraps in Create activity and delivers
  • Streams/Posts - Object-centric paginated collection of posts (not activities) with inline reaction summaries
  • Actor Streams Discovery - Actor profile includes streams array for client discovery
  • Outbound Follow - Send Follow activities to remote actors via client/follow.py, queued and delivered by the activity processor
  • Inbound Accept(Follow) - Match incoming Accepts against pending Follows (by activity ID, with actor-pair fallback) and move the target into the following collection
  • Pending Follows Tracking - Symlinks in following_pending/ reference the sent Follow activity in the outbox until Accept arrives
  • Outbox Queue Processing - Symmetric outbox/queue/ mirrors the inbox queue: CLI tools enqueue, python -m activity_processor delivers
  • Data Access Layer - data_access/follow.py provides storage-agnostic primitives for follower / following / pending state (foundation for swapping the file backend later)

File Structure:

data/
├── actor.json              # Your actor profile (auto-generated)
├── followers.json          # OrderedCollection of accepted followers
├── following.json          # OrderedCollection of accepted follows (we follow them)
├── following_pending/      # Symlinks -> outbox/<follow-id>.json for follows
│                           #   awaiting Accept
├── blocked.json            # Block list (actors and domains)
├── posts/
│   ├── local/              # Your authored post objects (UUID directories)
│   │   └── {uuid}/
│   │       ├── post.json       # Post object with inline reaction summaries
│   │       ├── likes.json      # OrderedCollection of actors who liked
│   │       ├── shares.json     # OrderedCollection of actors who shared
│   │       └── replies.json    # OrderedCollection of replies
│   └── remote/             # Received posts from followed actors (URL-derived paths)
│       └── {domain}/{path}/
│           ├── object.json     # Original AS2 object (untouched)
│           └── metadata.json   # Provenance: signed_by, received_at, accepted_by_rule
├── outbox/                 # Outgoing activity objects
│   ├── create-20250921-143022-123456.json
│   ├── follow-20260525-120000-000000.json
│   └── queue/              # Symlinks to outbox activities awaiting delivery
└── inbox/                  # Received activities from other servers
    ├── follow-*.json           # Activity files (original, untouched)
    ├── follow-*.meta.json      # Provenance metadata (signed_by, received_at)
    └── queue/                  # Symlinks to inbox activities awaiting processing

Current Capabilities:

  • ✅ Others can discover your actor via WebFinger
  • ✅ Others can follow your actor and read your posts
  • ✅ You receive and process all incoming activities
  • ✅ Automatic follower management (add/remove followers)
  • ✅ Auto-respond to Follow requests with Accept activities
  • ✅ Deliver new posts to all followers automatically
  • ✅ HTTP signature verification for incoming activities (configurable)
  • ✅ HTTP signature signing for all outgoing deliveries
  • ✅ Receive and track Like activities per post
  • ✅ Receive and track Announce (share) activities per post
  • ✅ Receive and store Create activities from trusted actors
  • ✅ Policy-based trust evaluation for incoming content (see docs/ACCEPT_POST_POLICY.md)
  • ✅ Send Follow activities to remote actors (CLI: ./client/follow.py --actor <url>)
  • ✅ Process Accept(Follow) activities and add accepted targets to the following collection

Configuration Options:

  • auto_accept_follow_requests - Automatically accept follow requests (default: true). Set to false for manual approval of followers
  • require_http_signatures - Require HTTP signatures on all incoming activities (default: false). Set to true for production to reject unsigned server-to-server traffic
  • max_page_size - Maximum items per page for paginated collections like outbox (default: 20). Clients can request smaller pages via ?limit=N

What's Next

Architecture:

  • Data access layer — Extend the storage-agnostic API beyond data_access/follow.py to cover blocked, posts, inbox provenance, etc., replacing the remaining direct file I/O in processors and endpoints
  • Integrate delivery into processors — Move activity_delivery.py into the activity_processor module as delivery.py, since delivery is outbox processing
  • Per-follower delivery tracking — Expand the queue to track delivery per-follower, enabling independent retries for failed deliveries
  • Migrate new_post.py to the outbox queueclient/new_post.py still delivers synchronously; switch it to the same queue + processor flow used by client/follow.py

Activity Types:

  • Reject(Follow) inbound — Handle remote Rejects of our pending Follow requests (currently a Reject would not match any handler)
  • Undo(Follow) outbound — Send an Undo to stop following an actor, remove them from following.json
  • Announce outbound — Send Announce activities to boost posts to followers
  • Delete — Tombstoning posts + federated Delete delivery. See AP §6.11
  • EmojiReact — Rich reactions per FEP-c0e0

Client-to-Server:

  • Inbox materializationstreams/home with objects from followed actors
  • Microsyntax processing — Server-side @mention / #hashtag / URL resolution on outbox POST
  • Object Integrity Proofs — Self-authenticating posts via FEP-8b32, embedding cryptographic signatures in objects (like Nostr's sig). HIGH PRIORITY: sign all posts from the start

Other:

  • Proper logging system (replace print() with Python's logging module)
  • Manual follow approval workflow
  • Mention and reply handling

Design Notes

See docs/ for detailed design documents:

  • docs/CLIENT_CONTRACT.md — What tinyFedi guarantees to clients (normalization, streams, auth)
  • docs/ACCEPT_POST_POLICY.md — How tinyFedi decides to accept/reject incoming Create activities (trust rules, forwarding, future trust graph)
  • docs/AP_Federation/SignaturesFlows.md — HTTP signature flows

About

A Fediverse Activity server for your "tinyHome" on the web

Topics

Resources

Stars

Watchers

Forks

Contributors