ActivityPub
A multi-tenant ActivityPub server for Ghost, built with Fedify. This service makes it possible for independent websites to publish their content directly to the Fediverse, enabling networked publishing to the open social web.
Running locally for development
Ghost's host-based pnpm dev proxies /.ghost/activitypub/*, /.well-known/webfinger, and /.well-known/nodeinfo to this service (default target 127.0.0.1:8080). This setup has only been tested on macOS using Docker for Mac and OrbStack.
Setup
- Install Ghost using
Install from sourceinstructions - Start the ActivityPub Service
pnpm dev && pnpm logs in the root directory of this project
- Use tailscale funnel --bg 2368 or ngrok http 2368 to expose Ghost's Admin development server (port 2368 in the main checkout, or GHOST_DEV_PORT from .ghost-dev.env in a worktree — not port 80)
- Start Ghost with the public URL
ACTIVITYPUB_PROXY_TARGET=127.0.0.1:8080 url=https://<your-tailnet-or-ngrok-url>/ pnpm dev
- pnpm dev sets url itself, so a url in ghost/config.local.json has no effect
- Recent Ghost checkouts default ACTIVITYPUB_PROXY_TARGET to 127.0.0.1:8080; set it explicitly on older ones
- If you were already running Ghost locally, make sure to restart it with the url= override
🏗️ Architecture & Development Guidelines
For AI assistants: See AGENTS.md for comprehensive guidance with code examples.
For developers: See Architecture Decision Records in /adr for detailed rationale.
Core Architecture Patterns
This service follows Domain-Driven Design with specific patterns:
- Immutable Entities with Events (ADR-0003) - Entities return new instances with domain events
- Result Type Pattern (ADR-0004) - Use Result
for explicit error handling - Error Objects in Results (ADR-0005) - Enhanced Result types with contextual error objects
- Class-Based Architecture (ADR-0006) - All components use classes with dependency injection
- Repository Pattern (ADR-0007) - Services orchestrate logic, repositories handle data access
- View Pattern for Reads (ADR-0008) - Optimized read queries separate from write path
- Hash-Based Lookups (ADR-0009) ⚠️ - ActivityPub IDs use SHA256 hashes
- Decorator Routing (ADR-0010) - Routes defined via decorators
Project Structure
src/
├── account/ # Immutable entities
├── post/ # Being migrated to immutable
├── activity-handlers/ # Class-based handlers
├── http/api/ # REST controllers
├── core/ # Shared utilities
└── dispatchers.ts # Legacy - don't add here
⚠️ Critical Gotchas
1. Database lookups MUST use SHA256 hashes (ADR-0009)
- Never use
where('ap_id', apId)- it returns empty results silently! - Always use
whereRaw('ap_id_hash = UNHEX(SHA2(?, 256))', [apId]) - Applies to:
ap_id,domain(with LOWER),ap_inbox_url(with LOWER)
- Use
isError(result),getValue(result),getError(result) - Never destructure directly like
[error, value]
- Views can query DB directly (read optimization)
- Services MUST go through repositories (write path)
- Parameter
accountService→ registered as'accountService' - Parameter
db→ registered as'db'
- Adding to
dispatchers.ts→ create new handler classes - Using
AccountType→ useAccountentity - Direct DB queries in services → use repositories
- String comparisons for AP IDs → use hash lookups
Code formatting + linting
We use Biome for code formatting and linting.
If you use VS Code, you can install the Biome extension to get inline feedback.
To enable auto-formatting on save, you'll need to set the default formatter to Biome and enable Format on Save in your VS Code settings.
Running Tests
Tests run within a Docker Compose stack. Common commands:
pnpm test— run everything (types, unit, integration, and e2e); slowpnpm test:unit— unit tests only; fastpnpm test:integration— integration tests onlypnpm test:cucumber— end-to-end (Cucumber) testspnpm test:single 'path/to/test'— run a single unit or integration testpnpm test:types— type-check withtsc
Migrations
Run pnpm migrate to apply pending up migrations against your dev db (this also happens automatically on pnpm dev). For the testing db, use docker compose run migrate-testing up.
To run other migrate commands, drop into a shell with docker compose exec -it migrate /bin/sh (or migrate-testing). The migrate binary is available there, along with a MYSQL_DB environment variable correctly formatted for the -database argument.
Copyright & license
Copyright (c) 2013-2026 Ghost Foundation - Released under the MIT license. Ghost and the Ghost Logo are trademarks of Ghost Foundation Ltd. Please see our trademark policy for info on acceptable usage.