Architecture
Hexagonal (Ports & Adapters) with Domain-Driven Design. Two processes share one PostgreSQL database.
Processes
| Process | Binary | Role |
|---|---|---|
| API server | thoughts |
HTTP API + ActivityPub endpoints |
| Event worker | thoughts-worker |
Federation fan-out, notifications, remote cache refresh |
Both processes are built from the same Docker image; run with different entrypoints.
Crate Layout
crates/
├── domain — pure types and port trait definitions, no external deps
├── application — use cases and event processing services (business logic)
├── api-types — shared REST API request/response DTOs
├── presentation — Axum HTTP router, OpenAPI spec, composition root for API process
├── bootstrap — binary: thoughts (API server)
├── worker — binary: thoughts-worker (event consumer)
└── adapters/
├── auth — JWT issuance/validation, Argon2 password hashing
├── postgres — PostgreSQL repositories for all domain entities
├── postgres-search — PostgreSQL trigram full-text search
├── postgres-federation — PostgreSQL-backed federation repository
├── activitypub-base — core AP protocol types, ActivityPubService, federation middleware
├── activitypub — project-specific AP wiring (ThoughtsObjectHandler, inbox/outbox)
├── nats — NATS transport implementing Transport + MessageSource ports
├── event-payload — shared event serialization DTOs
└── event-transport — Transport trait + EventPublisherAdapter / MessageSource + EventConsumerAdapter
Traffic Flow
In production, Traefik is the entry point (shared external service). API and frontend run on separate subdomains — no internal proxy needed.
User's Browser
│
▼
Traefik (external, shared)
├── api.domain.com ──────► thoughts (API server :8000)
│ │
└── domain.com ──────────► Next.js Frontend (:3000)
│
PostgreSQL NATS JetStream
│
thoughts-worker
(AP delivery, notifications)
In development, services are exposed directly on localhost ports — no proxy needed.
Domain Layer
crates/domain defines only pure types and port traits — no framework or DB dependencies:
- Models:
User,Thought,Follow,Like,Boost,Block,Tag,Notification,RemoteActor,TopFriend,ApiKey,FeedEntry - Value objects:
UserId,ThoughtId,Username,Email,Content,PasswordHash, … - Port traits:
UserRepository,ThoughtRepository,LikeRepository,BoostRepository,FollowRepository,BlockRepository,FeedRepository,NotificationRepository,FederationActionPort,OutboundFederationPort,EventPublisher,AuthService, … - Domain events: published by use cases, consumed by the worker
Application Layer
crates/application contains use cases backed by port traits. No concrete adapters here — fully unit-testable with in-memory fakes from domain's test-helpers feature:
| Module | Responsibility |
|---|---|
auth |
Register, login |
thoughts |
Create, delete, get thread, edit |
social |
Follow/unfollow, like/unlike, boost/unboost, block |
feed |
Home feed, public feed, user feed, tag feed |
notifications |
List, count unread, mark read |
profile |
Update profile, top friends |
api_keys |
Create, list, revoke |
federation_management |
Pending followers, accept/reject, domain blocks |
Event System
- Use cases publish
DomainEvents viaEventPublisher(port) adapters/natsimplementsEventPublisher→ writes to NATS JetStreamthoughts-workerconsumes events viaEventConsumer(pull consumer, 1-hour TTL caching)- Worker handlers perform AP fan-out and write notifications back to PostgreSQL
NATS is optional — when not configured, events are queued in-process and the worker runs without it (federation and notifications still work, just synchronously).
Authentication
| Method | Header | Use case |
|---|---|---|
| JWT | Authorization: Bearer <token> |
Web client sessions (short-lived) |
| API Key | Authorization: ApiKey <key> |
Third-party apps (long-lived, user-generated) |
ActivityPub
GET /users/{username} performs content negotiation:
Accept: application/activity+json→ AP actor JSON- Otherwise → REST profile JSON
Federation endpoints:
GET /.well-known/webfinger— WebFinger discoveryGET /.well-known/nodeinfo+GET /nodeinfo/2.1— NodeInfoGET /users/{username}/outbox— paginated outboxGET /users/{username}/followers//following— AP collectionsPOST /users/{username}/inbox— shared inbox