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 @[email protected] - 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.
Process incoming activities:
python -m activity_processor` #or set up as a cron jobNote: Activities received in the inbox are automatically queued to be processed!
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
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
File Structure:
data/
├── actor.json # Your actor profile (auto-generated)
├── followers.json # Collection of followers (auto-generated)
├── 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
└── inbox/ # Received activities from other servers
├── follow-*.json # Activity files (original, untouched)
├── follow-*.meta.json # Provenance metadata (signed_by, received_at)
└── queue/ # Symlinks to 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)
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 — Centralize file-based data access (followers, following, blocked, posts) into a shared module, replacing scattered direct file I/O across processors and endpoints
- Outbox queue processing — Move outbox delivery into the queue system so CLI tools just create + queue activities, and the processor handles delivery (with retry on failure)
- 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
Activity Types:
- 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:
- Following — Send Follow activities, maintain
following.json, handle Accept/Reject - 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