artvandervennet/odyfeed
Experience the Odyssey like never beforeβwhere Odysseus, Poseidon, and Athena share their stories across the federated web, all while your data remains sovereign in your personal Solid Pod.
OdyFeed ποΈ
A decentralized social network that brings Greek mythology to the modern web through ActivityPub federation, Solid Pod storage, and Webmentions.
Experience the Odyssey like never beforeβwhere Odysseus, Poseidon, and Athena share their stories across the federated web, all while your data remains sovereign in your personal Solid Pod.
Table of Contents
- What is OdyFeed?
- Features
- Setup & Development
- Project Structure
- Technologies Deep Dive
- Common Pitfalls & Troubleshooting
- Testing & Verification
- Deployment Considerations
- Contributing
- Resources & Further Reading
What is OdyFeed?
OdyFeed is an educational demonstration of modern decentralized web technologies, showcasing how to build a federated social network that respects user privacy and data sovereignty. It combines three powerful protocols:
- π ActivityPub: For federated social networking (compatible with Mastodon, Pleroma, and other fediverse platforms)
- π Solid Pods: For user-controlled data storage with fine-grained access control
- π¬ Webmentions: For decentralized comments and interactions across the web
The application tells the story of Homer's Odyssey through the eyes of mythological characters (Odysseus, Poseidon, Athena), who "post" about events as they unfold. Users can authenticate with their Solid Pod, create posts, interact with content, and federate with other ActivityPub serversβall while maintaining full control over their data.
Why OdyFeed?
This project was created as a learning resource and proof-of-concept for:
- Understanding ActivityPub federation mechanics (inbox/outbox, HTTP signatures, actor discovery)
- Implementing Solid OIDC authentication and Pod storage operations
- Working with RDF/Turtle and Linked Data principles
- Building a modern web application with Vue 3, Nuxt, and TypeScript
- Exploring decentralized web standards in a practical context
Features
Core Functionality
β Federated Social Networking
- Full ActivityPub implementation (Follow, Like, Reply, Announce)
- HTTP Signature verification for secure federation
- Compatible with Mastodon and other fediverse platforms
β Solid Pod Integration
- OAuth 2.0 / OIDC authentication with any Solid provider
- Automatic Pod container creation with proper ACL permissions
- Activity storage in user's Pod (inbox/outbox as JSON-LD files)
- Profile data stored as RDF/Turtle
β Webmention Support
- Receive webmentions on posts
- Parse microformats2 (h-entry, h-card)
- Automatic validation and storage
β Mythological Narrative
- Pre-defined actors (Greek gods) with unique personalities
- Story events from the Odyssey served as Linked Data
- AI-generated posts (OpenAI) that match character tone
- Timeline grouped by narrative events
β Modern Web UI
- Vue 3 Composition API with
<script setup> - Nuxt 4 with SSR disabled (client-side rendering)
- Nuxt UI components with Tailwind CSS
- Dark mode support
- Responsive design
Setup & Development
Prerequisites
Ensure you have the following installed:
- Node.js v18+ (LTS recommended)
- pnpm v8+ (Package manager)
- A Solid Pod (get one from solidcommunity.net or inrupt.net)
Environment Variables
Create a .env file in the root directory:
# Application Base URL
# CRITICAL: This must match your deployment domain!
# For local development:
BASE_URL=http://localhost:3000
# For production (example):
# BASE_URL=https://odyfeed.example.com
# OpenAI API Key (optional, for AI-generated posts)
# Get your key from https://platform.openai.com/api-keys
OPENAI_API_KEY=sk-your-api-key-here
# ActivityPub Pagination (optional, defaults to 20)
ACTIVITYPUB_PAGE_SIZE=20Important Notes
- BASE_URL: The
clientid.jsonldfile is automatically generated from this value. Solid providers use this URL for OAuth redirects. - OPENAI_API_KEY: Only required if you want AI-generated posts during user registration. The app works without it, but sample posts will use fallback content.
Installation
# Clone the repository
git clone https://github.com/yourusername/OdyFeed.git
cd OdyFeed
# Install dependencies
pnpm install
# Verify installation
pnpm run devRunning the Application
Development Mode
pnpm run devThe application will be available at http://localhost:3000.
Production Build
# Build the application
pnpm run build
# Preview production build
pnpm run previewGenerate Static Site (Not Recommended)
pnpm run generate
β οΈ Note: Since OdyFeed uses Solid authentication (client-side only), static generation has limited use. Keepssr: falseinnuxt.config.ts.
Project Structure
OdyFeed/
βββ app/ # Frontend application
β βββ api/ # Client-side API functions
β β βββ activities.ts # ActivityPub activity creators
β β βββ actors.ts # Actor profile fetching
β β βββ auth.ts # Authentication API calls
β β βββ timeline.ts # Timeline data fetching
β βββ assets/
β β βββ css/
β β βββ main.css # Global styles & CSS variables
β βββ components/
β β βββ Actor/ # Actor-specific components
β β βββ atoms/ # Atomic UI components
β β βββ Form/ # Form components
β β βββ Post/ # Post display components
β β βββ Webmention/ # Webmention components
β β βββ AppHeader.vue
β β βββ AppFooter.vue
β β βββ ...
β βββ composables/ # Vue composables (reusable logic)
β β βββ useAuth.ts # Authentication state & actions
β β βββ useAuthProviders.ts # Solid provider discovery
β β βββ useModal.ts # Modal management
β β βββ usePostActions.ts # Like/Reply/Share actions
β β βββ ...
β βββ layouts/
β β βββ default.vue # Default layout with header/footer
β βββ middleware/
β β βββ auth.ts # Route authentication guard
β βββ mutations/ # Pinia Colada mutations (writes)
β β βββ auth.ts # Login/logout/register mutations
β β βββ like.ts # Like/unlike mutations
β β βββ reply.ts # Reply creation mutation
β β βββ webmention.ts # Webmention sending mutation
β βββ pages/ # Nuxt pages (routes)
β β βββ index.vue # Home timeline
β β βββ about.vue # About page
β β βββ inbox.vue # User inbox
β β βββ profile.vue # User profile
β β βββ register.vue # Registration page
β β βββ ...
β βββ plugins/
β β βββ auth-session.client.ts # Initialize auth session
β β βββ solid-vcard.client.ts # Register Solid vCard web component
β βββ queries/ # Pinia Colada queries (reads)
β β βββ auth.ts # Auth status query
β β βββ inbox.ts # User inbox query
β β βββ post.ts # Single post query
β β βββ replies.ts # Post replies query
β β βββ timeline.ts # Timeline query
β β βββ webmentions.ts # Webmentions query
β βββ stores/
β β βββ authStore.ts # Pinia auth state (central store)
β βββ types/
β β βββ index.ts # Type definitions
β β βββ oidc.ts # OIDC-specific types
β βββ utils/
β βββ authHelper.ts # Auth utility functions
β βββ fetch.ts # Custom fetch wrapper
β βββ oidc.ts # OIDC utilities
β βββ postHelpers.ts # Post formatting helpers
β βββ queryKeys.ts # Query key factory
β βββ rdf.ts # RDF parsing (client-side)
β βββ solidHelpers.ts # Solid Pod helpers
β
βββ server/ # Backend (Nitro API)
β βββ api/
β β βββ actors/
β β β βββ [username]/
β β β βββ index.get.ts # Actor profile endpoint
β β β βββ inbox.get.ts # Get user inbox (private)
β β β βββ inbox.post.ts # Receive federated activities
β β β βββ outbox.get.ts # Get user outbox (public)
β β β βββ outbox.post.ts # Send activities (federation)
β β β βββ status/
β β β βββ [id].get.ts # Individual post endpoint
β β βββ auth/
β β β βββ callback.get.ts # OAuth callback handler
β β β βββ login.post.ts # Initiate Solid login
β β β βββ logout.post.ts # Clear session
β β β βββ register.post.ts # Register new user
β β β βββ status.get.ts # Check auth status
β β βββ timeline.get.ts # Aggregated timeline
β β βββ webmentions/
β β βββ index.get.ts # List webmentions
β β βββ index.post.ts # Receive webmention
β βββ middleware/
β β βββ auth.ts # Inject auth context
β β βββ errorHandler.ts # Global error logging
β βββ routes/
β β βββ .well-known/
β β β βββ webfinger.ts # WebFinger endpoint (federation)
β β βββ actors.ts # Serve actors.ttl
β β βββ clientid.jsonld.ts # OIDC client document
β β βββ events.ts # Serve events.ttl
β β βββ vocab.ts # Serve vocabulary
β βββ utils/ # Server utilities
β βββ aclGenerator.ts # Generate Solid ACL rules
β βββ actorEndpointHelpers.ts # ActivityPub helpers
β βββ actorHelpers.ts # Actor profile generation
β βββ authHelpers.ts # Auth validation
β βββ crypto.ts # RSA key generation, HTTP signing
β βββ federation.ts # ActivityPub federation logic
β βββ fileStorage.ts # Local file storage wrapper
β βββ httpSignature.ts # HTTP Signature verification
β βββ logger.ts # Structured logging
β βββ microformats.ts # Webmention microformat parsing
β βββ podStorage.ts # Solid Pod read/write operations
β βββ postGenerator.ts # Generate mythological posts (AI)
β βββ rdf.ts # RDF/Turtle parsing
β βββ sessionCookie.ts # Session cookie management
β βββ sessionStorage.ts # Session persistence
β βββ solidSession.ts # Solid Session hydration
β βββ solidStorage.ts # Solid storage backend
β βββ typeIndexGenerator.ts # Generate Solid Type Indexes
β
βββ shared/ # Shared between client & server
β βββ constants.ts # Namespaces, endpoints, defaults
β βββ types/
β βββ activitypub.ts # ActivityPub interfaces
β βββ api.ts # API request/response types
β βββ base.ts # Base types
β βββ index.ts # Type exports
β βββ mappers.ts # Data transformation utilities
β βββ mutations.ts # Mutation payload types
β βββ solid.ts # Solid ACL types
β βββ webmention.ts # Webmention types
β
βββ public/ # Static assets
β βββ actors.ttl # Mythological actors (Greek gods)
β βββ events.ttl # Mythological events (Odyssey)
β βββ vocab.ttl # Custom RDF vocabulary
β βββ favicon.ico
β βββ robots.txt
β
βββ data/ # Runtime data (not in version control)
β βββ sessions/ # User session metadata
β βββ solid-sessions/ # Solid session storage (DPoP keys)
β βββ posts/ # Published posts (JSON-LD)
β βββ users/
β βββ webid-mappings.json # WebID β username β actorId mappings
β
βββ logs/
β βββ activitypub.log # Application logs
β
βββ .env # Environment variables
βββ .env.example # Environment template
βββ nuxt.config.ts # Nuxt configuration
βββ package.json
βββ pnpm-lock.yaml
βββ tsconfig.json
βββ eslint.config.mjs
Frontend Architecture
Framework: Nuxt 4 (Vue 3 + Composition API)
Key Patterns:
- Composition API: All components use
<script setup>for cleaner, more maintainable code - Pinia Colada: Data fetching with queries (reads) and mutations (writes)
- Composables: Reusable logic extracted into
composables/directory - Atomic Design: Components organized by complexity (atoms β molecules β organisms)
State Management:
- Pinia: Central auth store (
authStore.ts) - Pinia Colada: Query caching with automatic invalidation
Styling:
- Nuxt UI: Pre-built components with Tailwind CSS
- Modern CSS: Native nesting, CSS variables, logical properties
- Dark Mode: Full theme support
Backend Architecture
Framework: Nitro (Nuxt's server engine)
Key Responsibilities:
- ActivityPub Federation: Handle inbox/outbox, HTTP signatures, actor serving
- Solid Pod Operations: Read/write to user Pods, manage sessions
- Webmention Processing: Receive, validate, store webmentions
- API Endpoints: Serve timeline, auth, and data endpoints
Data Storage:
- Solid Pods: User data (posts, profile, activities)
- Local Filesystem: Sessions, mappings, webmentions, published posts
- In-Memory: Active session cache (DPoP keys)
Shared Resources
Constants (shared/constants.ts):
- Namespaces (ActivityStreams, Solid, FOAF, etc.)
- Activity types (Note, Like, Follow, etc.)
- File paths, Pod containers, endpoint paths
Types (shared/types/):
- TypeScript interfaces for ActivityPub objects
- Solid Pod ACL configurations
- API request/response types
- Webmention structures
Technologies Deep Dive
ActivityPub Federation
ActivityPub is a W3C recommendation for decentralized social networking. OdyFeed implements both the Client-to-Server (C2S) and Server-to-Server (S2S) protocols.
How It Works
- Actor Discovery: Each user has a unique actor ID (e.g.,
https://odyfeed.example.com/api/actors/alice) - Inbox/Outbox: Actors have an inbox (receive) and outbox (send) for activities
- Federation: Activities are sent to remote servers with HTTP Signatures for authentication
- Collections: Followers, following, and posts are served as ActivityStreams Collections
Core Endpoints
| Endpoint | Method | Description | Public? |
|---|---|---|---|
/api/actors/:username |
GET | Actor profile (Person object) | β Yes |
/api/actors/:username/inbox |
GET | User's inbox (paginated) | β Auth required |
/api/actors/:username/inbox |
POST | Receive activities from remote servers | β Yes (with HTTP Signature) |
/api/actors/:username/outbox |
GET | User's outbox (paginated) | β Yes |
/api/actors/:username/outbox |
POST | Send activities (Like, Reply, etc.) | β Auth required |
/api/actors/:username/status/:id |
GET | Individual post | β Yes |
/.well-known/webfinger |
GET | WebFinger discovery | β Yes |
HTTP Signatures
All federated activities are signed using RSA-SHA256. Each actor has a public/private key pair stored in their Solid Pod (/settings/keys.json).
Signature Process:
- Generate request digest (SHA-256 hash of body)
- Create signing string from HTTP headers
- Sign with RSA private key
- Include
Signatureheader with request
Example Signature Header:
Signature: keyId="https://odyfeed.example.com/api/actors/alice#main-key",
algorithm="rsa-sha256",
headers="(request-target) host date digest content-type",
signature="Base64EncodedSignature=="
Implementation: See server/utils/crypto.ts and server/utils/httpSignature.ts
Inbox Flow (Receiving Activities)
// server/api/actors/[username]/inbox.post.ts
1. Receive activity from remote server
β
2. Verify HTTP Signature
- Fetch sender's public key from their actor profile
- Validate signature matches body and headers
β
3. Save activity to user's Solid Pod
- Store at /social/inbox/{activityId}.json
β
4. Process activity based on type
- Follow β Auto-send Accept activity
- Like β Update post likes collection
- Create (Reply) β Add to post replies
β
5. Return 202 AcceptedOutbox Flow (Sending Activities)
// server/api/actors/[username]/outbox.post.ts
1. Client sends activity to user's outbox
β
2. Validate user owns the outbox (auth check)
β
3. Save activity to user's Solid Pod
- Store at /social/outbox/{activityId}.json
β
4. Extract recipients (to, cc fields)
β
5. Dereference recipient actors
- Fetch each recipient's actor profile
- Extract their inbox URL
β
6. Federate activity to each inbox
- Sign request with user's private key
- POST to remote inbox
- Handle delivery failures gracefully
β
7. Return federation results
{
id: savedUrl,
federated: { total: 5, successful: 4, failed: 1 }
}Implementation: See server/utils/federation.ts
Mastodon Compatibility
OdyFeed is compatible with Mastodon and other fediverse platforms. To ensure interoperability:
Required Features:
- β
WebFinger endpoint (
/.well-known/webfinger) - β
Actor profile with
publicKeyfield - β HTTP Signatures on all federated requests
- β
Content-Type: application/ld+json; profile="https://www.w3.org/ns/activitystreams" - β
Proper
@contextwith Mastodon extensions (toot namespace)
Mastodon-Specific Context Extensions:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"toot": "http://joinmastodon.org/ns#",
"sensitive": "as:sensitive",
"votersCount": "toot:votersCount"
}
]
}Testing Federation with Mastodon:
- Deploy OdyFeed to a public HTTPS domain
- From Mastodon, search for
@yourname@yourdomain.com - Follow the OdyFeed user
- OdyFeed should auto-accept the follow
- Post from OdyFeed should appear in your Mastodon timeline
Common Issues:
- WebFinger must use HTTPS: Mastodon won't federate with HTTP-only servers
- Clock skew: HTTP Signature validation fails if server time is off by >5 minutes
- Missing headers: Ensure
Date,Digest, andHostheaders are present
Solid Pod Integration
Solid (Social Linked Data) is a web decentralization project that allows users to store their data in personal online data stores called Pods.
Authentication (OIDC)
OdyFeed uses Solid-OIDC (OpenID Connect) for authentication.
Flow:
1. User enters their WebID (e.g., https://alice.solidcommunity.net/profile/card#me)
β
2. Discover OIDC issuer from WebID
- Fetch WebID document (Turtle/JSON-LD)
- Extract solid:oidcIssuer predicate
β
3. Initiate OAuth flow
- Redirect to issuer with client_id (clientid.jsonld URL)
- Request scopes: openid, webid, offline_access
β
4. User authenticates at Solid provider
β
5. Provider redirects back with authorization code
- Callback: /api/auth/callback?code=...&state=...
β
6. Exchange code for tokens
- Access token, ID token, refresh token
- DPoP (Demonstrating Proof-of-Possession) bound tokens
β
7. Hydrate session on server
- Store session in data/sessions/
- Store Solid session (with DPoP keys) in data/solid-sessions/
- Create authenticated fetch function
β
8. Access user's Pod with authenticated fetch
Implementation: See server/api/auth/ and server/utils/solidSession.ts
Permissions (ACL - Access Control Lists)
Each container in the user's Pod has specific permissions:
| Container | Permission Type | Public Read? | Public Write? | Public Append? |
|---|---|---|---|---|
/social/inbox/ |
PublicAppendPrivateRead | β No | β No | β Yes |
/social/outbox/ |
PublicReadOwnerWrite | β Yes | β No | β No |
/social/followers/ |
PublicReadOwnerWrite | β Yes | β No | β No |
/social/following/ |
PublicReadOwnerWrite | β Yes | β No | β No |
/profile/ |
PublicReadOwnerWrite | β Yes | β No | β No |
/settings/ |
PrivateOwnerOnly | β No | β No | β No |
Why PublicAppendPrivateRead for Inbox?
- Remote ActivityPub servers need to POST activities (append)
- Only the owner should read their inbox (privacy)
- Prevents inbox snooping by third parties
ACL Example (Turtle format):
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
<#public>
a acl:Authorization;
acl:accessTo <./>;
acl:default <./>;
acl:agentClass foaf:Agent;
acl:mode acl:Append.
<#owner>
a acl:Authorization;
acl:accessTo <./>;
acl:default <./>;
acl:agent <https://alice.solidcommunity.net/profile/card#me>;
acl:mode acl:Read, acl:Write, acl:Control.Implementation: See server/utils/aclGenerator.ts
Get/Post to Pods
Library: @inrupt/solid-client for LDP (Linked Data Platform) operations
Writing Data (Save Activity):
import { saveFileInContainer } from '@inrupt/solid-client'
import { getAuthenticatedFetch } from '~/server/utils/solidSession'
const authenticatedFetch = await getAuthenticatedFetch(webId)
const activityBlob = new Blob([JSON.stringify(activity)], {
type: 'application/ld+json'
})
const savedFile = await saveFileInContainer(
containerUrl,
activityBlob,
{
slug: 'my-activity.json',
fetch: authenticatedFetch
}
)Reading Data (Fetch Activity):
import { getFile } from '@inrupt/solid-client'
const file = await getFile(activityUrl, { fetch: authenticatedFetch })
const text = await file.text()
const activity = JSON.parse(text)Listing Container Contents (Inbox/Outbox pagination):
import { getSolidDataset, getContainedResourceUrlAll } from '@inrupt/solid-client'
const dataset = await getSolidDataset(containerUrl, { fetch: authenticatedFetch })
const urls = getContainedResourceUrlAll(dataset)
// Returns: ['https://pod.example/social/inbox/activity1.json', ...]Implementation: See server/utils/podStorage.ts
clientid.jsonld
The clientid.jsonld file tells Solid providers how to configure OAuth for OdyFeed.
Auto-Generated from BASE_URL:
{
"@context": "https://www.w3.org/ns/solid/oidc-context.jsonld",
"client_id": "https://odyfeed.example.com/clientid.jsonld",
"client_name": "OdyFeed",
"client_uri": "https://odyfeed.example.com",
"logo_uri": "https://odyfeed.example.com/favicon.ico",
"redirect_uris": ["https://odyfeed.example.com/api/auth/callback"],
"scope": "openid webid offline_access",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}Served at: /clientid.jsonld
Why It's Important:
- Solid providers fetch this document during OAuth flow
client_idmust be a publicly accessible URLredirect_urismust exactly match callback URL- Changing
BASE_URLrequires regenerating this file (automatic on server start)
Implementation: See server/routes/clientid.jsonld.ts
Webmentions
Webmentions are a W3C recommendation for notifications between websites. When someone links to your content, you receive a webmention.
How It Works
1. Site A publishes content with link to Site B
<a href="https://odyfeed.example.com/post/123">Great post!</a>
β
2. Site A sends webmention to Site B
POST /api/webmentions
Content-Type: application/x-www-form-urlencoded
source=https://site-a.com/my-post
&target=https://odyfeed.example.com/post/123
β
3. Site B (OdyFeed) validates the webmention
- Fetch source URL
- Verify it contains a link to target
- Parse microformats2 (h-entry)
β
4. Store webmention
- Add to post's webmentions collection
- Include author info, content excerpt
β
5. Display on target post
- Show as comment/like/mention
Endpoints
| Endpoint | Method | Description |
|---|---|---|
/api/webmentions |
POST | Receive webmention |
/api/webmentions/site |
GET | List all site webmentions |
/api/webmentions/posts/:username/:id |
GET | List webmentions for specific post |
Advertise Webmention Endpoint:
<!-- In <head> -->
<link rel="webmention" href="https://odyfeed.example.com/api/webmentions">Implementation: See server/api/webmentions/index.post.ts
Microformats2 Parsing
OdyFeed parses microformats2 markup to extract metadata from source pages:
h-entry (Blog post/article):
<article class="h-entry">
<h1 class="p-name">Article Title</h1>
<p class="p-summary">Short description</p>
<div class="e-content">Full article content...</div>
<a href="https://odyfeed.example.com/post/123" class="u-in-reply-to">Reply</a>
<a href="https://author.com" class="p-author h-card">
<img src="avatar.jpg" class="u-photo" alt="">
<span class="p-name">Author Name</span>
</a>
<time class="dt-published" datetime="2026-01-20">Jan 20, 2026</time>
</article>Webmention Type Detection:
u-like-ofβ Likeu-repost-ofβ Repost/Shareu-in-reply-toβ Comment/Reply- Default β Mention
Implementation: See server/utils/microformats.ts
Linked Data (RDF)
OdyFeed uses RDF (Resource Description Framework) to represent structured data about mythological actors, events, and posts.
Events / Actors / Vocabulary
Actors (public/actors.ttl):
@prefix myth: <https://odyfeed.example.com/vocab#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
<actors/odysseus>
a myth:Actor ;
foaf:name "Odysseus" ;
myth:tone "slim, berekend, volhardend" ;
myth:avatar "https://api.dicebear.com/7.x/avataaars/svg?seed=odysseus" .
<actors/poseidon>
a myth:Actor ;
foaf:name "Poseidon" ;
myth:tone "wraakzuchtig, almachtig" ;
myth:avatar "https://api.dicebear.com/7.x/avataaars/svg?seed=poseidon" .Events (public/events.ttl):
@prefix myth: <https://odyfeed.example.com/vocab#> .
@prefix dct: <http://purl.org/dc/terms/> .
<events/01-trojan-horse>
a myth:Event ;
dct:title "De list van het paard" ;
myth:sequence 1 ;
myth:location "Troje" ;
myth:description "Met een houten paard mislukt Troje definitief." ;
myth:involvesActor <actors/odysseus>, <actors/athena> .
<events/02-cyclops-cave>
a myth:Event ;
dct:title "In de grot van de Cyclopen" ;
myth:sequence 2 ;
myth:location "Eiland van de Cyclopen" ;
myth:description "Polyphemus, de eenogige reus, verslindt enkele mannen..." ;
myth:involvesActor <actors/odysseus>, <actors/poseidon> .Vocabulary (public/vocab.ttl):
@prefix myth: <https://odyfeed.example.com/vocab#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
myth:Actor
a rdfs:Class ;
rdfs:comment "An actor in the mythological world." .
myth:Event
a rdfs:Class ;
rdfs:comment "A mythological event." .
myth:tone
a rdfs:Property ;
rdfs:label "tone" ;
rdfs:comment "The personality tone of an actor." .
myth:sequence
a rdfs:Property ;
rdfs:label "sequence" ;
rdfs:comment "The sequence number of an event." .Access:
/actorsβ Servesactors.ttl/eventsβ Servesevents.ttl/vocabβ Servesvocab.ttl
Data Generation
Post Generation with OpenAI:
When a user registers and matches a mythological actor (e.g., username "odysseus" β Odysseus), OdyFeed generates 3 sample posts using OpenAI:
// server/utils/postGenerator.ts
const prompt = `
You are ${actor.name}, the Greek god/hero.
Your personality: ${actor.tone}
Write a social media post about this event:
Event: ${event.title}
Description: ${event.description}
Requirements:
- Write in first person
- Match the character's tone
- Keep it under 280 characters
- Be engaging and dramatic
`
const response = await openai.chat.completions.create({
model: "gpt-4",
messages: [{ role: "user", content: prompt }],
max_tokens: 150
})
const postContent = response.choices[0].message.contentPosts are stored as:
- ActivityPub Note in local storage (
data/posts/{username}/{id}.jsonld) - Activity in User's Pod (
/social/outbox/{activityId}.json)
Timeline Aggregation:
The timeline groups posts by narrative events:
// server/api/timeline.get.ts
const events = parseEvents() // From events.ttl
const actors = parseActors() // From actors.ttl
const posts = await fetchAllUserPosts() // From Solid Pods
const groupedByEvent = events.map(event => ({
event,
posts: posts.filter(post => post.aboutEvent === event.id)
}))
return { groupedByEvent, mythActors: actors }Implementation: See server/utils/postGenerator.ts and server/utils/rdf.ts
Common Pitfalls & Troubleshooting
1. "accountId mismatch" Error During OAuth Callback
Symptom: OAuth callback fails with cryptic error about DPoP key mismatch.
Cause: The Session object used in callback doesn't have the same DPoP keys as the one used in login.
Fix: Ensure pendingSessions map is correctly tracking session instances.
Verification:
# Check server logs for:
# "β
Recovered pending session from memory"
# "DPoP key in storage: β
FOUND"Prevention: Don't restart the dev server between login and callback. If you must, implement disk-based pending session recovery.
2. HTTP Signature Verification Fails
Symptom: Remote servers reject your federated activities with 401/403.
Common Causes:
- Clock skew: Your server's clock is >5 minutes off
- Wrong key format: Private key not PEM-encoded PKCS#8
- Body mismatch:
Digestheader doesn't match request body
Debug Steps:
# 1. Check server time
Get-Date
# 2. Check logs (logs/activitypub.log)
Get-Content logs/activitypub.log -Tail 50
# 3. Test signature locally
curl -X POST http://localhost:3000/api/actors/testuser/inbox `
-H "Content-Type: application/ld+json" `
-d '{"type":"Follow","actor":"http://localhost:3000/api/actors/sender"}'Fix:
- Sync server clock:
w32tm /resync(Windows) - Verify key format in Pod:
/settings/keys.json - Check
Digestcalculation inserver/utils/crypto.ts
3. Pod Access Fails (401 Unauthorized)
Symptom: Server can't read/write to user's Pod.
Causes:
- Token expired: Refresh token invalid or revoked
- Wrong WebID: Session mapped to incorrect Pod
- ACL misconfiguration: Container ACLs too restrictive
Debug:
// Add to server/utils/podStorage.ts
const authenticatedFetch = await getAuthenticatedFetch(webId)
if (!authenticatedFetch) {
console.error('β No authenticated fetch for', webId)
}Fix - Re-authenticate:
- User logs out
- Delete session from
data/sessions/ - User logs in again
- System re-registers user with fresh tokens
4. "Username Already Registered" Error
Symptom: Registration fails even though user hasn't registered before.
Cause: WebID already mapped to a username in data/users/webid-mappings.json.
Check Mapping:
Get-Content data/users/webid-mappings.json | ConvertFrom-JsonFix - Reset Registration:
# Backup first
Copy-Item data/users/webid-mappings.json data/users/webid-mappings.json.bak
# Edit JSON to remove the WebID entry
notepad data/users/webid-mappings.json
# Or completely reset (CAUTION: deletes all user mappings)
Remove-Item data/users/webid-mappings.json5. CORS Errors with Solid Provider
Symptom: Login fails with CORS error in browser console.
Cause: Some Solid providers have strict CORS policies.
Workaround:
- Use
solidcommunity.net(most permissive) - Ensure
BASE_URLmatches your deployment domain exactly - Check
nuxt.config.tsβvite.server.allowedHostsincludes your domain
6. Federation Not Working (Activities Not Delivered)
Checklist:
-
BASE_URLuses HTTPS (or federating with localhost servers) - Actor profile is publicly accessible (test with
curl) - Private key exists in Pod (
/settings/keys.json) - HTTP Signature includes all required headers
- Recipient's inbox is correct (check remote actor profile)
- Remote server isn't blocking your domain
Test Federation Manually:
# Fetch your actor profile
curl https://your-domain.com/api/actors/yourname `
-H "Accept: application/ld+json"
# Should return JSON with "publicKey" field7. Timeline Shows No Posts
Causes:
- No users registered: Timeline aggregates posts from all registered users
- Pod access failed: Server can't read outbox containers
- No posts created: Users haven't posted yet
Quick Fix - Verify Timeline Endpoint:
curl http://localhost:3000/api/timelineExpected response: Array of groupedByEvent objects with posts.
8. TypeScript Errors After Update
Symptom: IDE shows red squiggles, build fails with type errors.
Fix:
# 1. Clear Nuxt cache
Remove-Item -Recurse -Force .nuxt
# 2. Clear node_modules cache
Remove-Item -Recurse -Force node_modules/.cache
# 3. Reinstall (if needed)
Remove-Item -Recurse -Force node_modules
pnpm install
# 4. Rebuild Nuxt
pnpm dev9. Webmentions Not Appearing
Causes:
- Source page doesn't link to target: Webmention validation fails
- Microformat parsing error: No h-entry found on source page
- Storage path incorrect: Post not found in
data/posts/
Debug:
# Test webmention endpoint
curl -X POST http://localhost:3000/api/webmentions `
-H "Content-Type: application/x-www-form-urlencoded" `
-d "source=https://example.com/post&target=http://localhost:3000/post/123"
# Check logs
Get-Content logs/activitypub.log -Tail 2010. OpenAI Posts Not Generating
Symptom: New users don't get sample posts during registration.
Cause: OPENAI_API_KEY not set or invalid.
Check:
$env:OPENAI_API_KEYFix:
- Get API key from OpenAI Platform
- Add to
.env:OPENAI_API_KEY=sk-... - Restart server
Fallback: If no API key, posts use generic content (see server/utils/postGenerator.ts)
Testing & Verification
Manual Testing Checklist
Authentication
- Login with Solid Pod redirects to provider
- OAuth callback returns to app successfully
- Session persists across page reloads
- Logout clears session
Registration
- Username validation works (lowercase, no spaces)
- Pod containers created during registration
- ActivityPub profile saved to Pod
- Sample posts generated (if OpenAI configured)
Solid Pod Integration
- Pod containers created during registration
- Activities saved as JSON files in Pod
- ACLs set correctly (inbox is append-only, outbox is readable)
- Private key stored in
/settings/keys.json
ActivityPub Federation
- Actor profile accessible at
/api/actors/:username - WebFinger works for
@username@domain - Outbox serves paginated activities
- HTTP Signatures valid on sent activities
- Remote activities received in inbox
Webmentions
- Endpoint advertised in
<link rel="webmention"> - Receives webmentions via POST
- Validates source links to target
- Parses microformats2 correctly
- Displays on post pages
UI/UX
- Timeline loads and groups by events
- Posts display with actor info
- Like button works (optimistic update)
- Reply modal opens and submits
- Dark mode toggle works
- Responsive on mobile
Automated Testing
Unit Tests (example, not currently implemented):
// tests/unit/crypto.test.ts
import { generateActorKeyPair, signRequest } from '@/server/utils/crypto'
describe('Crypto Utils', () => {
test('generates valid RSA key pair', () => {
const { publicKey, privateKey } = generateActorKeyPair()
expect(publicKey).toContain('BEGIN PUBLIC KEY')
expect(privateKey).toContain('BEGIN PRIVATE KEY')
})
test('signs request correctly', () => {
const { privateKey } = generateActorKeyPair()
const headers = signRequest({
privateKey,
keyId: 'https://example.com/actor#key',
url: 'https://remote.com/inbox',
method: 'POST',
body: '{"type":"Follow"}'
})
expect(headers.Signature).toBeDefined()
expect(headers.Digest).toContain('SHA-256=')
})
})Integration Tests (example):
// tests/integration/activitypub.test.ts
describe('ActivityPub Endpoints', () => {
test('GET /api/actors/:username returns Actor object', async () => {
const response = await fetch('http://localhost:3000/api/actors/testuser')
const actor = await response.json()
expect(actor.type).toBe('Person')
expect(actor.inbox).toBeDefined()
})
test('POST to inbox with valid signature succeeds', async () => {
// ... implementation
})
})Deployment Considerations
Production Checklist
- HTTPS Required: ActivityPub federation requires HTTPS (Mastodon won't federate with HTTP)
- BASE_URL: Set to your production domain (e.g.,
https://odyfeed.example.com) - Sessions: Persist sessions to database (currently filesystem-based)
- Rate Limiting: Add rate limiting to API endpoints
- Monitoring: Set up logging aggregation (e.g., LogDNA, Papertrail)
- Backups: Regular backups of
data/directory - Scaling: Consider Redis for session storage if scaling horizontally
Environment-Specific Config
Development:
BASE_URL=http://localhost:3000
OPENAI_API_KEY=sk-dev-keyProduction:
BASE_URL=https://odyfeed.example.com
OPENAI_API_KEY=sk-prod-key
ACTIVITYPUB_PAGE_SIZE=50Hosting Recommendations
- Vercel: Works well for Nuxt apps (set
ssr: false) - Netlify: Similar to Vercel
- Self-Hosted VPS: Full control, install Node.js + Nginx
- Docker: Containerize for consistent deployments
Docker Example (not included, but recommended):
FROM node:18-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
CMD ["node", ".output/server/index.mjs"]Contributing
Contributions are welcome! This project is primarily educational, but improvements are encouraged.
How to Contribute
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Commit your changes:
git commit -m 'Add amazing feature' - Push to the branch:
git push origin feature/amazing-feature - Open a Pull Request
Code Style
- Follow the coding instructions in
global-copilot-instructions - Use TypeScript for all new code
- Write self-documenting code (avoid unnecessary comments)
- Use function expressions:
const myFunc = function () {} - Test locally before submitting PR
Areas for Improvement
- Add automated tests (unit + integration)
- Implement database for sessions (replace filesystem)
- Add rate limiting middleware
- Improve error handling and user feedback
- Add admin dashboard for managing users
- Support for more ActivityPub activities (Block, Remove, Update)
- Better mobile responsiveness
- Accessibility improvements (ARIA labels, keyboard navigation)
- Performance optimizations (lazy loading, code splitting)
- Add more mythological actors and events
Resources & Further Reading
Official Specifications
- ActivityPub W3C Recommendation: The official specification
- ActivityStreams 2.0: Vocabulary for social data
- Solid Protocol: Specification for decentralized data storage
- HTTP Signatures (draft-cavage-http-signatures-12): Authentication for HTTP requests
- Webmention W3C Recommendation: Decentralized notifications
Implementation Guides
- Mastodon ActivityPub Guide: Practical guide with Mastodon-specific extensions
- Solid Developer Resources: Getting started with Solid development
- Inrupt JavaScript Client Libraries: Official Solid client library docs
- ActivityPub Rocks!: Test suite and validator
Community & Tools
- Fediverse Developer Network: Resources for building federated apps
- SocialHub: ActivityPub community forum
- Solid Forum: Community support for Solid development
- WebMention.io: Hosted webmention service (alternative approach)
Interesting Projects
- Mastodon: Ruby-based fediverse platform
- Pleroma: Lightweight fediverse server
- PeerTube: Federated video platform (ActivityPub)
- Pixelfed: Federated photo sharing (ActivityPub)
- Inrupt PodSpaces: Commercial Solid Pod provider
Learning Resources
- How to implement ActivityPub in your project: Mastodon blog post
- Understanding Solid Pods: FAQ and beginner guides
- RDF Primer: Introduction to RDF concepts
- Microformats2 Spec: Parsing semantic HTML
License
This project is open source and available under the MIT License.
Acknowledgments
- Homer: For the original Odyssey (circa 8th century BCE)
- Tim Berners-Lee: For inventing the Web and championing Solid
- W3C Social Web Working Group: For ActivityPub and related standards
- Inrupt: For Solid client libraries and developer tools
- Mastodon Community: For federation best practices and interoperability
- OpenAI: For GPT models used in post generation
Contact & Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Author: Your Name (@yourhandle@mastodon.social)
Built with β€οΈ and a passion for the decentralized web
"Tell me, Muse, of the man of many ways, who was driven far journeys..." β Homer, The Odyssey