| 1 |
# Makenotwork -- Architecture |
| 2 |
|
| 3 |
Creator platform with 0% platform fee (only Stripe's ~3% processing fee). Rust/Axum monolith, server-rendered HTML, PostgreSQL, deployed natively on Hetzner VPS behind Caddy. |
| 4 |
|
| 5 |
## System Overview |
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
| Language | Rust (2024 edition) | |
| 10 |
| Web framework | Axum | |
| 11 |
| Database | PostgreSQL (sqlx, compile-time checked queries) | |
| 12 |
| Templates | Askama (server-rendered HTML) | |
| 13 |
| Interactivity | HTMX (no JS framework) | |
| 14 |
| Payments | Stripe Connect | |
| 15 |
| Email | Postmark (transactional + broadcast) | |
| 16 |
| Storage | Hetzner Object Storage (S3-compatible) | |
| 17 |
| Reverse proxy | Caddy (TLS, static files, custom domains) | |
| 18 |
| CDN / DDoS | Cloudflare (Full Strict SSL, mTLS) | |
| 19 |
| Hosting | Hetzner VPS (systemd, no Docker) | |
| 20 |
| Banking | Mercury (business checking) | |
| 21 |
| Auth | Argon2id passwords, tower-sessions (PostgreSQL-backed), TOTP, WebAuthn passkeys | |
| 22 |
|
| 23 |
Single binary, single process. Migrations auto-run on boot. Background tasks (health monitor, scheduler) run as spawned Tokio tasks within the same process. |
| 24 |
|
| 25 |
For capacity planning and the upgrade path from current single-VM topology through 100k creators (including cost projections), see `scaling.md`. |
| 26 |
|
| 27 |
## Code Structure |
| 28 |
|
| 29 |
``` |
| 30 |
MNW/server/src/ |
| 31 |
main.rs Entry point (tracing, pool, session store, app state, graceful shutdown) |
| 32 |
lib.rs Library root (AppState, build_app, route composition) |
| 33 |
config.rs Configuration from environment variables |
| 34 |
constants.rs Centralized magic numbers (limits, timeouts, rate limits) |
| 35 |
error.rs AppError enum → HTTP status + HTML/JSON response |
| 36 |
auth.rs Argon2 hashing, session management, extractors (AuthUser, MaybeUser, AdminUser) |
| 37 |
csrf.rs CSRF middleware (token generation + validation) |
| 38 |
validation/ Input validation helpers (directory module) |
| 39 |
helpers.rs Shared utility functions, rate limiter constructors |
| 40 |
payments/ Stripe Connect client wrapper (directory module) |
| 41 |
storage.rs S3 storage backend (trait + implementation, presigned URLs) |
| 42 |
synckit_auth.rs SyncKit JWT token creation + extraction |
| 43 |
build_runner.rs SSH-based remote build execution |
| 44 |
git/ Git source browser logic (git2, syntax highlighting, directory module) |
| 45 |
monitor.rs Background health monitor (DB, S3, sessions) |
| 46 |
scheduler.rs Background scheduler (scheduled publish, mailing list delivery) |
| 47 |
mt_client.rs HTTP client for Multithreaded internal API |
| 48 |
rss.rs RSS/Atom feed generation |
| 49 |
wordlist.rs Wordlist for human-readable invite codes |
| 50 |
pricing.rs Access control and pricing model trait |
| 51 |
db/ Database queries (module per domain) |
| 52 |
routes/ HTTP handlers (module per domain) |
| 53 |
templates/ Askama template structs (public, dashboard, partials) |
| 54 |
types/ Shared request/response types |
| 55 |
email/ Email composition (templates) + HMAC-signed URL tokens |
| 56 |
scanning/ 6-layer file scanning pipeline |
| 57 |
``` |
| 58 |
|
| 59 |
## Route Organization |
| 60 |
|
| 61 |
Routes are composed in `lib.rs::build_app()` from 13 route modules: |
| 62 |
|
| 63 |
|
| 64 |
|
| 65 |
| `pages::public` | `/`, `/u/`, `/p/`, `/i/`, `/discover`, `/docs`, `/pricing` | Public-facing HTML pages | |
| 66 |
| `pages::dashboard` | `/dashboard/` | Creator/user dashboard (HTMX tabs) | |
| 67 |
| `pages::blog` | `/p/{slug}/blog/` | Blog pages | |
| 68 |
| `pages::feeds` | `/feed/` | RSS/Atom feeds | |
| 69 |
| `pages::email_actions` | `/verify/`, `/reset/`, `/confirm-delete/` | Email link handlers | |
| 70 |
| `api` | `/api/` | JSON API (CRUD, exports, license keys, domains) | |
| 71 |
| `auth` | `/login`, `/join`, `/logout`, `/forgot-password` | Authentication pages + handlers | |
| 72 |
| `admin` | `/_admin/` | Admin panels (waitlist, users, uploads, appeals, reports) | |
| 73 |
| `storage` | `/upload/`, `/download/` | File upload (multipart) + download (presigned S3) | |
| 74 |
| `stripe` | `/stripe/` | Stripe Connect OAuth + webhooks | |
| 75 |
| `synckit` | `/synckit/` | SyncKit sync API (auth, push/pull, devices, keys, blobs) | |
| 76 |
| `oauth` | `/oauth/` | OAuth 2.0 provider (authorize, token, userinfo) | |
| 77 |
| `ota` | `/ota/` | OTA update server (Tauri-compatible protocol) | |
| 78 |
| `builds` | `/builds/` | CI/build pipeline (configs, trigger, status, logs) | |
| 79 |
| `git` | `/g/` | Git source browser (tree, blob, commits, blame, diff) | |
| 80 |
| `git_issues` | `/g/{owner}/{repo}/issues/` | Git issue tracker | |
| 81 |
| `postmark` | `/webhooks/postmark/` | Postmark inbound email + bounce/delivery webhooks | |
| 82 |
| `custom_domain` | (fallback) | Custom domain resolution (profile/project/item by Host header) | |
| 83 |
|
| 84 |
### API Route Tiers |
| 85 |
|
| 86 |
The `/api/` routes are split into tiers with independent rate limits (tower-governor): |
| 87 |
|
| 88 |
|
| 89 |
|
| 90 |
| Write | burst 10, 2/sec per IP | All POST/PUT/DELETE (CRUD, blog, tiers, keys, codes, domains) | |
| 91 |
| Export | burst 3, 1/sec per IP | `/api/export/*` (projects, sales, purchases, followers, content) | |
| 92 |
| License keys | burst 20, 5/sec per IP | `/api/keys/*` (validate, deactivate, status) | |
| 93 |
| Validate | burst 10, 1/sec per IP | `/api/validate/*` (slug uniqueness checks) | |
| 94 |
| Read | No limit (alpha scale) | All GET endpoints | |
| 95 |
|
| 96 |
A `json_error_layer` middleware converts HTML error responses to `{"error": "..."}` on API routes. |
| 97 |
|
| 98 |
## Database Layer |
| 99 |
|
| 100 |
PostgreSQL via sqlx with compile-time checked queries. Numbered migrations in `migrations/`, auto-applied on boot; the directory is the source of truth. Connection pool: 25 max connections, 3-second acquire timeout. |
| 101 |
|
| 102 |
### DB Modules |
| 103 |
|
| 104 |
Each `db/` submodule handles a specific domain: |
| 105 |
|
| 106 |
|
| 107 |
|
| 108 |
| `users` | User accounts, profiles, preferences, email verification | |
| 109 |
| `auth` | Login attempts, lockout tracking | |
| 110 |
| `sessions` | Session tracking rows (for remote revocation) | |
| 111 |
| `totp` | TOTP secrets, backup codes | |
| 112 |
| `passkeys` | WebAuthn credential storage | |
| 113 |
| `projects` | Creator projects (software, blog, etc.) | |
| 114 |
| `items` | Content items (digital goods, text, audio) | |
| 115 |
| `versions` | Item version history | |
| 116 |
| `chapters` | Text content chapters | |
| 117 |
| `blog_posts` | Project blog posts (scheduled publish, web-only flag) | |
| 118 |
| `transactions` | Purchase/payment records | |
| 119 |
| `subscriptions` | Subscription tiers + Stripe subscription tracking | |
| 120 |
| `license_keys` | Software license key generation + validation | |
| 121 |
| `promo_codes` | Discount and free-access promo codes | |
| 122 |
| `follows` | Follow relationships (users, projects, tags) | |
| 123 |
| `discover` | Discovery/browse queries (filtering, sorting, pagination) | |
| 124 |
| `analytics` | View counts, download counts, revenue aggregation | |
| 125 |
| `tags` | TagTree-based tagging (path column, per-app config) | |
| 126 |
| `categories` | Content categories | |
| 127 |
| `labels` | Creator-defined labels for content organization | |
| 128 |
| `collections` | User-curated item collections | |
| 129 |
| `custom_links` | Profile custom links (reorderable) | |
| 130 |
| `custom_domains` | Custom domain DNS verification + mapping | |
| 131 |
| `synckit` | SyncKit apps, devices, sync_log, keys, blobs | |
| 132 |
| `ota` | OTA releases + artifacts | |
| 133 |
| `builds` | Build configs + build records | |
| 134 |
| `oauth` | OAuth clients, authorization codes, tokens | |
| 135 |
| `git_repos` | Git repository metadata + visibility | |
| 136 |
| `ssh_keys` | User SSH public keys | |
| 137 |
| `issues` | Git issue tracker (issues, comments, labels) | |
| 138 |
| `patches` | Email patch handling | |
| 139 |
| `mailing_lists` | Project mailing lists + subscriber management | |
| 140 |
| `content_insertions` | Reusable content clips (S3-backed) | |
| 141 |
| `email_suppressions` | Email unsubscribe tracking | |
| 142 |
| `scanning` | File scan results + quarantine status | |
| 143 |
| `health` | Health check history | |
| 144 |
| `monitor` | Health monitoring snapshots | |
| 145 |
| `waitlist` | Creator waitlist + invite codes | |
| 146 |
| `invites` | Creator-to-creator invite system | |
| 147 |
| `reports` | User content reports | |
| 148 |
| `fan_plus` | Fan+ subscription tracking | |
| 149 |
| `creator_tiers` | Creator tier enforcement (storage limits, feature gates) | |
| 150 |
| `bundles` | Item bundling/packs | |
| 151 |
| `email_signups` | Early signup tracking | |
| 152 |
|
| 153 |
### Type System |
| 154 |
|
| 155 |
- `define_pg_uuid_id!` macro -- newtype UUID wrappers (`UserId`, `ProjectId`, `ItemId`, etc.) |
| 156 |
- `impl_str_enum!` macro -- bidirectional enum-string conversion with sqlx Type/Encode/Decode |
| 157 |
- `validated_types` -- `Username`, `Slug`, `Email` with parse-time validation |
| 158 |
- `models` -- database row structs (`DbUser`, `DbProject`, `DbItem`, etc.) |
| 159 |
|
| 160 |
## Key Integrations |
| 161 |
|
| 162 |
### Stripe Connect |
| 163 |
|
| 164 |
Stripe Connect Standard for creator payouts. Creators connect their Stripe account via OAuth. MNW takes 0% platform fee -- only Stripe's ~3% processing fee applies. |
| 165 |
|
| 166 |
- Checkout sessions for one-time purchases and subscriptions |
| 167 |
- Webhook handlers for payment confirmation, subscription lifecycle, disputes |
| 168 |
- Creator tier subscriptions (Basic $16, Small Files $24, Big Files $36, Everything $60) |
| 169 |
- Fan+ consumer subscriptions ($8/mo) |
| 170 |
- Promo codes (percentage/fixed discount, free access) |
| 171 |
|
| 172 |
### Postmark |
| 173 |
|
| 174 |
Transactional email (password reset, email verification, purchase receipts, new-device notifications) and broadcast email (mailing list delivery for content announcements, blog post notifications). |
| 175 |
|
| 176 |
- HMAC-signed tokens in email action URLs (password reset, verification, account deletion) |
| 177 |
- Inbound webhook for email patch processing |
| 178 |
- Bounce/delivery webhooks for suppression tracking |
| 179 |
- Dev mode: logs emails when `POSTMARK_TOKEN` is not set |
| 180 |
|
| 181 |
### SyncKit |
| 182 |
|
| 183 |
Developer sync infrastructure hosted on MNW. JWT-authenticated API for client apps (GO, BB, AF). |
| 184 |
|
| 185 |
- Push/pull changelog sync with E2E encryption (ChaCha20-Poly1305 + Argon2) |
| 186 |
- Device management, key envelopes |
| 187 |
- Blob storage (separate S3 bucket, presigned upload/download) |
| 188 |
- OTA update server (Tauri-compatible protocol -- releases, artifacts, version checks) |
| 189 |
- CI/build pipeline (SSH-based remote builds, post-receive hooks, build configs) |
| 190 |
|
| 191 |
### DocEngine |
| 192 |
|
| 193 |
Extracted crate (`shared/docengine/`) for documentation rendering. Loads markdown files from disk at startup, renders to HTML with section hierarchy, navigation, and search index. Serves `/docs/*` routes. |
| 194 |
|
| 195 |
### TagTree |
| 196 |
|
| 197 |
Shared tag standard (`shared/tagtree/`). Hierarchical tags with path column (migration 038). Per-app config: MNW allows 5 levels, 100 max tags. Used for content discovery and filtering. |
| 198 |
|
| 199 |
### File Scanning |
| 200 |
|
| 201 |
6-layer malware scanning pipeline for uploaded files: |
| 202 |
|
| 203 |
|
| 204 |
|
| 205 |
| 1. Content-type | In-process | Magic-byte verification against declared file type | |
| 206 |
| 2. Structural | In-process | Binary analysis (PE headers in non-download contexts) | |
| 207 |
| 3. Archive | In-process | ZIP bomb detection (ratio, depth, uncompressed size limits) | |
| 208 |
| 4. YARA | In-process | Custom rule matching (compiled at startup) | |
| 209 |
| 5. ClamAV | External | Antivirus scan via Unix socket (optional) | |
| 210 |
| 6. MalwareBazaar | External | SHA-256 hash lookup against known malware (optional) | |
| 211 |
|
| 212 |
Fail-closed: scanner errors hold files for admin review rather than allowing through. |
| 213 |
|
| 214 |
### Git Source Browser |
| 215 |
|
| 216 |
`git2`-based source browser reading bare repos from disk. Syntax highlighting via `syntect`. Features: tree/blob browsing, commit history, blame, diff, file log. Issue tracker with labels and comments. SSH clone via `ssh.makenot.work`. |
| 217 |
|
| 218 |
### Multithreaded Integration |
| 219 |
|
| 220 |
HTTP client (`mt_client`) for the Multithreaded forum instance. HMAC-signed internal API for: |
| 221 |
|
| 222 |
- Auto-provisioning forum communities when projects are created |
| 223 |
- Linking discussion threads to content items |
| 224 |
- Mailing list subscriber management |
| 225 |
|
| 226 |
## Security Model |
| 227 |
|
| 228 |
### Authentication |
| 229 |
|
| 230 |
- **Passwords:** Argon2id with random salt per hash |
| 231 |
- **Sessions:** `tower-sessions` with PostgreSQL-backed store, ID regeneration on login (prevents fixation), 7-day expiry on inactivity |
| 232 |
- **Session cache:** DashMap caches recent session validations (TTL from `constants::SESSION_TOUCH_CACHE_SECS`, currently 5s) to skip per-request DB touch |
| 233 |
- **2FA:** TOTP (totp-rs, 6-digit, 30-second step, +/-1 skew) + WebAuthn passkeys (webauthn-rs) |
| 234 |
- **Account lockout:** 5 failed attempts triggers 15-minute lockout |
| 235 |
- **New-device notifications:** Email alert on login from unrecognized session |
| 236 |
|
| 237 |
### Authorization |
| 238 |
|
| 239 |
- `AuthUser` extractor -- required login, loads session user |
| 240 |
- `MaybeUser` extractor -- optional login for public pages |
| 241 |
- `AdminUser` extractor -- admin-only, returns 404 (not 403) to hide admin routes |
| 242 |
- Resource ownership verified per-request (`ensure_project_owner`, `verify_item_ownership`) |
| 243 |
|
| 244 |
### Network Security |
| 245 |
|
| 246 |
- **Cloudflare:** Full (Strict) SSL, Authenticated Origin Pulls (mTLS), DDoS protection |
| 247 |
- **Origin CA:** Wildcard cert for `*.makenot.work`, 15-year RSA |
| 248 |
- **Caddy:** Reverse proxy, on-demand TLS for custom domains, static file serving |
| 249 |
- **Firewall:** ufw + fail2ban + hardened sshd |
| 250 |
- **Custom domains:** DNS TXT verification via Cloudflare DoH, Caddy on-demand TLS, DashMap domain cache |
| 251 |
|
| 252 |
### Request Security |
| 253 |
|
| 254 |
- **CSRF:** Middleware-enforced tokens on all mutating requests. HTMX gets headers via `hx-headers` on body element. JS uses `csrfHeaders()`. |
| 255 |
- **Rate limiting:** tower-governor per-IP rate limits on all write endpoints (see API Route Tiers above) |
| 256 |
- **Request body limit:** 1 MB default (tower-http `RequestBodyLimitLayer`) |
| 257 |
- **Anti-enumeration:** 400ms delay on username availability checks |
| 258 |
|
| 259 |
### Data Security |
| 260 |
|
| 261 |
- **SyncKit E2E encryption:** Server stores only encrypted blobs, never plaintext user data |
| 262 |
- **HMAC-signed email tokens:** Password reset, verification, deletion links with expiry |
| 263 |
- **File scanning:** 6-layer pipeline, fail-closed on scanner errors |
| 264 |
|
| 265 |
## Template System |
| 266 |
|
| 267 |
Askama templates with HTMX for dynamic interactions. Three layers: |
| 268 |
|
| 269 |
|
| 270 |
|
| 271 |
| Full pages | `templates/pages/`, `templates/dashboards/` | Extend `base.html`, define `{% block content %}` | |
| 272 |
| Tab content | `templates/partials/tabs/` | Standalone HTML fragments, loaded via `hx-get` | |
| 273 |
| Feedback | `templates/partials/` | Alert, form status, save status fragments | |
| 274 |
|
| 275 |
Template structs are defined in `src/templates/` (public.rs, dashboard.rs, partials.rs) with a macro (`impl_into_response!`) that implements `IntoResponse` for all template types. |
| 276 |
|
| 277 |
Dashboard pages use HTMX tabs: each tab button triggers `hx-get` to load content into `#tab-content`. Admin actions return full table partials for HTMX swap. |
| 278 |
|
| 279 |
## AppState |
| 280 |
|
| 281 |
Shared application state (`Arc`-wrapped by Axum): |
| 282 |
|
| 283 |
|
| 284 |
|
| 285 |
| `db` | `PgPool` | PostgreSQL connection pool | |
| 286 |
| `config` | `Config` | Environment-based configuration | |
| 287 |
| `s3` | `Option<Arc<dyn StorageBackend>>` | Main file storage (uploads, covers, downloads) | |
| 288 |
| `synckit_s3` | `Option<Arc<dyn StorageBackend>>` | Separate SyncKit blob storage bucket | |
| 289 |
| `stripe` | `Option<StripeClient>` | Stripe Connect client | |
| 290 |
| `email` | `EmailClient` | Postmark email client (logs in dev) | |
| 291 |
| `docs` | `Arc<DocLoader>` | Documentation pages (loaded at startup) | |
| 292 |
| `scanner` | `Option<Arc<ScanPipeline>>` | File scanning pipeline (YARA rules compiled at startup) | |
| 293 |
| `webauthn` | `Arc<Webauthn>` | WebAuthn relying party | |
| 294 |
| `syntax` | `Option<Arc<SyntaxHighlighter>>` | Git source browser syntax highlighting | |
| 295 |
| `session_cache` | `Arc<DashMap<UserSessionId, Instant>>` | Recent session validation cache | |
| 296 |
| `mt_client` | `Option<MtClient>` | Multithreaded internal API client | |
| 297 |
| `domain_cache` | `Arc<DashMap<String, UserId>>` | Verified custom domain lookup cache | |
| 298 |
|
| 299 |
Optional fields (`Option`) allow the server to run in degraded mode when external services are not configured (no S3 = no uploads, no Stripe = no payments, etc.). |
| 300 |
|
| 301 |
## Background Tasks |
| 302 |
|
| 303 |
Two spawned Tokio tasks, coordinated via `watch::channel` for graceful shutdown: |
| 304 |
|
| 305 |
|
| 306 |
|
| 307 |
| Health monitor | 60 seconds | Probes DB, S3, sessions. Records history. Alerts on status transitions. | |
| 308 |
| Scheduler | 60 seconds | Publishes scheduled items/blog posts. Sends mailing list announcements. | |
| 309 |
|
| 310 |
## Concurrency Model |
| 311 |
|
| 312 |
- **Tokio multi-threaded runtime** drives all I/O |
| 313 |
- **`Arc<DashMap<>>`** for session cache and domain cache (lock-free concurrent maps) |
| 314 |
- **Graceful shutdown:** SIGINT/SIGTERM triggers shutdown signal, 10-second drain window for in-flight requests, then force exit |
| 315 |
- **Connection pool:** sqlx PgPool (25 connections) handles concurrent DB access |
| 316 |
- **`into_make_service_with_connect_info`** provides peer IP for rate limiting |
| 317 |
|
| 318 |
## Testing |
| 319 |
|
| 320 |
1,028+ tests (486 unit + 542 integration + 28 health + 4 load). Zero failures. |
| 321 |
|
| 322 |
- **Unit tests:** In-file `#[cfg(test)]` modules, no DB required |
| 323 |
- **Integration tests:** Each test creates/drops its own PostgreSQL database. Test harness in `tests/harness/`. Workflow tests in `tests/workflows/`. |
| 324 |
- **Astra:** Dev server with `RUST_TEST_THREADS=8` (96-core machine, but PG overwhelms at full parallelism) |
| 325 |
|
| 326 |
## Key Design Decisions |
| 327 |
|
| 328 |
- **Monolith over microservices:** Single binary, single process. SyncKit, OTA, builds, git browser, and OAuth are all route modules in the same app. Simplifies deployment, testing, and state sharing. |
| 329 |
- **Server-rendered HTML over SPA:** Askama templates + HTMX. No client-side routing, no JS framework, no build step. Tab content loaded as HTML fragments. |
| 330 |
- **PostgreSQL sessions over Redis:** `tower-sessions-sqlx-store` keeps sessions in PostgreSQL. One fewer service to operate. Session cache (DashMap) reduces per-request DB load. |
| 331 |
- **Optional everything:** S3, Stripe, ClamAV, YARA, git browser, MT integration, and custom domains are all `Option` fields. The server starts and serves pages even when external services are unavailable. |
| 332 |
- **Compile-time SQL:** sqlx checks all queries at compile time against the database schema. No runtime SQL generation. |
| 333 |
- **Fail-closed scanning:** File uploads held for admin review on scanner errors, never silently allowed through. |
| 334 |
- **Native deployment:** systemd unit, direct binary on disk. Cross-compiled via `cargo zigbuild`. No Docker. |
| 335 |
|
| 336 |
## Key Paths |
| 337 |
|
| 338 |
|
| 339 |
|
| 340 |
| Entry point | `src/main.rs` | |
| 341 |
| Library root | `src/lib.rs` | |
| 342 |
| Configuration | `src/config.rs` | |
| 343 |
| Constants | `src/constants.rs` | |
| 344 |
| Error types | `src/error.rs` | |
| 345 |
| Authentication | `src/auth.rs` | |
| 346 |
| CSRF middleware | `src/csrf.rs` | |
| 347 |
| Database modules | `src/db/` (65 files) | |
| 348 |
| Route modules | `src/routes/` (17 submodules) | |
| 349 |
| Template structs | `src/templates/` | |
| 350 |
| Email service | `src/email/` | |
| 351 |
| File scanning | `src/scanning/` | |
| 352 |
| Stripe integration | `src/payments/` | |
| 353 |
| S3 storage | `src/storage.rs` | |
| 354 |
| SyncKit auth | `src/synckit_auth.rs` | |
| 355 |
| Build runner | `src/build_runner.rs` | |
| 356 |
| Git browser | `src/git/` | |
| 357 |
| MT client | `src/mt_client.rs` | |
| 358 |
| Health monitor | `src/monitor.rs` | |
| 359 |
| Scheduler | `src/scheduler.rs` | |
| 360 |
| Shared types | `src/types/` | |
| 361 |
| Askama templates | `templates/` | |
| 362 |
| Migrations | `migrations/` (numbered, applied in order) | |
| 363 |
| Static assets | `static/` | |
| 364 |
| Integration tests | `tests/` | |
| 365 |
| Deploy scripts | `deploy/` | |
| 366 |
|