# Multithreaded Architecture Forum-first community software integrated with Makenot.work. Users authenticate via MNW OAuth (PKCE flow). Each forum community maps to an MNW project. ## High-Level Overview ``` ┌──────────────────────────────────────────────┐ │ Browser (Askama HTML + HTMX) │ └──────────────────┬───────────────────────────┘ │ HTTP ┌──────────────────▼───────────────────────────┐ │ Axum HTTP Server │ │ │ │ ┌─────────┐ ┌────────┐ ┌──────┐ ┌────────┐ │ │ │ routes │ │ auth │ │ csrf │ │ seed │ │ │ └────┬────┘ └────┬───┘ └──────┘ └────────┘ │ │ │ │ │ │ ┌────▼───────────▼──────────────────────┐ │ │ │ mt-db (PostgreSQL queries/mutations) │ │ │ │ └── mt-core (domain models) │ │ │ └───────────────────────────────────────┘ │ └───────────────────────────────────────────────┘ │ │ ┌────────▼─────────┐ ┌─────────▼────────────┐ │ PostgreSQL │ │ MNW API │ │ (forum data) │ │ (OAuth, userinfo, │ │ │ │ project directory) │ └──────────────────┘ └──────────────────────┘ ``` ## Workspace Structure ``` multithreaded/ ├── Cargo.toml # Workspace (v0.1.1, Rust 2024) ├── src/ │ ├── main.rs # Entry point, server setup, migrations │ ├── lib.rs # Module exports, AppState │ ├── routes.rs # 30+ route handlers │ ├── auth.rs # MNW OAuth callback, userinfo fetch │ ├── csrf.rs # Token generation, constant-time comparison │ ├── config.rs # MNW_BASE_URL, OAUTH_CLIENT_ID from env │ ├── markdown.rs # pulldown-cmark with HTML stripping │ ├── seed.rs # Idempotent demo data seeding │ └── templates/ # Askama view models ├── crates/ │ ├── mt-core/ # Domain models, error types │ └── mt-db/ # PostgreSQL queries and mutations ├── templates/ # HTML templates (Askama) ├── static/ # CSS, fonts, htmx.min.js ├── migrations/ # 10 PostgreSQL migrations ├── tests/ # Integration tests └── deploy/ # deploy.sh, systemd unit, env template ``` ## Crate Dependencies ``` multithreaded (src/) ├── mt-db │ └── mt-core └── mt-core (for models in templates/routes) ``` ## Domain Models (`mt-core`) | Model | Fields | |-------|--------| | `User` | mnw_account_id, username, display_name, avatar_url | | `Community` | id, name, slug, description, created_at | | `Category` | id, community_id, name, slug, description, sort_order | | `Thread` | id, category_id, author_id, title, pinned, locked, timestamps | | `Post` | id, thread_id, author_id, body, edited_at, deleted_at | | `Membership` | user_id, community_id, role (owner/moderator/member) | | `CommunityBan` | user_id, community_id, reason, expires_at, is_mute | | `ModLogEntry` | community_id, actor_id, action, target, details, timestamp | ## Database (PostgreSQL) 10 migrations: | Migration | Purpose | |-----------|---------| | 001 | Users table (MNW account references) | | 002 | Communities | | 003 | Categories (with sort_order) | | 004 | Threads (title, pinned, locked, last_activity_at) | | 005 | Posts (body, soft delete support) | | 006 | Memberships (role enum: owner, moderator, member) | | 007 | Soft delete columns on threads and posts | | 008 | Community bans (with expiry, mute flag) | | 009 | Mod log (action audit trail) | | 010 | Platform-level suspensions (communities + users) | ## Routes ### Forum (public) | Method | Path | Purpose | |--------|------|---------| | GET | `/` | Forum directory (fetches projects from MNW API) | | GET | `/p/{slug}` | Project forum (categories + recent threads) | | GET | `/p/{slug}/members` | Community member list with roles | | GET | `/p/{slug}/{category}` | Category (threads paginated, 25/page) | | GET | `/p/{slug}/{category}/new` | Create thread form | | POST | `/p/{slug}/{category}/new` | Create thread | | GET | `/p/{slug}/{category}/{thread_id}` | Thread (posts paginated, 50/page) | | POST | `/p/{slug}/{category}/{thread_id}/reply` | Reply to thread | ### Thread/Post Management (authenticated) | Method | Path | Purpose | |--------|------|---------| | GET/POST | `/p/{slug}/{category}/{thread_id}/edit` | Edit thread | | POST | `/p/{slug}/{category}/{thread_id}/delete` | Soft delete thread | | POST | `/p/{slug}/{category}/{thread_id}/pin` | Pin/unpin thread | | POST | `/p/{slug}/{category}/{thread_id}/lock` | Lock/unlock thread | | GET/POST | `.../posts/{post_id}/edit` | Edit post (15-min window) | | POST | `.../posts/{post_id}/delete` | Soft delete post | ### Community Settings (owner only) | Method | Path | Purpose | |--------|------|---------| | GET/POST | `/p/{slug}/settings` | Community name/description | | POST | `/p/{slug}/settings/categories/new` | Create category | | GET/POST | `/p/{slug}/settings/categories/{id}/edit` | Edit category | | POST | `/p/{slug}/settings/categories/{id}/move` | Reorder category | ### Moderation (moderator+) | Method | Path | Purpose | |--------|------|---------| | GET | `/p/{slug}/moderation` | Moderation dashboard | | POST | `/p/{slug}/moderation/ban` | Ban user (with expiry) | | POST | `/p/{slug}/moderation/unban` | Unban user | | POST | `/p/{slug}/moderation/mute` | Mute user (write-only restriction) | | POST | `/p/{slug}/moderation/unmute` | Unmute user | | GET | `/p/{slug}/moderation/log` | Mod log (paginated) | ### Platform Admin (PLATFORM_ADMIN_ID only) | Method | Path | Purpose | |--------|------|---------| | GET | `/_admin` | Platform admin dashboard | | POST | `/_admin/communities/{id}/suspend` | Suspend community | | POST | `/_admin/communities/{id}/unsuspend` | Unsuspend community | | POST | `/_admin/users/{id}/suspend` | Suspend user | | POST | `/_admin/users/{id}/unsuspend` | Unsuspend user | ### Auth & System | Method | Path | Purpose | |--------|------|---------| | GET | `/auth/login` | Initiate MNW OAuth (PKCE) | | GET | `/auth/callback` | OAuth callback (exchange code, set session) | | GET | `/auth/logout` | Clear session | | GET | `/api/health` | Health check (monitored by PoM) | ## Security - **CSRF**: SHA256 tokens on all POST/PUT/DELETE, constant-time comparison, middleware layer - **Sessions**: tower-sessions with PostgresStore, 7-day expiry, SameSite::Lax - **XSS**: Markdown rendered via pulldown-cmark with HTML tags stripped - **Auth**: MNW OAuth PKCE — no passwords stored locally - **Moderation**: Role hierarchy enforced on all read/write handlers - **Suspensions**: Platform-level suspension checks on all handlers ## Deployment Deployed to two targets. Both run on port 3400, use systemd, and share the same service unit (`deploy/multithreaded.service`). Public domain: `forums.makenot.work` (Cloudflare-proxied, points to hetzner). ### Hetzner (production) - **Host**: `alpha-west-1` (Tailscale `100.120.174.96`, public `5.78.144.244`) - **SSH**: `root@100.120.174.96` (via Tailscale) - **Install path**: `/opt/multithreaded/` - **Build**: cross-compiled on macOS via `cargo zigbuild --release --target x86_64-unknown-linux-gnu` - **Deploy script**: `deploy/deploy-hetzner.sh` (build, upload binary + static + migrations, restart) - `--setup` -- first-time: create system user, dirs, database, build, install, seed - `--config` -- upload systemd unit, static assets, migrations only - **Env file**: `deploy/env.hetzner` -> `/opt/multithreaded/.env` - **Reverse proxy**: Caddy (TLS termination via Cloudflare Origin CA, `forums.makenot.work`) - **Bind address**: `127.0.0.1:3400` (Caddy fronts it; no direct external access) - **OAuth**: `client_id=mt-forums-6378957b452bbbc906c3db8edd072d64`, redirect to `https://forums.makenot.work/auth/callback`, `MNW_BASE_URL=https://makenot.work` ### Astra (staging/dev) - **Host**: `astra` (Tailscale `100.106.221.39`) - **SSH**: `max@100.106.221.39` (via Tailscale) - **Install path**: `/opt/multithreaded/` - **Build**: native build on astra (aarch64). Source rsynced to `~/src/multithreaded/`, built with `cargo build --release`, binary copied to `/opt/multithreaded/`. - **Deploy script**: `deploy/deploy.sh` (rsync source, build remote, deploy files, restart) - `--setup` -- first-time: create system user, dirs, database, build, install, seed - **Env file**: `deploy/env.production` -> `/opt/multithreaded/.env` - **No reverse proxy**: direct access on port 3400 via Tailscale IP - **Bind address**: `0.0.0.0:3400` - **OAuth**: `MNW_BASE_URL=http://127.0.0.1:3000` (local MNW instance), redirect to `http://100.106.221.39:3400/auth/callback` ### Shared Details - **systemd unit**: `deploy/multithreaded.service` - Runs as `multithreaded` system user - `EnvironmentFile=/opt/multithreaded/.env` - Security hardening: `NoNewPrivileges`, `ProtectSystem=strict`, `ProtectHome`, `PrivateTmp`, `MemoryMax=512M` - Depends on `postgresql.service` - **Migrations**: auto-applied on boot (`sqlx::migrate!()`) - **Seeding**: `./multithreaded --seed` (idempotent, run once after first deploy) - **reqwest TLS**: uses `rustls-tls` feature (not `native-tls`). Required for cross-compilation to x86_64 Linux from macOS -- native-tls depends on OpenSSL which complicates cross builds. - **Prerequisites for hetzner cross-compile**: `brew install zig`, `cargo install cargo-zigbuild`, `rustup target add x86_64-unknown-linux-gnu` ### Monitoring - PoM monitors `forums.makenot.work` -- health check on `/api/health`, TLS validation, route probes, DNS verification - PoM targets: MNW, MT (`forums.makenot.work`), htpy.app ### Deploy Files ``` deploy/ ├── deploy.sh # Astra deploy (rsync + native build) ├── deploy-hetzner.sh # Hetzner deploy (cross-compile + upload) ├── multithreaded.service # systemd unit (shared) ├── env.production # Env vars for astra └── env.hetzner # Env vars for hetzner ``` ## Testing 90 tests total: - 56 integration tests (CSRF, auth, CRUD, permissions, moderation, bans, admin) - 18 unit tests in mt-core (error types, helpers) - 16 unit tests in mt-db (query builders, model conversions)