# Multithreaded Architecture ## 1. System Overview Multithreaded (MT) is a forum platform for MNW creators. Each MNW project gets a community forum where creators and their audiences can discuss items, post devlogs, and organize conversations by category. MT is a standalone web service that delegates authentication to MNW and receives commands from MNW via an internal API. MT serves HTML pages directly (server-side rendered via Askama templates and enhanced with HTMX). There is no SPA, no JavaScript framework, and no client-side routing. Users interact with MT through their browser; the server owns all rendering and state. ### Position in the MNW ecosystem ``` MNW Server (makenot.work) | |-- OAuth provider (user accounts, tokens) |-- Internal API caller (community creation, cross-posted threads) | v Multithreaded (forums.makenot.work) | |-- PostgreSQL (forum data, sessions, search indexes) |-- S3 (image uploads, optional) ``` MNW is the source of truth for user identity. MT mirrors user data locally via ON CONFLICT upserts on every login and internal API call. Communities in MT map 1:1 to projects in MNW, created either when a user first visits or when MNW calls the internal API to provision one. ## 2. Crate Structure MT is a Cargo workspace with three crates. The boundary rule is strict: library crates contain no web framework types. ``` multithreaded/ (workspace root) Cargo.toml # Workspace definition + root crate deps src/ # Root crate (binary) crates/ mt-core/ # Domain types, zero internal deps mt-db/ # Database queries/mutations, depends on mt-core ``` ### Root crate (multithreaded) The binary. Owns the Axum server, route handlers, templates, middleware (CSRF, sessions, rate limiting), OAuth client, S3 integration, link preview fetching, and the internal HMAC-authenticated API. Depends on both mt-core and mt-db, plus shared crates from `MNW/shared/` (docengine for Markdown rendering, tagtree for tag validation, s3-storage for object storage). ### mt-core Leaf crate with no internal dependencies. Defines domain enums used across the codebase: - `CommunityRole` (Owner, Moderator, Member) with permission helpers - `BanType` (Ban, Mute) - `ModAction` (19 variants covering all auditable actions) - `SortColumn` / `SortOrder` for thread listing queries - `time_format` module for relative timestamps ("3 hours ago") ### mt-db Database access layer. Depends only on mt-core, sqlx, chrono, and uuid. Split into two modules: - `queries.rs` -- read-only functions returning `sqlx::FromRow` projection structs shaped for templates - `mutations.rs` -- write functions (insert, update, upsert, soft delete) All SQL uses positional parameters (`$1`, `$2`). No ORM, no query builder. Projection structs are purpose-built for each query, not generic domain models. ## 3. Data Flows ### Post creation 1. User submits a form (POST to `/p/{slug}/{category}/new` or `/{thread_id}/reply`). 2. Rate limiter checks per-IP write budget (burst 10, then 2/sec). 3. CSRF middleware validates the synchronizer token. 4. Handler extracts `SessionUser` from the session, verifies community membership, checks ban/mute status and thread lock state. 5. Markdown body is rendered to HTML via `docengine::render_strict()` with @mention resolution. 6. `mt_db::mutations::create_post()` inserts the post and updates the thread's `last_activity_at`. 7. Link preview extraction runs in the background: URLs are parsed from the Markdown via pulldown-cmark, fetched with an SSRF-safe HTTP client, and OG metadata is stored. 8. Redirect back to the thread with a toast message. ### Moderation flow Content moderation operates at three levels: **User flagging.** Any authenticated user can flag a post with a reason (spam, rule_breaking, off_topic) and optional detail. Flags are stored in `post_flags` and visible on the moderation dashboard. **Auto-hide.** Each community has a configurable `auto_hide_threshold` (nullable). When a flag is inserted and the threshold is set, `auto_hide_if_threshold_met()` atomically counts distinct flaggers on that post and sets `removed_at` + `removed_by` if the count meets or exceeds the threshold. This is logged as `AutoHidePost` in the mod log. **Mod-remove.** Moderators and owners can directly remove posts via `/posts/{post_id}/remove` or through the flag queue. Removing via the flag queue also resolves all pending flags on that post. Both paths log the action to the mod log. **Bans and mutes.** Moderators can ban (full access revocation) or mute (write-only restriction) users within their community, with optional duration and reason. Role hierarchy is enforced: mods cannot ban other mods, only owners can. Expired bans are cleaned up opportunistically when the moderation page loads. All moderation actions are recorded in the `mod_log` table with actor, action type, target user/post, and optional reason. The mod log is paginated and visible to moderators. ### Thread tracking Users can track threads to monitor new activity. The tracked threads page shows unread counts (posts since last visit) and @mention indicators. Tracking is opt-in per thread and can be bulk-cleared. ## 4. Authentication MT has no user database of its own in the traditional sense. All authentication flows through MNW's OAuth 2.0 server with PKCE (Proof Key for Code Exchange). ### OAuth flow 1. `/auth/login` generates a PKCE verifier (32 random bytes, base64url-encoded) and challenge (SHA-256 of verifier), stores the verifier in the session, and redirects to `MNW_BASE_URL/oauth/authorize`. 2. MNW authenticates the user and redirects back to `/auth/callback` with an authorization code and state nonce. 3. Callback validates the state nonce, exchanges the code for an access token (POST to `/oauth/token` with the PKCE verifier), and fetches `/oauth/userinfo` with the token. 4. The user is upserted locally (`ON CONFLICT (mnw_account_id) DO UPDATE`), suspension status is checked, and a session is created with `user_id`, `username`, and `display_name`. 5. Session ID is cycled after login to prevent session fixation. Token exchange and userinfo fetch both retry up to 2 times on 5xx or network errors with exponential backoff (500ms, 1000ms). ### Extractors - `MaybeUser(Option)` -- infallible, used on all routes. Returns `None` for anonymous users. - `PlatformAdmin(SessionUser)` -- returns 404 (not 403) to non-admins, hiding admin routes entirely. ### Internal API authentication MNW-to-MT requests (community creation, thread cross-posting) bypass OAuth and use HMAC-SHA256: - `X-Internal-Timestamp` -- Unix timestamp, rejected if >60 seconds from server time - `X-Internal-Signature` -- HMAC-SHA256 of `"timestamp\nbody"` using a shared secret The `InternalAuth` extractor validates both before passing the request body to the handler. Constant-time comparison prevents timing attacks on the signature. ## 5. Session Storage Sessions are stored in PostgreSQL via `tower-sessions-sqlx-store`. There is no Redis. Key details: - Cookie name: `mt_session` - SameSite: Lax - Expiry: 7 days of inactivity - Expired sessions are cleaned up hourly by a background task (`continuously_delete_expired`) - Session data stored: `user_id` (UUID), `username`, `display_name`, plus transient OAuth state (PKCE verifier, state nonce) during login ## 6. Rate Limiting Write endpoints (all POST routes) are rate-limited per IP using `tower_governor`: - Burst: 10 requests - Sustained: 2 requests/second (one token per 500ms) - Key extractor: `SmartIpKeyExtractor` (handles X-Forwarded-For behind reverse proxy) Rate limiting is applied as a route layer on the write routes group only. Read routes have no rate limit. The internal API is also exempt (it uses HMAC auth, not sessions). ## 7. Key Design Decisions ### HTMX-based SSR, no SPA MT serves complete HTML pages with HTMX for progressive enhancement (search results as swapped fragments). This eliminates client-side state management, reduces JavaScript to near zero, and makes the forum functional without JS. The search endpoint (`/search`) returns HTML fragments for HTMX swap, not JSON. ### Community-scoped permissions All permissions (roles, bans, mutes) are scoped to a single community. A user can be an owner in one community, a banned user in another, and a regular member in a third. There is no global moderator role -- only the platform admin (a single user ID set via env var) has cross-community authority. ### Immutable post bodies with footnotes and endorsements Post bodies cannot be edited after creation (enforced since migration 011). Authors can append footnotes (corrections, clarifications) and other users can endorse posts. This preserves conversation integrity while allowing authors to add context. ### Soft delete everywhere Threads and posts use `deleted_at` timestamps rather than hard deletes. Moderator removals use a separate `removed_at` / `removed_by` pair to distinguish author deletions from mod actions. This supports audit trails and potential appeals. ### Internal API for cross-service coordination Rather than sharing a database between MNW and MT, the two services communicate via a signed internal API. MNW can create communities, post threads (e.g., release notes), and query thread stats. This keeps the services independently deployable and the databases isolated. ### Security headers Every response includes: Content-Security-Policy (default-src 'self', no frame-ancestors), X-Content-Type-Options (nosniff), X-Frame-Options (DENY), and Cache-Control (private, no-cache by default). CSP is strict -- no inline scripts, no external resources. ## 8. Scaling Considerations ### Thread listing Thread lists are sorted by `last_activity_at` (or reply count), with pinned threads first. The `last_activity_at` column is denormalized on the threads table and updated on every new post, avoiding a JOIN/subquery on every listing page load. Pagination uses LIMIT/OFFSET with 25 threads per page. ### Search indexing Search uses a two-layer approach: - **Full-text search**: PostgreSQL `tsvector` columns (generated, stored) on `threads.title` and `posts.body_markdown`, with GIN indexes. Queries use `websearch_to_tsquery` for natural language input. - **Fuzzy matching**: `pg_trgm` extension with GIN trigram indexes on thread titles and post bodies. Combined with full-text ranking (`ts_rank * 2.0 + similarity`) to blend exact and fuzzy results. Search queries union thread title matches with post body matches (deduplicated), ordered by combined rank, capped at 20 results. Community-scoped search is supported via an optional `scope` parameter. ### Image uploads Images are stored in S3 (not in PostgreSQL), with metadata tracked in the database. Uploads are validated for type (png, jpg, gif, webp), size (5 MB max), and extension/content-type consistency. JPEG EXIF metadata is stripped server-side before upload. Image keys use the format `mt/{community_slug}/{uuid}.{ext}`. ### Connection pooling MT uses sqlx's built-in connection pool (`PgPool`). Sessions, forum data, and search all share the same pool. The session store has its own cleanup task but no separate connection pool.