Gabriel Kaszewski 0592861edd
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 5m2s
test / unit (pull_request) Successful in 16m19s
test / integration (pull_request) Failing after 17m15s
refactor: 5 architectural improvements (Tasks 2-5 + Task 6 fix)
- feat(domain): Hashtag value object with canonical extract() — unifies two
  divergent private implementations; fields pre-compute raw/normalized/url_slug/ap_name

- feat(presentation): Deps<S: FromAppState> extractor — each handler now
  declares its exact dependency surface; AppState unchanged; handlers
  become unit-testable without mocking all 20 deps

- refactor(feed): replace 5 flat FeedRepository methods with FeedQuery/FeedScope
  — single query() method; SQL shared logic lives once; adding feed types
  no longer requires 5 edits

- refactor(activitypub): ActivityPubRepository + OutboundFederationPort moved
  out of domain::ports into activitypub-base::ap_ports — domain crate no
  longer knows about AP IDs, inboxes, or actor URLs

- fix(outbox): OutboxRelay now opens a per-row transaction so FOR UPDATE
  SKIP LOCKED actually holds the lock during publish + mark_delivered
2026-05-15 18:54:20 +02:00

Thoughts

A self-hosted microblogging server with full ActivityPub federation. Write short posts, follow people on Mastodon and other Fediverse servers, and receive their posts in your feed. Built in Rust with a Next.js frontend.

Features

  • Short-form posts (thoughts) with replies, boosts, and likes
  • Full ActivityPub federation — follow/unfollow remote actors, accept/reject followers, federated content broadcast as Note objects, paginated outbox, NodeInfo discovery, WebFinger, shared inbox, actor profile sync
  • Remote actor discovery — search by @user@instance handle, view full remote profiles (bio, banner, profile fields, posts, followers, following tabs), follow from within the UI
  • Worker-backed remote caches — remote posts and follower/following lists are fetched by the NATS worker and cached locally; profiles populate on first visit and refresh in the background
  • Content negotiation at GET /users/{username} — serves ActivityPub actor JSON or REST profile based on Accept header
  • Federation moderation — per-instance domain blocking, per-user actor blocking with Block activity delivery, delivery filter excludes blocked actors and blocked-domain inboxes
  • Async event fan-out via NATS JetStream — notifications and AP delivery run in a separate worker process; pull consumer with 1-hour TTL caching
  • JWT authentication (Bearer token)
  • OpenAPI documentation at /docs (Swagger UI) and /scalar (Scalar)
  • Full-text search over thoughts and users via PostgreSQL trigram indexes
  • Top friends — pin up to 5 users as highlighted contacts
  • API keys for third-party client access
  • Home feed, public feed, and per-user thought timelines

Architecture

Hexagonal (Ports & Adapters) with Domain-Driven Design:

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 the API process
bootstrap           — binary: thoughts (API server)
worker              — binary: thoughts-worker (event consumer — notifications, AP fan-out)
adapters/
  auth                 — JWT issuance and 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 ActivityPub 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

Prerequisites

  • Rust stable (1.80+)
  • PostgreSQL 15+
  • NATS (optional — federation and notifications still work without it, events queue in-process)

Environment Variables

Copy .env.example to .env and fill in your values:

DATABASE_URL=postgres://postgres:password@localhost:5432/thoughts
JWT_SECRET=change-me
BASE_URL=http://localhost:3000
NATS_URL=nats://localhost:4222   # optional

See .env.example for all available options.

Run

# API server (runs migrations automatically on startup)
cargo run -p bootstrap

# Event worker — federation fan-out and notifications (separate terminal)
cargo run -p worker

Both processes share the same PostgreSQL database. The worker is optional but required for ActivityPub delivery to remote servers.

Test

# Unit tests — no database required
cargo test -p application

# Full workspace (requires DATABASE_URL pointing to a running PostgreSQL)
cargo test --workspace

The application crate contains unit tests for all event services and use cases backed by in-memory fakes from domain's test-helpers feature. These are the fastest feedback loop for business logic.

API

All REST endpoints are under the root path. Authentication uses Authorization: Bearer <token> obtained from POST /auth/login.

Interactive API documentation is available at runtime:

  • Swagger UIhttp://localhost:8000/docs
  • Scalarhttp://localhost:8000/scalar

Frontend

The Next.js frontend lives in thoughts-frontend/. It requires two environment variables:

NEXT_PUBLIC_API_URL=http://localhost:8000        # client-side requests
NEXT_PUBLIC_SERVER_SIDE_API_URL=http://localhost:8000  # SSR requests
cd thoughts-frontend
bun install
bun run dev   # http://localhost:3000

Docker

The backend image contains both thoughts (API server) and thoughts-worker (event processor). Run them as separate containers:

docker build -t thoughts .

# API server
docker run -p 8000:8000 \
  -e DATABASE_URL=postgres://postgres:password@db:5432/thoughts \
  -e JWT_SECRET=change-me \
  -e BASE_URL=https://yourdomain.example.com \
  -e NATS_URL=nats://nats:4222 \
  thoughts

# Event worker (same image, different entrypoint)
docker run \
  -e DATABASE_URL=postgres://postgres:password@db:5432/thoughts \
  -e BASE_URL=https://yourdomain.example.com \
  -e NATS_URL=nats://nats:4222 \
  --entrypoint ./thoughts-worker \
  thoughts

# Frontend
docker build -t thoughts-frontend \
  --build-arg NEXT_PUBLIC_API_URL=https://api.yourdomain.example.com \
  --build-arg NEXT_PUBLIC_SERVER_SIDE_API_URL=http://thoughts:8000 \
  thoughts-frontend/
docker run -p 3000:3000 thoughts-frontend

See compose.yml for a full local development stack.

License

MIT License. See LICENSE.

Description
Nostalgic social platform with Frutiger Aero style. 128-char posts, custom CSS profiles, and full ActivityPub federation — interoperable with Mastodon, Misskey, and Movies Diary.
https://thoughts.gabrielkaszewski.dev/ Readme MIT 7.8 MiB
Languages
Rust 59.1%
TypeScript 38.9%
CSS 1.4%
Dockerfile 0.3%
Shell 0.1%
Other 0.1%