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.
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
File Structure:
data/
├── actor.json # Your actor profile (auto-generated)
├── followers.json # Collection of followers (auto-generated)
├── posts/ # Individual post objects (UUID directories)
│ └── {uuid}/
│ ├── post.json
│ └── likes.json # Per-post likes collection (auto-generated)
├── outbox/ # Outgoing activity objects
│ └── create-20250921-143022-123456.json
└── inbox/ # Received activities from other servers
├── follow-*.json # Follow requests received
├── undo-*.json # Unfollow activities received
└── 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
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:
- 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 — Per-post shares collection at
/posts/{id}/shares, withUndo(Announce). See AP §5.8 - Delete — Tombstoning posts + federated Delete delivery. See AP §6.11
- EmojiReact — Rich reactions per FEP-c0e0
Other:
- Proper logging system (replace
print()with Python'sloggingmodule) - Manual follow approval workflow
- Mention and reply handling
- Client-to-Server endpoints (see
docs/C2S.mdfor design)
Completed:
- ✅ Deprecation fixes (Python 3.11+ datetime)
- ✅ Update Activity support with federated notifications
- ✅ UUID-based post IDs with dedicated directories
- ✅ Outbox folder organization with dynamic paginated endpoint
- ✅ Activity ID microsecond timestamps
- ✅ Activity processor module with auto-discovery and config-as-parameter
- ✅ Like activity processing with per-post likes collection and endpoint
- ✅ Undo(Like) with cleanup of empty likes collections
See docs/ for detailed design documents:
docs/C2S.md— Client-to-Server API designdocs/AP_Federation/SignaturesFlows.md— HTTP signature flows
