| 1 |
# Multithreaded Architecture |
| 2 |
|
| 3 |
Forum-first community software integrated with Makenot.work. Users authenticate via MNW OAuth (PKCE flow). Each forum community maps to an MNW project. |
| 4 |
|
| 5 |
## High-Level Overview |
| 6 |
|
| 7 |
``` |
| 8 |
┌──────────────────────────────────────────────┐ |
| 9 |
│ Browser (Askama HTML + HTMX) │ |
| 10 |
└──────────────────┬───────────────────────────┘ |
| 11 |
│ HTTP |
| 12 |
┌──────────────────▼───────────────────────────┐ |
| 13 |
│ Axum HTTP Server │ |
| 14 |
│ │ |
| 15 |
│ ┌─────────┐ ┌────────┐ ┌──────┐ ┌────────┐ │ |
| 16 |
│ │ routes │ │ auth │ │ csrf │ │ seed │ │ |
| 17 |
│ └────┬────┘ └────┬───┘ └──────┘ └────────┘ │ |
| 18 |
│ │ │ │ |
| 19 |
│ ┌────▼───────────▼──────────────────────┐ │ |
| 20 |
│ │ mt-db (PostgreSQL queries/mutations) │ │ |
| 21 |
│ │ └── mt-core (domain models) │ │ |
| 22 |
│ └───────────────────────────────────────┘ │ |
| 23 |
└───────────────────────────────────────────────┘ |
| 24 |
│ │ |
| 25 |
┌────────▼─────────┐ ┌─────────▼────────────┐ |
| 26 |
│ PostgreSQL │ │ MNW API │ |
| 27 |
│ (forum data) │ │ (OAuth, userinfo, │ |
| 28 |
│ │ │ project directory) │ |
| 29 |
└──────────────────┘ └──────────────────────┘ |
| 30 |
``` |
| 31 |
|
| 32 |
## Workspace Structure |
| 33 |
|
| 34 |
``` |
| 35 |
multithreaded/ |
| 36 |
├── Cargo.toml # Workspace (v0.1.1, Rust 2024) |
| 37 |
├── src/ |
| 38 |
│ ├── main.rs # Entry point, server setup, migrations |
| 39 |
│ ├── lib.rs # Module exports, AppState |
| 40 |
│ ├── routes.rs # 30+ route handlers |
| 41 |
│ ├── auth.rs # MNW OAuth callback, userinfo fetch |
| 42 |
│ ├── csrf.rs # Token generation, constant-time comparison |
| 43 |
│ ├── config.rs # MNW_BASE_URL, OAUTH_CLIENT_ID from env |
| 44 |
│ ├── markdown.rs # pulldown-cmark with HTML stripping |
| 45 |
│ ├── seed.rs # Idempotent demo data seeding |
| 46 |
│ └── templates/ # Askama view models |
| 47 |
├── crates/ |
| 48 |
│ ├── mt-core/ # Domain models, error types |
| 49 |
│ └── mt-db/ # PostgreSQL queries and mutations |
| 50 |
├── templates/ # HTML templates (Askama) |
| 51 |
├── static/ # CSS, fonts, htmx.min.js |
| 52 |
├── migrations/ # 10 PostgreSQL migrations |
| 53 |
├── tests/ # Integration tests |
| 54 |
└── deploy/ # deploy.sh, systemd unit, env template |
| 55 |
``` |
| 56 |
|
| 57 |
## Crate Dependencies |
| 58 |
|
| 59 |
``` |
| 60 |
multithreaded (src/) |
| 61 |
├── mt-db |
| 62 |
│ └── mt-core |
| 63 |
└── mt-core (for models in templates/routes) |
| 64 |
``` |
| 65 |
|
| 66 |
## Domain Models (`mt-core`) |
| 67 |
|
| 68 |
|
| 69 |
|
| 70 |
| `User` | mnw_account_id, username, display_name, avatar_url | |
| 71 |
| `Community` | id, name, slug, description, created_at | |
| 72 |
| `Category` | id, community_id, name, slug, description, sort_order | |
| 73 |
| `Thread` | id, category_id, author_id, title, pinned, locked, timestamps | |
| 74 |
| `Post` | id, thread_id, author_id, body, edited_at, deleted_at | |
| 75 |
| `Membership` | user_id, community_id, role (owner/moderator/member) | |
| 76 |
| `CommunityBan` | user_id, community_id, reason, expires_at, is_mute | |
| 77 |
| `ModLogEntry` | community_id, actor_id, action, target, details, timestamp | |
| 78 |
|
| 79 |
## Database (PostgreSQL) |
| 80 |
|
| 81 |
10 migrations: |
| 82 |
|
| 83 |
|
| 84 |
|
| 85 |
| 001 | Users table (MNW account references) | |
| 86 |
| 002 | Communities | |
| 87 |
| 003 | Categories (with sort_order) | |
| 88 |
| 004 | Threads (title, pinned, locked, last_activity_at) | |
| 89 |
| 005 | Posts (body, soft delete support) | |
| 90 |
| 006 | Memberships (role enum: owner, moderator, member) | |
| 91 |
| 007 | Soft delete columns on threads and posts | |
| 92 |
| 008 | Community bans (with expiry, mute flag) | |
| 93 |
| 009 | Mod log (action audit trail) | |
| 94 |
| 010 | Platform-level suspensions (communities + users) | |
| 95 |
|
| 96 |
## Routes |
| 97 |
|
| 98 |
### Forum (public) |
| 99 |
|
| 100 |
|
| 101 |
| GET | `/` | Forum directory (fetches projects from MNW API) | |
| 102 |
| GET | `/p/{slug}` | Project forum (categories + recent threads) | |
| 103 |
| GET | `/p/{slug}/members` | Community member list with roles | |
| 104 |
| GET | `/p/{slug}/{category}` | Category (threads paginated, 25/page) | |
| 105 |
| GET | `/p/{slug}/{category}/new` | Create thread form | |
| 106 |
| POST | `/p/{slug}/{category}/new` | Create thread | |
| 107 |
| GET | `/p/{slug}/{category}/{thread_id}` | Thread (posts paginated, 50/page) | |
| 108 |
| POST | `/p/{slug}/{category}/{thread_id}/reply` | Reply to thread | |
| 109 |
|
| 110 |
### Thread/Post Management (authenticated) |
| 111 |
|
| 112 |
|
| 113 |
| GET/POST | `/p/{slug}/{category}/{thread_id}/edit` | Edit thread | |
| 114 |
| POST | `/p/{slug}/{category}/{thread_id}/delete` | Soft delete thread | |
| 115 |
| POST | `/p/{slug}/{category}/{thread_id}/pin` | Pin/unpin thread | |
| 116 |
| POST | `/p/{slug}/{category}/{thread_id}/lock` | Lock/unlock thread | |
| 117 |
| GET/POST | `.../posts/{post_id}/edit` | Edit post (15-min window) | |
| 118 |
| POST | `.../posts/{post_id}/delete` | Soft delete post | |
| 119 |
|
| 120 |
### Community Settings (owner only) |
| 121 |
|
| 122 |
|
| 123 |
| GET/POST | `/p/{slug}/settings` | Community name/description | |
| 124 |
| POST | `/p/{slug}/settings/categories/new` | Create category | |
| 125 |
| GET/POST | `/p/{slug}/settings/categories/{id}/edit` | Edit category | |
| 126 |
| POST | `/p/{slug}/settings/categories/{id}/move` | Reorder category | |
| 127 |
|
| 128 |
### Moderation (moderator+) |
| 129 |
|
| 130 |
|
| 131 |
| GET | `/p/{slug}/moderation` | Moderation dashboard | |
| 132 |
| POST | `/p/{slug}/moderation/ban` | Ban user (with expiry) | |
| 133 |
| POST | `/p/{slug}/moderation/unban` | Unban user | |
| 134 |
| POST | `/p/{slug}/moderation/mute` | Mute user (write-only restriction) | |
| 135 |
| POST | `/p/{slug}/moderation/unmute` | Unmute user | |
| 136 |
| GET | `/p/{slug}/moderation/log` | Mod log (paginated) | |
| 137 |
|
| 138 |
### Platform Admin (PLATFORM_ADMIN_ID only) |
| 139 |
|
| 140 |
|
| 141 |
| GET | `/_admin` | Platform admin dashboard | |
| 142 |
| POST | `/_admin/communities/{id}/suspend` | Suspend community | |
| 143 |
| POST | `/_admin/communities/{id}/unsuspend` | Unsuspend community | |
| 144 |
| POST | `/_admin/users/{id}/suspend` | Suspend user | |
| 145 |
| POST | `/_admin/users/{id}/unsuspend` | Unsuspend user | |
| 146 |
|
| 147 |
### Auth & System |
| 148 |
|
| 149 |
|
| 150 |
| GET | `/auth/login` | Initiate MNW OAuth (PKCE) | |
| 151 |
| GET | `/auth/callback` | OAuth callback (exchange code, set session) | |
| 152 |
| GET | `/auth/logout` | Clear session | |
| 153 |
| GET | `/api/health` | Health check (monitored by PoM) | |
| 154 |
|
| 155 |
## Security |
| 156 |
|
| 157 |
- **CSRF**: SHA256 tokens on all POST/PUT/DELETE, constant-time comparison, middleware layer |
| 158 |
- **Sessions**: tower-sessions with PostgresStore, 7-day expiry, SameSite::Lax |
| 159 |
- **XSS**: Markdown rendered via pulldown-cmark with HTML tags stripped |
| 160 |
- **Auth**: MNW OAuth PKCE — no passwords stored locally |
| 161 |
- **Moderation**: Role hierarchy enforced on all read/write handlers |
| 162 |
- **Suspensions**: Platform-level suspension checks on all handlers |
| 163 |
|
| 164 |
## Deployment |
| 165 |
|
| 166 |
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). |
| 167 |
|
| 168 |
### Hetzner (production) |
| 169 |
|
| 170 |
- **Host**: `alpha-west-1` (Tailscale `100.120.174.96`, public `5.78.144.244`) |
| 171 |
- **SSH**: `root@100.120.174.96` (via Tailscale) |
| 172 |
- **Install path**: `/opt/multithreaded/` |
| 173 |
- **Build**: cross-compiled on macOS via `cargo zigbuild --release --target x86_64-unknown-linux-gnu` |
| 174 |
- **Deploy script**: `deploy/deploy-hetzner.sh` (build, upload binary + static + migrations, restart) |
| 175 |
- `--setup` -- first-time: create system user, dirs, database, build, install, seed |
| 176 |
- `--config` -- upload systemd unit, static assets, migrations only |
| 177 |
- **Env file**: `deploy/env.hetzner` -> `/opt/multithreaded/.env` |
| 178 |
- **Reverse proxy**: Caddy (TLS termination via Cloudflare Origin CA, `forums.makenot.work`) |
| 179 |
- **Bind address**: `127.0.0.1:3400` (Caddy fronts it; no direct external access) |
| 180 |
- **OAuth**: `client_id=mt-forums-6378957b452bbbc906c3db8edd072d64`, redirect to `https://forums.makenot.work/auth/callback`, `MNW_BASE_URL=https://makenot.work` |
| 181 |
|
| 182 |
### Astra (staging/dev) |
| 183 |
|
| 184 |
- **Host**: `astra` (Tailscale `100.106.221.39`) |
| 185 |
- **SSH**: `max@100.106.221.39` (via Tailscale) |
| 186 |
- **Install path**: `/opt/multithreaded/` |
| 187 |
- **Build**: native build on astra (aarch64). Source rsynced to `~/src/multithreaded/`, built with `cargo build --release`, binary copied to `/opt/multithreaded/`. |
| 188 |
- **Deploy script**: `deploy/deploy.sh` (rsync source, build remote, deploy files, restart) |
| 189 |
- `--setup` -- first-time: create system user, dirs, database, build, install, seed |
| 190 |
- **Env file**: `deploy/env.production` -> `/opt/multithreaded/.env` |
| 191 |
- **No reverse proxy**: direct access on port 3400 via Tailscale IP |
| 192 |
- **Bind address**: `0.0.0.0:3400` |
| 193 |
- **OAuth**: `MNW_BASE_URL=http://127.0.0.1:3000` (local MNW instance), redirect to `http://100.106.221.39:3400/auth/callback` |
| 194 |
|
| 195 |
### Shared Details |
| 196 |
|
| 197 |
- **systemd unit**: `deploy/multithreaded.service` |
| 198 |
- Runs as `multithreaded` system user |
| 199 |
- `EnvironmentFile=/opt/multithreaded/.env` |
| 200 |
- Security hardening: `NoNewPrivileges`, `ProtectSystem=strict`, `ProtectHome`, `PrivateTmp`, `MemoryMax=512M` |
| 201 |
- Depends on `postgresql.service` |
| 202 |
- **Migrations**: auto-applied on boot (`sqlx::migrate!()`) |
| 203 |
- **Seeding**: `./multithreaded --seed` (idempotent, run once after first deploy) |
| 204 |
- **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. |
| 205 |
- **Prerequisites for hetzner cross-compile**: `brew install zig`, `cargo install cargo-zigbuild`, `rustup target add x86_64-unknown-linux-gnu` |
| 206 |
|
| 207 |
### Monitoring |
| 208 |
|
| 209 |
- PoM monitors `forums.makenot.work` -- health check on `/api/health`, TLS validation, route probes, DNS verification |
| 210 |
- PoM targets: MNW, MT (`forums.makenot.work`), htpy.app |
| 211 |
|
| 212 |
### Deploy Files |
| 213 |
|
| 214 |
``` |
| 215 |
deploy/ |
| 216 |
├── deploy.sh # Astra deploy (rsync + native build) |
| 217 |
├── deploy-hetzner.sh # Hetzner deploy (cross-compile + upload) |
| 218 |
├── multithreaded.service # systemd unit (shared) |
| 219 |
├── env.production # Env vars for astra |
| 220 |
└── env.hetzner # Env vars for hetzner |
| 221 |
``` |
| 222 |
|
| 223 |
## Testing |
| 224 |
|
| 225 |
90 tests total: |
| 226 |
- 56 integration tests (CSRF, auth, CRUD, permissions, moderation, bans, admin) |
| 227 |
- 18 unit tests in mt-core (error types, helpers) |
| 228 |
- 16 unit tests in mt-db (query builders, model conversions) |
| 229 |
|