Files
thoughts/README.md
Gabriel Kaszewski fe4960d30d docs: replace activitypub-base with k-ap in architecture overview
Reflects the migration from the local activitypub-base crate to the
external k-ap library, with an accurate description of what it provides.
2026-05-25 00:57:29 +02:00

239 lines
11 KiB
Markdown

# 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) with API key support for third-party clients
- 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
- Home feed, public feed, and per-user thought timelines
- Rate limiting and registration control
## Federation
Thoughts implements the [ActivityPub](https://www.w3.org/TR/activitypub/) protocol, making it compatible with Mastodon, Misskey, Pleroma, and other Fediverse software.
### Fediverse endpoints
| Endpoint | Description |
|---|---|
| `GET /.well-known/webfinger` | WebFinger discovery (`?resource=acct:user@host`) |
| `GET /.well-known/nodeinfo` | NodeInfo pointer |
| `GET /nodeinfo/2.0` | NodeInfo 2.0 — software metadata |
| `GET /users/{username}` | Actor profile (content-negotiated: JSON-LD or REST) |
| `GET /users/{username}/outbox` | Paginated outbox of `Note` activities |
| `POST /users/{username}/inbox` | Per-actor inbox |
| `POST /inbox` | Shared inbox for bulk delivery |
### Federation flow
1. A remote user follows `@you@yourinstance.com` → Mastodon sends a `Follow` activity to `/users/you/inbox`
2. Thoughts accepts and delivers an `Accept` back to the remote actor's inbox
3. When you post, Thoughts fans out a `Create(Note)` activity to all remote followers via the NATS worker
4. Remote posts from people you follow are fetched, cached, and shown in your home feed
### Without NATS
Federation still works without NATS — activities are processed in-process synchronously. The worker is required for async fan-out delivery to remote servers at scale. See [Environment Variables](#environment-variables).
### Instance moderation
- **Domain blocks** — block an entire instance; no activities are delivered to or accepted from blocked domains
- **Actor blocks** — block individual remote actors; a `Block` activity is delivered and they are filtered from all feeds
## 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
storage — object storage adapter (local filesystem + S3/MinIO) implementing the MediaStore port
postgres — PostgreSQL repositories for all domain entities
postgres-search — PostgreSQL trigram full-text search
postgres-federation — PostgreSQL-backed federation repository
k-ap (external) — generic AP protocol layer (ActivityPubService, actor management, inbox/outbox routing, follower tracking, WebFinger, NodeInfo, HTTP signatures)
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
```
The `domain` and `application` crates have zero concrete adapter dependencies. All I/O goes through `&dyn Port` traits, keeping business logic fully testable with in-memory fakes.
## Media Storage
Users can upload avatar and banner images via `PUT /users/me/avatar` and `PUT /users/me/banner` (multipart/form-data). Uploaded images are served at `GET /media/*path` (public, no auth required). Set `STORAGE_BACKEND` to configure the backend.
## Prerequisites
- Rust stable (1.80+)
- PostgreSQL 15+
- NATS with JetStream (optional — see [Without NATS](#without-nats))
## Environment Variables
Copy `.env.example` to `.env` and fill in your values.
### Required
| Variable | Description |
|---|---|
| `DATABASE_URL` | PostgreSQL connection string |
| `JWT_SECRET` | Secret used to sign JWT tokens — use a long random string in production |
| `BASE_URL` | Public URL of the API server — used for ActivityPub actor URLs and canonical links |
### Optional
| Variable | Default | Description |
|---|---|---|
| `HOST` | `0.0.0.0` | Interface to bind |
| `PORT` | `3000` | Port to listen on |
| `NATS_URL` | — | NATS connection string. If unset, a no-op publisher is used and events are not delivered to the worker |
| `CORS_ORIGINS` | `*` | Comma-separated allowed origins for CORS, e.g. `https://app.example.com` |
| `RATE_LIMIT` | disabled | Max requests per minute per IP |
| `ALLOW_REGISTRATION` | `true` | Set to `false` to close sign-ups |
| `RUST_ENV` | `development` | Set to `production` to disable ActivityPub debug logging |
| `RUST_LOG` | `info` | Log level filter (`error`, `warn`, `info`, `debug`, `trace`) |
| `STORAGE_BACKEND` | `local` | Storage backend: `local` or `s3` |
| `STORAGE_PATH` | — | Local filesystem path for media (required when `STORAGE_BACKEND=local`) |
| `STORAGE_PREFIX` | — | Optional key prefix for all stored objects |
| `S3_ENDPOINT` | — | S3/MinIO endpoint URL (required when `STORAGE_BACKEND=s3`) |
| `S3_ACCESS_KEY_ID` | — | S3 access key (required when `STORAGE_BACKEND=s3`) |
| `S3_SECRET_ACCESS_KEY` | — | S3 secret key (required when `STORAGE_BACKEND=s3`) |
| `S3_BUCKET` | — | S3 bucket name (required when `STORAGE_BACKEND=s3`) |
| `S3_REGION` | `us-east-1` | S3 region |
| `UPLOAD_MAX_BYTES` | `5242880` | Max upload size in bytes (default 5 MiB) |
| `UPLOAD_ALLOWED_TYPES` | `image/jpeg,image/png,image/gif,image/webp,image/avif` | Comma-separated allowed MIME types |
## Run
```bash
# 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
```bash
# 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 UI** — `http://localhost:8000/docs`
- **Scalar** — `http://localhost:8000/scalar`
## Frontend
The Next.js frontend lives in `thoughts-frontend/`. It requires two environment variables:
```env
NEXT_PUBLIC_API_URL=http://localhost:8000 # client-side requests
NEXT_PUBLIC_SERVER_SIDE_API_URL=http://localhost:8000 # SSR requests
```
```bash
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:
```bash
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 \
-e STORAGE_BACKEND=local \
-e STORAGE_PATH=/data/media \
-v media_vol:/data/media \
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
```
### Local development stack
`compose.yml` spins up the full stack: PostgreSQL, NATS (with JetStream and monitoring on port 8222), the API server, the event worker, and the frontend.
```bash
docker compose up
```
Services:
| Service | Port | Description |
|---|---|---|
| `postgres` | 5432 | PostgreSQL 16 |
| `nats` | 4222 / 8222 | NATS with JetStream; 8222 is the monitoring endpoint |
| `api` | 8000 | Thoughts API server |
| `worker` | — | Event worker (no exposed port) |
| `frontend` | 3000 | Next.js frontend |
## Contributing
Contributions are welcome. A few guidelines:
- **Run tests before opening a PR.** At minimum: `cargo test -p application` (no database needed). For adapter changes: `cargo test --workspace` with a live database.
- **Keep the hexagonal boundary.** `domain` and `application` must not import any adapter crate. Use `&dyn Port` traits for all I/O.
- **No ORM.** The project uses raw `sqlx`. Keep it that way.
- **ActivityPub changes** — test against a live Mastodon instance if possible, or use the AP debug logs (`RUST_ENV=development`).
- **Small, focused PRs** are easier to review than large ones.
For significant changes, open an issue first to discuss the approach.
## License
MIT License. See [LICENSE](LICENSE).