Clone
3
Architecture
Gabriel Kaszewski edited this page 2026-05-15 15:16:41 +00:00

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

  1. Use cases publish DomainEvents via EventPublisher (port)
  2. adapters/nats implements EventPublisher → writes to NATS JetStream
  3. thoughts-worker consumes events via EventConsumer (pull consumer, 1-hour TTL caching)
  4. 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 discovery
  • GET /.well-known/nodeinfo + GET /nodeinfo/2.1 — NodeInfo
  • GET /users/{username}/outbox — paginated outbox
  • GET /users/{username}/followers / /following — AP collections
  • POST /users/{username}/inbox — shared inbox