Sn now includes built-in ActivityPub support, allowing your blog to participate in the federated social web (also known as the "Fediverse"). This means people can follow your blog from Mastodon, Pleroma, and other ActivityPub-compatible social networks.
- Features
- Configuration
- Storage Architecture
- Usage
- Multi-Author Posts
- Receiving Comments
- Development and Integration
- Security
- Moderation
- Environment Variables
- Troubleshooting
- Performance Considerations
- Compatibility
- Contributing
- Resources
Note for Contributors: This application is designed to be completely self-contained and must work with both local files and virtual filesystem (git mode). Do not create external scripts or utilities that require direct filesystem access, as they will not work in git mode where files are stored in a virtual filesystem only accessible to the running application. All recovery and maintenance operations should be implemented as built-in commands accessible via the main sn binary.
- Actor Profile: Each user becomes an ActivityPub actor that can be discovered and followed
- WebFinger Discovery: Support for
/.well-known/webfingerendpoint for actor discovery - Post Federation: Automatically publishes new blog posts to followers
- Comment Support: Receives replies/comments from the fediverse (stored as files)
- HTTP Signatures: Cryptographic verification of all federated activities
- Dual Storage: Clean separation of content and ActivityPub data using git branches
The absolute minimum to enable ActivityPub:
title: "My Blog"
rooturl: "https://myblog.com/"
activitypub:
enabled: true
primary_user: "admin"
master_key: "your-secret-master-key-here"
users:
admin:
displayName: "Blog Author"
passwordhash: "$2a$10$..." # Use `sn passwd admin` to generateThat's it! Everything else is derived automatically from your existing config.
title: "My Awesome Blog"
rooturl: "https://myblog.example.com/"
# ActivityPub configuration
activitypub:
enabled: true
primary_user: "admin"
master_key: "your-secret-master-key-here"
branch: "activitypub-data"
commit_interval_minutes: 10
# Optional overrides (only specify if different from main config):
# title: "Different ActivityPub Name" # Override title for ActivityPub
# rooturl: "https://public-domain.com/" # Override rooturl for ActivityPub
# domain: "public-domain.com" # Override domain for ActivityPub
icon: "https://myblog.example.com/icon.png" # Optional profile icon
banner: "https://myblog.example.com/banner.png" # Optional profile banner
# insecure: false # Allow HTTP (for development)
# Users (at least one required)
users:
admin:
displayName: "John Doe"
bio: "Tech blogger and open source enthusiast"
passwordhash: "$2a$10$..."
alice:
displayName: "Alice Johnson"
bio: "Senior Developer"
passwordhash: "$2a$10$..."
# Repository configuration
repos:
blog:
path: "posts"
activitypub: true # Enable ActivityPub for this repo
owner: "admin" # Fallback user for this repo
drafts:
path: "drafts"
activitypub: false # Disable ActivityPub for draftsActivityPub automatically reuses your existing config:
- Site Name: Uses
title(override withactivitypub.titleif needed) - Domain: Extracted from
rooturlhost (override withactivitypub.domainif needed) - Base URL: Uses
rooturl(override withactivitypub.rooturlif needed)
activitypub.titleoverridestitleactivitypub.rooturloverridesrooturlactivitypub.domainoverrides domain (parsed fromrooturl)
No separate site section needed - all ActivityPub settings live together.
activitypub.enabled: Enable/disable ActivityPub functionality (default: false)activitypub.primary_user: Which user should be the main ActivityPub actoractivitypub.master_key: Master key for encrypting ActivityPub keys (REQUIRED when enabled)
activitypub.branch: Git branch name for storing ActivityPub data (default: "activitypub-data")activitypub.commit_interval_minutes: How often to commit ActivityPub changes (default: 10, set to 0 for immediate commits - useful for testing)
activitypub.title: Override site name for ActivityPub (different fromtitle)activitypub.rooturl: Override base URL for ActivityPub (different fromrooturl)activitypub.domain: Override domain for ActivityPub (different fromrooturlhost)activitypub.icon: URL to your ActivityPub profile icon/avataractivitypub.banner: URL to your ActivityPub profile banner imageactivitypub.insecure: Allow HTTP for development (default: false)
activitypub: Whether posts from this repo should be federated (default: true if global ActivityPub is enabled)owner: Fallback user when posts don't specify valid authors (used mainly for deletions)
title: "Dev Blog"
rooturl: "http://localhost:8080/"
activitypub:
enabled: true
primary_user: "dev"
master_key: "dev-master-key-123"
commit_interval_minutes: 0 # Immediate commits for testing
insecure: true # Allow HTTP for local testing
users:
dev:
displayName: "Developer"
passwordhash: "$2a$10$..."title: "Company Internal Blog"
rooturl: "https://internal.company.com/"
activitypub:
enabled: true
primary_user: "editor"
master_key: "production-master-key-very-secure"
# Public federation uses different domain:
title: "ACME Corp Tech Blog"
rooturl: "https://blog.company.com/"
icon: "https://blog.company.com/logo.png"
banner: "https://blog.company.com/banner.jpg"
users:
editor:
displayName: "Chief Editor"
passwordhash: "$2a$10$..."Sn uses a unique dual-checkout approach to keep ActivityPub data separate from your content:
Main Repository (main branch):
├── posts/
├── pages/
├── config.yaml
└── (no ActivityPub data)
ActivityPub Repository (activitypub-data branch):
├── posts/ # Merged from main branch
├── pages/ # Merged from main branch
├── config.yaml # Merged from main branch
└── .activitypub/ # Only exists on this branch
├── keys.json
├── metadata.json
├── users/ # Per-user ActivityPub data
│ ├── alice/
│ │ ├── followers.json
│ │ └── following.json
│ └── bob/
│ ├── followers.json
│ └── following.json
└── comments/
└── blog/
└── my-post-slug/
└── comment-123.json
- Clean Separation: Main branch contains only your content
- Historical Correlation: ActivityPub commits reference content state
- No Branch Switching: Two separate working directories
- Complete Audit Trail: Git history shows relationship between content and engagement
- Per-User Data: Each user has their own followers/following stored separately
In local filesystem mode, ActivityPub data is stored in a .activitypub/ directory within your main content directory.
Once configured, your blog becomes discoverable through:
- WebFinger:
https://yourdomain.com/.well-known/webfinger?resource=acct:username@yourdomain.com - Actor Profile:
https://yourdomain.com/@username
People can follow your blog by searching for @username@yourdomain.com in their ActivityPub client.
When ActivityPub is enabled for a repo, new posts are automatically:
- Published: Sent to all followers as
Createactivities from the post's primary author - Updated: Changes sent as
Updateactivities from the same author - Deleted: Deletions sent as
Deleteactivities (may fall back to repo owner)
Sn supports multi-author posts with proper ActivityPub attribution and federation.
Posts can have multiple authors specified in their frontmatter:
---
title: "Collaborative Post"
authors:
- alice
- bob
---The system determines the publishing author using this order:
- Post Authors: Uses the first valid author from the post's frontmatter
- Repo Owner: Falls back to the repo's configured owner
- Primary User: Falls back to the global
activitypub.primary_user - First User: Falls back to the first user in the configuration
Single Author Post:
authors:
- alice- Actor:
@alice@domain.com(publishes the post) - AttributedTo:
"https://domain.com/@alice" - Delivered to: Alice's followers
Multi-Author Post:
authors:
- alice # Primary author
- bob # Co-author- Actor:
@alice@domain.com(primary author publishes) - AttributedTo:
["https://domain.com/@alice", "https://domain.com/@bob"] - CC: Both Alice's and Bob's followers
- Delivered to: Alice's followers (primary author)
Invalid Author Fallback:
authors:
- nonexistent_user- Fallback to: Repo owner with warning logged
- Actor:
@admin@domain.com(or configured fallback)
- Login as any user at
/_/frontend - Create a new post - automatically uses logged-in user as author
- Post federates from that user's ActivityPub actor
---
title: "Team Collaboration"
authors:
- alice
- bob
- charlie
---
This post was written by our entire team working together.The ActivityPub object for a multi-author post looks like:
{
"@context": ["https://www.w3.org/ns/activitystreams"],
"type": "Article",
"attributedTo": [
"https://myblog.com/@alice",
"https://myblog.com/@bob"
],
"name": "Collaborative Post",
"content": "Post content...",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": [
"https://myblog.com/@alice/followers",
"https://myblog.com/@bob/followers"
]
}The Create Activity has:
- Actor: The primary author (alice)
- Object: The article with multiple attributedTo values
- CC: Followers of all authors for maximum reach
Each user maintains separate ActivityPub data:
.activitypub/users/
├── alice/
│ ├── followers.json
│ └── following.json
├── bob/
│ ├── followers.json
│ └── following.json
└── admin/
├── followers.json
└── following.json
title: "Multi-Author Blog"
rooturl: "https://yourdomain.com/"
activitypub:
enabled: true
primary_user: "admin"
master_key: "test-master-key-123"
commit_interval_minutes: 0 # Immediate commits for testing
users:
admin:
displayName: "Site Admin"
passwordhash: "$2a$10$..."
alice:
displayName: "Alice Johnson"
passwordhash: "$2a$10$..."
bob:
displayName: "Bob Wilson"
passwordhash: "$2a$10$..."# Test each user's profile
curl -H "Accept: application/activity+json" https://yourdomain.com/@alice
curl -H "Accept: application/activity+json" https://yourdomain.com/@bob
# Check separate follower lists
curl -H "Accept: application/activity+json" https://yourdomain.com/@alice/followers
curl -H "Accept: application/activity+json" https://yourdomain.com/@bob/followers- "No valid authors found": Add authors to
usersconfig section - "Failed to publish to ActivityPub": Restart Sn to generate keys
- Posts from wrong author: Check logs for fallback warnings
- Primary Author First: List the main author first (they become the ActivityPub actor)
- Valid Users Only: Only include authors who exist in the
usersconfiguration - Consider Followers: Primary author's followers will see the post
- Consistent Usernames: Use consistent usernames between config and frontmatter
Comments/replies from the fediverse are:
- Received: Via the inbox endpoint
- Verified: HTTP signatures are checked
- Stored: As JSON files in the ActivityPub storage
- Available: Through the ActivityPub manager for display in templates
if ActivityPubManager != nil && ActivityPubManager.IsEnabled() {
// ActivityPub functionality available
}blogPost := &activitypub.BlogPost{
Title: "My Blog Post",
URL: "https://myblog.com/posts/my-post",
HTMLContent: "<p>Post content...</p>",
MarkdownContent: "Post content...",
PublishedAt: time.Now(),
Tags: []string{"tech", "blog"},
Authors: []string{"alice", "bob"}, // Multiple authors supported
Repo: "posts",
Slug: "my-post",
}
err := ActivityPubManager.PublishPost(blogPost)When a post has multiple authors, the ActivityPub object will look like this:
{
"@context": ["https://www.w3.org/ns/activitystreams"],
"type": "Article",
"attributedTo": [
"https://myblog.com/@alice",
"https://myblog.com/@bob"
],
"name": "Collaborative Post",
"content": "Post content...",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": [
"https://myblog.com/@alice/followers",
"https://myblog.com/@bob/followers"
]
}The Activity (Create/Update) will have:
- Actor: The primary author (alice)
- Object: The article with multiple attributedTo values
- CC: Followers of all authors (for maximum reach)
comments, err := ActivityPubManager.GetComments("posts", "my-post-slug")All ActivityPub requests are signed using RSA-SHA256 HTTP signatures. Sn:
- Generates 2048-bit RSA keys automatically on first run
- Signs all outgoing requests
- Verifies signatures on incoming requests
- Stores keys encrypted using AES-GCM with the master key
ActivityPub keys are stored encrypted to protect against unauthorized access:
- Required Configuration:
activitypub.master_keymust be set when ActivityPub is enabled - Application Won't Start: Missing master key prevents ActivityPub initialization
- Environment Override: Can be set via
SN_ACTIVITYPUB__MASTER_KEYenvironment variable - Key Derivation: Master key string is hashed with SHA-256 to create 32-byte AES key
- Algorithm: AES-256-GCM (authenticated encryption)
- Storage: RSA keys encrypted and stored in
.activitypub/keys.json - Nonce: Random nonce generated for each encryption operation
- Base64 Encoding: Encrypted data is base64-encoded for safe file storage
- File Permissions: Key file stored with 0600 permissions (owner read/write only)
- Unique Master Keys: Use different master keys for development, staging, and production
- Key Length: Use long, randomly generated master keys (32+ characters recommended)
- Environment Variables: Store master key in environment variables, not config files
- Backup: Securely backup master key - losing it makes existing encrypted keys unrecoverable
- Rotation: If master key is compromised, regenerate ActivityPub keys with new master key
# Generate a secure random master key (Linux/macOS)
openssl rand -base64 32
# Alternative using /dev/urandom
head -c 32 /dev/urandom | base64
# Generate using Python
python3 -c "import secrets; print(secrets.token_urlsafe(32))"Example usage:
# Set via environment variable (recommended)
export SN_ACTIVITYPUB__MASTER_KEY="$(openssl rand -base64 32)"
./sn serve
# Or set in config (less secure)
# activitypub.master_key: "your-generated-key-here"ActivityPub endpoints include built-in protections:
- Request validation: Malformed requests are rejected
- Signature verification: Unsigned or invalid signatures are rejected
- User validation: Requests for non-existent users return 404
The codebase includes reserved interfaces for two-tier comment moderation:
- Inbound Storage Filtering: Filter comments before storage
- Display Filtering: Filter stored comments before template rendering
These systems are designed but not yet implemented, allowing for future moderation capabilities without architectural changes.
SN_ACTIVITYPUB__MASTER_KEY=your-secret-master-keySN_GIT_REPO=https://github.com/user/blog.git
SN_GIT_USERNAME=your-username
SN_GIT_PASSWORD=your-token-or-passwordSN_CONFIG=/path/to/sn.yamlIf you have old configuration with duplicate values, clean it up:
# OLD - Remove these duplicates:
title: "My Blog"
rooturl: "https://myblog.com/"
site:
name: "My Blog" # ❌ Remove (duplicate of title)
domain: "myblog.com" # ❌ Remove (from rooturl)
base_url: "https://myblog.com/" # ❌ Remove (duplicate of rooturl)
icon: "/icon.png" # ❌ Move to activitypub section
# NEW - Clean structure:
title: "My Blog"
rooturl: "https://myblog.com/"
activitypub:
enabled: true
master_key: "your-secret-key" # ✅ Required for encryption
icon: "/icon.png" # ✅ Moved here (ActivityPub-specific)- Check Configuration: Ensure
activitypub.enabled: true - Master Key Missing: Ensure
activitypub.master_keyis set (required) - Verify Users: At least one user must be configured
- Check Logs: Look for ActivityPub initialization messages
- Git Mode Key Issues: If using
SN_GIT_REPO, use./sn regen-keysfor key problems - Test WebFinger: Try accessing
/.well-known/webfinger?resource=acct:user@yourdomain.com
- Missing Branch: The ActivityPub branch is created automatically on first run
- Permission Issues: Ensure git credentials have push access
- Conflicts: ActivityPub data commits are designed to avoid conflicts
-
Missing Master Key:
activitypub.master_key is requirederror- Set
activitypub.master_keyin config orSN_ACTIVITYPUB__MASTER_KEYenv var - Application won't start without this value when ActivityPub is enabled
- Set
-
Base64 Decoding Error:
failed to decode base64: illegal base64 dataerror- Corrupted or incompatible keys.json file from previous version
- Git Mode (SN_GIT_REPO set): Use command:
./sn regen-keys(ONLY option for git mode) - Local Mode: Delete corrupted keys file:
rm .activitypub/keys.jsonOR use./sn regen-keys - Restart Sn to generate new encrypted keys
- Note: Existing followers will need to re-follow your accounts
-
Key Decryption Failed:
failed to decrypt keyserror- Master key changed but existing encrypted keys.json exists
- Wrong master key being used for existing encrypted file
- Delete
.activitypub/keys.jsonto regenerate with new master key - Or restore original master key value
-
Keys File Corrupted:
keys file corruptederror- File became corrupted or partially written
- Git Mode Recovery (when using SN_GIT_REPO):
# ONLY option for git mode - files are in virtual filesystem ./sn regen-keys ./sn serve - Local Mode Recovery:
# Use built-in command (recommended) ./sn regen-keys # OR manual steps (LOCAL MODE ONLY - requires direct filesystem access) cp -r .activitypub activitypub-backup-$(date +%Y%m%d) rm .activitypub/keys.json ./sn serve
-
Key Generation Failed:
failed to generate RSA keyerror- Insufficient system entropy - restart and try again
- Check system permissions for random number generation
-
File Permission Issues: Can't read/write keys.json
- Ensure
.activitypub/directory has proper permissions - Keys file should be 0600 (owner read/write only)
- Ensure
- HTTP Signatures: Check logs for signature verification errors
- ActivityPub Compatibility Issues:
cannot unmarshal object into Go struct field Actor.@contexterror- Remote servers sending different
@contextformats (object vs array) - This has been fixed in recent versions - update your Sn binary
- The application now accepts flexible ActivityPub JSON formats
- Remote servers sending different
- Network: Ensure your server is reachable from the internet
- SSL/TLS: ActivityPub requires HTTPS in production
ActivityPub data is normally committed periodically to avoid:
- Excessive Git History: Too many micro-commits
- Performance Impact: Frequent I/O operations
- Remote Pressure: Constant pushes to git remote
For testing, set commit_interval_minutes: 0 to commit immediately after each change.
- Activity Delivery: Sent to followers in background
- Comment Processing: Handled asynchronously
- Key Generation: Only done once on initialization
- Single Delivery: Multi-author posts are delivered once from the primary author
- Per-User Storage: Each user's followers/following are stored separately
- Aggregated Reach: Posts include all authors' followers in CC for maximum visibility
- Mastodon: Full compatibility
- Pleroma: Full compatibility
- Misskey: Basic compatibility
- PeerTube: Follows and comments work
Sn implements core ActivityPub features according to the W3C specification:
- Actor profiles and discovery
- Activity delivery (Create, Update, Delete)
- Collections (followers, following, outbox)
- HTTP Signatures for authentication
When contributing ActivityPub features:
- Follow Design Philosophy: Keep it simple and maintainable
- Test Federation: Verify with real ActivityPub servers
- Document Changes: Update this file for new features
- Consider Storage: Ensure changes work with dual-checkout approach