A minimalist ActivityPub server designed for easy integration with personal websites.
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.
- 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):
- Building tinyFedi - part 1: Here we explore the basics of AP and build around Actors and its Outbox.
- Building tinyFedi - part 2: We finish the basics by building around the Inbox and Activity delivery.
- Building tinyFedi - part 3: coming soon HTTP Signatures
- Building tinyFedi - part 4: coming soon Update, Like, and Annouce Activities.
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.pemSecurity Note: Keys are automatically excluded from version control via .gitignore.
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
python app.pyAdd 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 jobNote: Activities received in the inbox AND outgoing activities placed in the outbox queue (e.g., follows) are both processed by this single command.
Designed to run behind a reverse proxy alongside existing websites:
location /activitypub/ {
proxy_pass http://localhost:5000/activitypub/;
}Install dependencies and run the test suite:
pip install -r requirements.txt
python -m pytest tests/ -vThis 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.
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.jsonand runtime data
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:
FollowProcessor→FollowUndoFollowProcessor→Undo.FollowAcceptFollowProcessor→Accept.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.
Implemented:
- WebFinger Discovery -
.well-known/webfingerfor 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
streamsarray 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_processordelivers - Data Access Layer -
data_access/follow.pyprovides 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 tofalsefor manual approval of followersrequire_http_signatures- Require HTTP signatures on all incoming activities (default: false). Set totruefor production to reject unsigned server-to-server trafficmax_page_size- Maximum items per page for paginated collections like outbox (default: 20). Clients can request smaller pages via?limit=N
Architecture:
- Data access layer — Extend the storage-agnostic API beyond
data_access/follow.pyto cover blocked, posts, inbox provenance, etc., replacing the remaining direct file I/O in processors and endpoints - Integrate delivery into processors — Move
activity_delivery.pyinto theactivity_processormodule asdelivery.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.pyto the outbox queue —client/new_post.pystill delivers synchronously; switch it to the same queue + processor flow used byclient/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 materialization —
streams/homewith 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'sloggingmodule) - Manual follow approval workflow
- Mention and reply handling
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
