max / makenotwork
7 files changed,
+1202 insertions,
-174 deletions
| @@ -30,47 +30,7 @@ Fair creator platform with 0% platform fee (only Stripe's ~3% processing fee). M | |||
| 30 | 30 | ||
| 31 | 31 | ## Ecosystem | |
| 32 | 32 | ||
| 33 | - | This is a monorepo containing the MNW server and all related ecosystem projects: | |
| 34 | - | ||
| 35 | - | | Project | Path | Description | | |
| 36 | - | |---------|------|-------------| | |
| 37 | - | | MNW Server | `server/` | Rust/Axum backend, HTMX frontend, PostgreSQL, Stripe Connect | | |
| 38 | - | | Multithreaded | `multithreaded/` | Forum software integrated with MNW (Rust/Axum/PostgreSQL, MNW OAuth) | | |
| 39 | - | | PoM | `pom/` | Production operations monitor (health checks, TLS tracking, email alerts) | | |
| 40 | - | | mnw-cli | `mnw-cli/` | CLI tool for MNW platform | | |
| 41 | - | ||
| 42 | - | Shared libraries live at `shared/` (docengine, tagtree, synckit-client, theme-common, s3-storage, themes, tauri-updater-ui). | |
| 43 | - | ||
| 44 | - | ## Repository Layout | |
| 45 | - | ||
| 46 | - | ``` | |
| 47 | - | MNW/ # Monorepo root | |
| 48 | - | server/ # MNW server (crate root) | |
| 49 | - | src/ # Application source | |
| 50 | - | migrations/ # SQLx migrations (numbered, auto-applied on boot) | |
| 51 | - | templates/ # Askama HTML templates | |
| 52 | - | static/ # CSS, JS, fonts, images | |
| 53 | - | tests/ # Integration tests (workflows/, load/, harness/) | |
| 54 | - | deploy/ # Deployment scripts and config files | |
| 55 | - | deploy.sh # Cross-compile + upload + restart | |
| 56 | - | makenotwork.service # systemd unit file | |
| 57 | - | Caddyfile # Reverse proxy config | |
| 58 | - | backup-db.sh # DB backup script | |
| 59 | - | error-pages/ # Custom 404/500/502 pages | |
| 60 | - | site-docs/ # DocEngine content (public/ and unpublished/) | |
| 61 | - | docs/ # Server-specific docs (todo, audit, architecture, etc.) | |
| 62 | - | multithreaded/ # Forum software | |
| 63 | - | pom/ # Production operations monitor | |
| 64 | - | mnw-cli/ # CLI tool | |
| 65 | - | shared/ # Shared libraries | |
| 66 | - | docengine/ # Markdown rendering + documentation engine | |
| 67 | - | tagtree/ # Hierarchical tag standard | |
| 68 | - | synckit-client/ # SyncKit cloud sync client SDK | |
| 69 | - | theme-common/ # Theme loading + parsing | |
| 70 | - | s3-storage/ # S3-compatible storage abstraction | |
| 71 | - | themes/ # TOML theme definitions | |
| 72 | - | tauri-updater-ui/ # OTA update UI components | |
| 73 | - | ``` | |
| 33 | + | Monorepo: `server/`, `multithreaded/`, `pom/`, `mnw-cli/`, `shared/`. No root `[workspace]` — each project is its own crate (see `server/docs/architecture.md` for full layout and why). | |
| 74 | 34 | ||
| 75 | 35 | ## Coding Patterns | |
| 76 | 36 | ||
| @@ -82,147 +42,31 @@ See `multithreaded/CONTRIBUTING.md` for MT-specific patterns (MNW OAuth, shared | |||
| 82 | 42 | ||
| 83 | 43 | ## Versioning | |
| 84 | 44 | ||
| 85 | - | - Semver in `Cargo.toml` (`env!("CARGO_PKG_VERSION")` compiles it into the binary for Sentry release strings) | |
| 45 | + | - Semver in `Cargo.toml` (`env!("CARGO_PKG_VERSION")` compiles it into the binary) | |
| 86 | 46 | - **Before every deploy to production**, ask the user what version to set — never auto-bump | |
| 87 | 47 | - Version bump = edit `Cargo.toml` version field before building | |
| 88 | 48 | ||
| 89 | - | --- | |
| 49 | + | ## SyncKit | |
| 90 | 50 | ||
| 91 | - | ## MNW SyncKit | |
| 51 | + | E2E encrypted cloud sync + OTA updates for indie apps. Design philosophy: general-purpose first, E2E encrypted by default, bring your own schema, auth via MNW accounts. See `server/docs/architecture.md` for component locations and `shared/synckit-client/` crate docs for the SDK. | |
| 92 | 52 | ||
| 93 | - | Developer infrastructure for indie apps, hosted on Makenotwork. | |
| 53 | + | ## Operations Quick Reference | |
| 94 | 54 | ||
| 95 | - | ### Services | |
| 55 | + | Detailed infrastructure, deployment, and troubleshooting docs live in `server/docs/`: | |
| 56 | + | - **Deployment:** `server/deploy/deploy.sh` (run from `MNW/server/`) | |
| 57 | + | - **Rollback:** `server/docs/rollback.md` | |
| 58 | + | - **Troubleshooting:** `server/docs/troubleshooting.md` | |
| 59 | + | - **Incident response:** `../_meta/incident_response.md` | |
| 60 | + | - **Infrastructure diagrams:** `../_meta/diagrams/infra/index.md` | |
| 61 | + | - **Service accounts:** `../_meta/service_accounts.md` | |
| 62 | + | - **Testing:** `server/docs/test_plan.md` | |
| 63 | + | - **Schema:** `server/docs/schema.md` | |
| 96 | 64 | ||
| 97 | - | - **Cloud Sync** — Push/pull changelog sync with E2E encryption, device management, conflict resolution | |
| 98 | - | - **OTA Updates** — App auto-update server (Tauri-compatible protocol), no app store dependency | |
| 65 | + | ### Key IPs (for LLM-assisted SSH/deploy commands) | |
| 99 | 66 | ||
| 100 | - | ### Design Philosophy | |
| 101 | - | ||
| 102 | - | - **General-purpose first** — API and SDK decisions should make sense for any app, not just GO | |
| 103 | - | - **E2E encrypted by default** — Server stores only encrypted blobs, never plaintext user data | |
| 104 | - | - **Bring your own schema** — Table names, row IDs, and data shapes are opaque to the server | |
| 105 | - | - **Auth via MNW accounts** — Users authenticate with their Makenot.work credentials | |
| 106 | - | ||
| 107 | - | ### Components | |
| 108 | - | ||
| 109 | - | | Component | Location | Role | | |
| 110 | - | |-----------|----------|------| | |
| 111 | - | | Server API | `server/src/routes/synckit.rs` | Axum endpoints (auth, push/pull, devices, keys) | | |
| 112 | - | | Server DB | `server/src/db/synckit.rs` | PostgreSQL queries (sync_apps, sync_devices, sync_log, sync_keys) | | |
| 113 | - | | Server Auth | `server/src/synckit_auth.rs` | JWT token creation + extraction | | |
| 114 | - | | Client SDK | `shared/synckit-client/` | Rust crate — HTTP client, E2E crypto, keychain storage | | |
| 115 | - | | Integration tests | `server/tests/workflows/synckit.rs` | 7 tests covering auth, devices, push/pull, keys, validation | | |
| 116 | - | ||
| 117 | - | ### Consumers | |
| 118 | - | ||
| 119 | - | | App | What it syncs | Status | | |
| 120 | - | |-----|---------------|--------| | |
| 121 | - | | GoingsOn | Tasks, projects, events, contacts, emails | Implemented | | |
| 122 | - | | Balanced Breakfast | Configs, feed sources, plugin manifests | Integrated | | |
| 123 | - | | audiofiles | Sample metadata, tags, VFS mappings | Integrated | | |
| 124 | - | ||
| 125 | - | --- | |
| 67 | + | - **Hetzner (prod):** `100.120.174.96` (Tailscale) / `5.78.144.244` (public) | |
| 68 | + | - **Astra (dev/test):** `100.106.221.39` (Tailscale) | |
| 126 | 69 | ||
| 127 | 70 | ## CI | |
| 128 | 71 | ||
| 129 | - | MNW CI runs self-hosted on astra (`server/deploy/run-ci.sh` — check, test, clippy, audit). GO, BB, and AF still have `.build.yml` manifests for builds.sr.ht (Arch Linux + Rust). Sourcehut (`https://sr.ht/~maxmj/`) remains active as a git mirror. MNW has a built-in git browser (G1, `git2`-based) that reads bare repos from disk. | |
| 130 | - | ||
| 131 | - | ## Infrastructure Diagrams | |
| 132 | - | ||
| 133 | - | Mermaid diagrams documenting the full MNW ecosystem live at `../_meta/diagrams/infra/`. See `index.md` for the table of contents, or open `viewer.html` in a browser to view all 52 diagrams rendered. Keep diagrams in sync when making infrastructure changes. | |
| 134 | - | ||
| 135 | - | ## Production Server (hetzner) | |
| 136 | - | ||
| 137 | - | Hetzner VPS, x86_64 Linux. Tailscale hostname: `alpha-west-1` (IP: `100.120.174.96`). Public IP: `5.78.144.244`. SSH as `root@100.120.174.96` (via Tailscale). | |
| 138 | - | ||
| 139 | - | ### Filesystem | |
| 140 | - | ||
| 141 | - | ``` | |
| 142 | - | /opt/makenotwork/ | |
| 143 | - | makenotwork # Application binary | |
| 144 | - | .env # Environment variables (secrets) | |
| 145 | - | static/ # CSS, JS, fonts, images | |
| 146 | - | error-pages/ # Custom error pages served by Caddy | |
| 147 | - | backup-db.sh # Database backup script | |
| 148 | - | src/ # Deployed source (for sqlx migrations) | |
| 149 | - | docs/ # Deployed docs (for /docs pages) | |
| 150 | - | ||
| 151 | - | /opt/git/ # Bare git repos for source browser | |
| 152 | - | makenotwork.git/ | |
| 153 | - | synckit-client.git/ | |
| 154 | - | ... | |
| 155 | - | ||
| 156 | - | /etc/caddy/Caddyfile # Caddy reverse proxy config | |
| 157 | - | /etc/systemd/system/makenotwork.service # systemd unit | |
| 158 | - | ``` | |
| 159 | - | ||
| 160 | - | ### Services | |
| 161 | - | ||
| 162 | - | - `makenotwork` — the application (systemd, runs as `makenotwork` user) | |
| 163 | - | - `caddy` — reverse proxy + TLS | |
| 164 | - | - `postgresql` — database (`makenotwork` db, `makenotwork` user) | |
| 165 | - | ||
| 166 | - | ### Deployment | |
| 167 | - | ||
| 168 | - | From the `MNW/server/` directory: | |
| 169 | - | ```sh | |
| 170 | - | ./deploy/deploy.sh # Full: build + config + binary + restart | |
| 171 | - | ./deploy/deploy.sh --quick # Build + binary + restart (no config upload) | |
| 172 | - | ./deploy/deploy.sh --config # Config files only (Caddyfile, systemd, error pages, static) | |
| 173 | - | ``` | |
| 174 | - | ||
| 175 | - | Cross-compiles with `cargo zigbuild` (requires `zig`, `cargo-zigbuild`, `x86_64-unknown-linux-gnu` target). | |
| 176 | - | ||
| 177 | - | ## Astra (dev/test server) | |
| 178 | - | ||
| 179 | - | General-purpose Linux box on Tailscale. SSH as `max@100.106.221.39`. | |
| 180 | - | ||
| 181 | - | - **OS:** Pop!_OS 24.04 LTS, aarch64 (96 cores, 125GB RAM, 929GB NVMe) | |
| 182 | - | - **Tailscale hostname:** `astra` (IP: `100.106.221.39`) | |
| 183 | - | - **PostgreSQL 16** — tuned: `max_connections=200`, `shared_buffers=4GB`, `work_mem=64MB`, `maintenance_work_mem=512MB` | |
| 184 | - | - **PG roles:** `postgres` (super), `max` (local, createdb), `mnw_staging` (createdb) | |
| 185 | - | - **Rust 1.94** — installed via rustup (`~/.cargo/bin/cargo`) | |
| 186 | - | - **Databases:** `postgres`, `devdb`, `makenotwork_staging` | |
| 187 | - | - **Staging copy:** `/home/max/staging/makenotwork/` (not a git repo, deployed copy) | |
| 188 | - | - **Environment:** `RUST_TEST_THREADS=8` (in `.bashrc` and `.profile`), `ulimit -n 65536` (in `/etc/security/limits.conf`) | |
| 189 | - | ||
| 190 | - | ### Running integration tests on astra | |
| 191 | - | ||
| 192 | - | ```sh | |
| 193 | - | # Using the test runner script (handles env vars and orphan cleanup): | |
| 194 | - | ssh 100.106.221.39 | |
| 195 | - | /home/max/staging/run-tests.sh # Run all | |
| 196 | - | /home/max/staging/run-tests.sh auth:: # Run filtered | |
| 197 | - | ||
| 198 | - | # Or manually: | |
| 199 | - | cd /home/max/staging/makenotwork | |
| 200 | - | TEST_DATABASE_URL="postgres:///postgres" cargo test --test integration -- --test-threads=8 | |
| 201 | - | ``` | |
| 202 | - | ||
| 203 | - | **Important:** Use `--test-threads=8` (or similar). The default parallelism (96 on astra) overwhelms PostgreSQL with too many simultaneous `CREATE DATABASE` calls. The `RUST_TEST_THREADS=8` env var handles this automatically. | |
| 204 | - | ||
| 205 | - | ### Cleaning up orphaned test databases | |
| 206 | - | ||
| 207 | - | Test databases (`mnw_test_*`) are orphaned if the test process is killed. The `run-tests.sh` script cleans these up automatically. Manual cleanup: | |
| 208 | - | ```sh | |
| 209 | - | psql -t -c "SELECT datname FROM pg_database WHERE datname LIKE 'mnw_test_%';" postgres \ | |
| 210 | - | | xargs -I{} psql -c "DROP DATABASE IF EXISTS \"{}\";" postgres | |
| 211 | - | ``` | |
| 212 | - | ||
| 213 | - | ## External Services | |
| 214 | - | ||
| 215 | - | - **Stripe Connect** — payments (live mode) | |
| 216 | - | - **Hetzner Object Storage** — S3-compatible file storage (fsn1 region) | |
| 217 | - | - **Postmark** — transactional email (password reset, verification, purchase receipts, notifications). Live mode. | |
| 218 | - | - **Cloudflare** — DNS, CDN, DDoS protection | |
| 219 | - | - **Fastmail** — business email (support@, legal@, max@) | |
| 220 | - | ||
| 221 | - | ## Testing | |
| 222 | - | ||
| 223 | - | ```sh | |
| 224 | - | cargo test # Unit tests (no DB needed) | |
| 225 | - | ||
| 226 | - | # Integration tests (need a running Postgres): | |
| 227 | - | TEST_DATABASE_URL="postgres://user:pass@host:5432/postgres" cargo test --test integration | |
| 228 | - | ``` | |
| 72 | + | MNW CI runs self-hosted on astra (`server/deploy/run-ci.sh`). MNW has a built-in git browser (G1, `git2`-based) at `/source/`. |
| @@ -0,0 +1,146 @@ | |||
| 1 | + | # Rollback Guide — Multithreaded | |
| 2 | + | ||
| 3 | + | ## Quick Rollback (Re-deploy Previous Binary) | |
| 4 | + | ||
| 5 | + | MT is deployed to two targets: Hetzner (production) and Astra (staging). Rollback means re-building a previous commit. | |
| 6 | + | ||
| 7 | + | ### Steps (Hetzner — production) | |
| 8 | + | ||
| 9 | + | 1. **Identify the last known-good commit:** | |
| 10 | + | ```bash | |
| 11 | + | cd MNW/multithreaded | |
| 12 | + | git log --oneline -10 | |
| 13 | + | ``` | |
| 14 | + | ||
| 15 | + | 2. **Check out and build the previous version:** | |
| 16 | + | ```bash | |
| 17 | + | git checkout <commit-hash> | |
| 18 | + | cargo zigbuild --release --target x86_64-unknown-linux-gnu | |
| 19 | + | ``` | |
| 20 | + | ||
| 21 | + | 3. **Deploy the rollback binary:** | |
| 22 | + | ```bash | |
| 23 | + | ssh root@100.120.174.96 "systemctl stop multithreaded || true" | |
| 24 | + | scp target/x86_64-unknown-linux-gnu/release/multithreaded root@100.120.174.96:/opt/multithreaded/multithreaded | |
| 25 | + | ssh root@100.120.174.96 "chmod +x /opt/multithreaded/multithreaded && chown multithreaded:multithreaded /opt/multithreaded/multithreaded" | |
| 26 | + | ssh root@100.120.174.96 "systemctl start multithreaded" | |
| 27 | + | ``` | |
| 28 | + | ||
| 29 | + | 4. **Verify:** | |
| 30 | + | ```bash | |
| 31 | + | ssh root@100.120.174.96 "systemctl status multithreaded --no-pager" | |
| 32 | + | ssh root@100.120.174.96 "curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1:3400" | |
| 33 | + | ``` | |
| 34 | + | ||
| 35 | + | 5. **Return to main branch:** | |
| 36 | + | ```bash | |
| 37 | + | git checkout main | |
| 38 | + | ``` | |
| 39 | + | ||
| 40 | + | ### Steps (Astra — staging) | |
| 41 | + | ||
| 42 | + | Astra builds natively (aarch64). The deploy script rsyncs source and builds on Astra. | |
| 43 | + | ||
| 44 | + | 1. **Check out the previous commit locally:** | |
| 45 | + | ```bash | |
| 46 | + | git checkout <commit-hash> | |
| 47 | + | ``` | |
| 48 | + | ||
| 49 | + | 2. **Re-deploy using the normal deploy script:** | |
| 50 | + | ```bash | |
| 51 | + | ./deploy/deploy.sh | |
| 52 | + | ``` | |
| 53 | + | This rsyncs the checked-out source to Astra, builds there, and restarts. | |
| 54 | + | ||
| 55 | + | 3. **Return to main branch:** | |
| 56 | + | ```bash | |
| 57 | + | git checkout main | |
| 58 | + | ``` | |
| 59 | + | ||
| 60 | + | ## Emergency Stop | |
| 61 | + | ||
| 62 | + | ```bash | |
| 63 | + | # Hetzner (production) | |
| 64 | + | ssh root@100.120.174.96 "systemctl stop multithreaded" | |
| 65 | + | ||
| 66 | + | # Astra (staging) | |
| 67 | + | ssh max@100.106.221.39 "sudo systemctl stop multithreaded" | |
| 68 | + | ``` | |
| 69 | + | ||
| 70 | + | To restart: | |
| 71 | + | ```bash | |
| 72 | + | ssh root@100.120.174.96 "systemctl start multithreaded" | |
| 73 | + | ssh max@100.106.221.39 "sudo systemctl start multithreaded" | |
| 74 | + | ``` | |
| 75 | + | ||
| 76 | + | ## Database Restore | |
| 77 | + | ||
| 78 | + | MT does not have automated backups. For a manual backup/restore: | |
| 79 | + | ||
| 80 | + | ### Create a manual backup | |
| 81 | + | ||
| 82 | + | ```bash | |
| 83 | + | # Hetzner | |
| 84 | + | ssh root@100.120.174.96 "sudo -u multithreaded pg_dump multithreaded | gzip > /opt/multithreaded/mt-backup-$(date +%Y%m%d).sql.gz" | |
| 85 | + | ||
| 86 | + | # Astra | |
| 87 | + | ssh max@100.106.221.39 "pg_dump multithreaded | gzip > /tmp/mt-backup-$(date +%Y%m%d).sql.gz" | |
| 88 | + | ``` | |
| 89 | + | ||
| 90 | + | ### Restore from backup | |
| 91 | + | ||
| 92 | + | 1. **Stop the application:** | |
| 93 | + | ```bash | |
| 94 | + | ssh root@100.120.174.96 "systemctl stop multithreaded" | |
| 95 | + | ``` | |
| 96 | + | ||
| 97 | + | 2. **Back up current state:** | |
| 98 | + | ```bash | |
| 99 | + | ssh root@100.120.174.96 "sudo -u multithreaded pg_dump multithreaded | gzip > /opt/multithreaded/mt-pre-restore.sql.gz" | |
| 100 | + | ``` | |
| 101 | + | ||
| 102 | + | 3. **Drop and recreate:** | |
| 103 | + | ```bash | |
| 104 | + | ssh root@100.120.174.96 "sudo -u postgres psql -c 'DROP DATABASE multithreaded;'" | |
| 105 | + | ssh root@100.120.174.96 "sudo -u postgres psql -c \"CREATE DATABASE multithreaded OWNER multithreaded;\"" | |
| 106 | + | ``` | |
| 107 | + | ||
| 108 | + | 4. **Restore:** | |
| 109 | + | ```bash | |
| 110 | + | ssh root@100.120.174.96 "gunzip -c /opt/multithreaded/mt-backup-YYYYMMDD.sql.gz | sudo -u multithreaded psql multithreaded" | |
| 111 | + | ``` | |
| 112 | + | ||
| 113 | + | 5. **Restart** (migrations auto-apply on boot): | |
| 114 | + | ```bash | |
| 115 | + | ssh root@100.120.174.96 "systemctl start multithreaded" | |
| 116 | + | ``` | |
| 117 | + | ||
| 118 | + | ## Service Architecture Reference | |
| 119 | + | ||
| 120 | + | ### Hetzner (production) | |
| 121 | + | ||
| 122 | + | - **Binary**: `/opt/multithreaded/multithreaded` | |
| 123 | + | - **Config**: `/opt/multithreaded/.env` | |
| 124 | + | - **Static**: `/opt/multithreaded/static/` | |
| 125 | + | - **Migrations**: `/opt/multithreaded/migrations/` | |
| 126 | + | - **Systemd unit**: `/etc/systemd/system/multithreaded.service` | |
| 127 | + | - **Logs**: `journalctl -u multithreaded -f` | |
| 128 | + | - **Port**: 127.0.0.1:3400 (Caddy reverse proxies `forums.makenot.work`) | |
| 129 | + | - **DB**: PostgreSQL `multithreaded` database, `multithreaded` user (peer auth) | |
| 130 | + | - **Domain**: `forums.makenot.work` (Cloudflare-proxied) | |
| 131 | + | ||
| 132 | + | ### Astra (staging) | |
| 133 | + | ||
| 134 | + | - **Binary**: `/opt/multithreaded/multithreaded` | |
| 135 | + | - **Config**: `/opt/multithreaded/.env` | |
| 136 | + | - **Source**: `~/src/multithreaded/` (rsynced from local) | |
| 137 | + | - **Shared deps**: `~/src/shared/` (docengine, tagtree, s3-storage) | |
| 138 | + | - **Port**: 0.0.0.0:3400 (direct access via Tailscale) | |
| 139 | + | - **DB**: PostgreSQL `multithreaded` database | |
| 140 | + | ||
| 141 | + | ### Common | |
| 142 | + | ||
| 143 | + | - **Restart policy**: `Restart=always`, `RestartSec=5` | |
| 144 | + | - **Depends on**: `postgresql.service` | |
| 145 | + | - **Migrations**: auto-applied on boot (`sqlx::migrate!()`) | |
| 146 | + | - **Memory limit**: 512M (`MemoryMax=512M`) |
| @@ -0,0 +1,127 @@ | |||
| 1 | + | # Rollback Guide — MNW Server | |
| 2 | + | ||
| 3 | + | ## Quick Rollback (Re-deploy Previous Binary) | |
| 4 | + | ||
| 5 | + | The previous binary is overwritten during deploy, so rollback means re-building a previous commit and deploying it. | |
| 6 | + | ||
| 7 | + | ### Steps | |
| 8 | + | ||
| 9 | + | 1. **Identify the last known-good commit:** | |
| 10 | + | ```bash | |
| 11 | + | cd MNW/server | |
| 12 | + | git log --oneline -10 | |
| 13 | + | ``` | |
| 14 | + | ||
| 15 | + | 2. **Check out and build the previous version:** | |
| 16 | + | ```bash | |
| 17 | + | git checkout <commit-hash> | |
| 18 | + | cargo zigbuild --release --target x86_64-unknown-linux-gnu | |
| 19 | + | ``` | |
| 20 | + | ||
| 21 | + | 3. **Deploy the rollback binary:** | |
| 22 | + | ```bash | |
| 23 | + | ssh root@100.120.174.96 "systemctl stop makenotwork || true" | |
| 24 | + | scp target/x86_64-unknown-linux-gnu/release/makenotwork root@100.120.174.96:/opt/makenotwork/makenotwork | |
| 25 | + | ssh root@100.120.174.96 "chmod +x /opt/makenotwork/makenotwork" | |
| 26 | + | ssh root@100.120.174.96 "systemctl start makenotwork" | |
| 27 | + | ``` | |
| 28 | + | ||
| 29 | + | 4. **Verify:** | |
| 30 | + | ```bash | |
| 31 | + | ssh root@100.120.174.96 "systemctl status makenotwork --no-pager" | |
| 32 | + | ssh root@100.120.174.96 "curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1:3000" | |
| 33 | + | ``` | |
| 34 | + | ||
| 35 | + | 5. **Return to main branch:** | |
| 36 | + | ```bash | |
| 37 | + | git checkout main | |
| 38 | + | ``` | |
| 39 | + | ||
| 40 | + | ## Emergency Stop | |
| 41 | + | ||
| 42 | + | Stop the application immediately. Caddy will serve the 502 error page (auto-retries every 10 seconds). | |
| 43 | + | ||
| 44 | + | ```bash | |
| 45 | + | ssh root@100.120.174.96 "systemctl stop makenotwork" | |
| 46 | + | ``` | |
| 47 | + | ||
| 48 | + | To restart: | |
| 49 | + | ```bash | |
| 50 | + | ssh root@100.120.174.96 "systemctl start makenotwork" | |
| 51 | + | ``` | |
| 52 | + | ||
| 53 | + | ## Database Restore | |
| 54 | + | ||
| 55 | + | Backups are created daily at 03:00 UTC by cron (`/opt/makenotwork/backup-db.sh`). Stored at `/opt/makenotwork/backups/`, 30-day retention. | |
| 56 | + | ||
| 57 | + | ### Restore from backup | |
| 58 | + | ||
| 59 | + | 1. **Stop the application:** | |
| 60 | + | ```bash | |
| 61 | + | ssh root@100.120.174.96 "systemctl stop makenotwork" | |
| 62 | + | ``` | |
| 63 | + | ||
| 64 | + | 2. **List available backups:** | |
| 65 | + | ```bash | |
| 66 | + | ssh root@100.120.174.96 "ls -lh /opt/makenotwork/backups/" | |
| 67 | + | ``` | |
| 68 | + | ||
| 69 | + | 3. **Create a backup of the current (broken) state first:** | |
| 70 | + | ```bash | |
| 71 | + | ssh root@100.120.174.96 "sudo -u makenotwork pg_dump makenotwork | gzip > /opt/makenotwork/backups/makenotwork-pre-restore.sql.gz" | |
| 72 | + | ``` | |
| 73 | + | ||
| 74 | + | 4. **Drop and recreate the database:** | |
| 75 | + | ```bash | |
| 76 | + | ssh root@100.120.174.96 "sudo -u postgres psql -c 'DROP DATABASE makenotwork;'" | |
| 77 | + | ssh root@100.120.174.96 "sudo -u postgres psql -c \"CREATE DATABASE makenotwork OWNER makenotwork;\"" | |
| 78 | + | ``` | |
| 79 | + | ||
| 80 | + | 5. **Restore from backup:** | |
| 81 | + | ```bash | |
| 82 | + | ssh root@100.120.174.96 "gunzip -c /opt/makenotwork/backups/makenotwork-YYYYMMDD-HHMMSS.sql.gz | sudo -u makenotwork psql makenotwork" | |
| 83 | + | ``` | |
| 84 | + | ||
| 85 | + | 6. **Restart the application** (migrations will run on boot and apply any missing ones): | |
| 86 | + | ```bash | |
| 87 | + | ssh root@100.120.174.96 "systemctl start makenotwork" | |
| 88 | + | ``` | |
| 89 | + | ||
| 90 | + | 7. **Verify:** | |
| 91 | + | ```bash | |
| 92 | + | ssh root@100.120.174.96 "systemctl status makenotwork --no-pager" | |
| 93 | + | ssh root@100.120.174.96 "curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1:3000" | |
| 94 | + | ``` | |
| 95 | + | ||
| 96 | + | ### Notes on DB Restore | |
| 97 | + | ||
| 98 | + | - Migrations are auto-applied on boot via sqlx. If you restore an older backup and the current binary has newer migrations, they'll run automatically. | |
| 99 | + | - If a migration is incompatible with the restored data, you'll need to also rollback the binary (see Quick Rollback above). | |
| 100 | + | - Data created between the backup and the restore will be lost. There's no WAL archiving — only daily `pg_dump`. | |
| 101 | + | ||
| 102 | + | ## Migration Rollback | |
| 103 | + | ||
| 104 | + | Migrations are additive (new tables, new columns with defaults). There's no built-in `down` migration. If a migration causes issues: | |
| 105 | + | ||
| 106 | + | 1. Manually write a reversal SQL script | |
| 107 | + | 2. Apply it: `ssh root@100.120.174.96 "sudo -u makenotwork psql makenotwork < /tmp/rollback.sql"` | |
| 108 | + | 3. Delete the migration row from `_sqlx_migrations` so it doesn't conflict: | |
| 109 | + | ```sql | |
| 110 | + | DELETE FROM _sqlx_migrations WHERE version = <migration_number>; | |
| 111 | + | ``` | |
| 112 | + | ||
| 113 | + | ## Service Architecture Reference | |
| 114 | + | ||
| 115 | + | - **Binary**: `/opt/makenotwork/makenotwork` | |
| 116 | + | - **Config**: `/opt/makenotwork/.env` | |
| 117 | + | - **Static**: `/opt/makenotwork/static/` | |
| 118 | + | - **Docs**: `/opt/makenotwork/docs/` | |
| 119 | + | - **Backups**: `/opt/makenotwork/backups/` | |
| 120 | + | - **Systemd unit**: `/etc/systemd/system/makenotwork.service` | |
| 121 | + | - **Caddy config**: `/etc/caddy/Caddyfile` | |
| 122 | + | - **Error pages**: `/opt/makenotwork/error-pages/` | |
| 123 | + | - **Git repos**: `/opt/git/` | |
| 124 | + | - **Logs**: `journalctl -u makenotwork -f` | |
| 125 | + | - **Port**: 127.0.0.1:3000 (Caddy reverse proxies from 443) | |
| 126 | + | - **DB**: PostgreSQL `makenotwork` database, `makenotwork` user (peer auth) | |
| 127 | + | - **Restart policy**: `Restart=always`, `RestartSec=5` |
| @@ -0,0 +1,512 @@ | |||
| 1 | + | # Schema — MNW Server | |
| 2 | + | ||
| 3 | + | PostgreSQL database. 57 migrations in `migrations/`, auto-applied on boot via sqlx. Extension: `pg_trgm` (trigram fuzzy search). | |
| 4 | + | ||
| 5 | + | ## Domain Map | |
| 6 | + | ||
| 7 | + | | Domain | Tables | Purpose | | |
| 8 | + | |--------|--------|---------| | |
| 9 | + | | Users & Auth | 6 | Accounts, passkeys, sessions, 2FA, login tokens | | |
| 10 | + | | Projects & Content | 8 | Creator projects, items, versions, chapters, insertions, sections, bundles | | |
| 11 | + | | Tags & Taxonomy | 4 | Hierarchical tags, item tagging, platform labels | | |
| 12 | + | | Commerce | 7 | Transactions, subscriptions, promo codes, license keys | | |
| 13 | + | | Creator Tiers | 2 | Platform subscription tiers (Basic/Small/Big/Streaming) | | |
| 14 | + | | Email & Mailing | 4 | Mailing lists, subscribers, suppressions, signups | | |
| 15 | + | | Social | 3 | Follows, blog posts, custom links | | |
| 16 | + | | Collections | 2 | User-curated item lists | | |
| 17 | + | | SyncKit | 5 | Cloud sync apps, devices, keys, changelog, blobs | | |
| 18 | + | | Git | 6 | Repos, SSH keys, issues, comments, labels | | |
| 19 | + | | OTA Updates | 4 | Releases, artifacts, build configs, build runs | | |
| 20 | + | | Custom Domains | 1 | Creator vanity domains | | |
| 21 | + | | OAuth | 1 | PKCE authorization codes | | |
| 22 | + | | Waitlist & Invites | 3 | Creator waves, waitlist, invite codes | | |
| 23 | + | | Content Security | 2 | Download fingerprints, streaming sessions | | |
| 24 | + | | Admin | 1 | Abuse reports | | |
| 25 | + | | Media | 1 | User media library (images for markdown) | | |
| 26 | + | | Import | 1 | Bulk import jobs (Patreon, Ko-fi, Gumroad) | | |
| 27 | + | | Sessions | 1 | HTTP sessions (tower-sessions) | | |
| 28 | + | ||
| 29 | + | --- | |
| 30 | + | ||
| 31 | + | ## Users & Authentication | |
| 32 | + | ||
| 33 | + | ### users | |
| 34 | + | Core accounts. Every user has one row; creator features are gated by `can_create_projects`. | |
| 35 | + | ||
| 36 | + | | Column | Type | Notes | | |
| 37 | + | |--------|------|-------| | |
| 38 | + | | id | UUID PK | | | |
| 39 | + | | username | TEXT UNIQUE | URL slug (`/@username`) | | |
| 40 | + | | email | TEXT UNIQUE | | | |
| 41 | + | | display_name | TEXT | | | |
| 42 | + | | password_hash | TEXT | argon2 | | |
| 43 | + | | email_verified | BOOL | | | |
| 44 | + | | totp_secret / totp_enabled | TEXT / BOOL | 2FA | | |
| 45 | + | | failed_login_attempts | INT | Resets on success | | |
| 46 | + | | locked_until | TIMESTAMPTZ | Lockout after 5 failures | | |
| 47 | + | | stripe_account_id | TEXT | Stripe Connect account | | |
| 48 | + | | stripe_onboarding_complete | BOOL | | | |
| 49 | + | | can_create_projects | BOOL | Creator gate | | |
| 50 | + | | creator_tier | TEXT | 'basic', 'small_files', 'big_files', 'streaming' | | |
| 51 | + | | storage_used_bytes | BIGINT | Computed from versions + insertions | | |
| 52 | + | | max_file_override_bytes | BIGINT | Per-user override | | |
| 53 | + | | grandfathered_until | TIMESTAMPTZ | Grace period for existing creators | | |
| 54 | + | | suspended_at | TIMESTAMPTZ | Null = active | | |
| 55 | + | | notify_sale / notify_follower / notify_release / notify_issues | BOOL | Email prefs | | |
| 56 | + | ||
| 57 | + | **Indexes:** email, username, email_verified, stripe_account — all B-tree. | |
| 58 | + | **Trigger:** `update_users_updated_at` — auto-sets `updated_at`. | |
| 59 | + | ||
| 60 | + | ### user_passkeys | |
| 61 | + | WebAuthn credentials for passwordless login. | |
| 62 | + | ||
| 63 | + | | Column | Type | Notes | | |
| 64 | + | |--------|------|-------| | |
| 65 | + | | id | UUID PK | | | |
| 66 | + | | user_id | UUID FK → users CASCADE | | | |
| 67 | + | | credential_json | JSONB | WebAuthn credential blob | | |
| 68 | + | | credential_id | BYTEA UNIQUE | Lookup key | | |
| 69 | + | | name | TEXT | User-assigned label | | |
| 70 | + | ||
| 71 | + | ### login_tokens | |
| 72 | + | Single-use email login links. | |
| 73 | + | ||
| 74 | + | - **FK:** user_id → users CASCADE | |
| 75 | + | - **Key columns:** token_hash, expires_at, used_at | |
| 76 | + | - **Indexed on:** user_id, expires_at | |
| 77 | + | ||
| 78 | + | ### user_sessions | |
| 79 | + | Active login sessions. Tracks last activity, UA, IP for "active sessions" UI. | |
| 80 | + | ||
| 81 | + | - **FK:** user_id → users CASCADE | |
| 82 | + | - **Indexed on:** user_id | |
| 83 | + | ||
| 84 | + | ### backup_codes | |
| 85 | + | 2FA recovery codes (hashed). Marked with `used_at` when consumed. | |
| 86 | + | ||
| 87 | + | - **FK:** user_id → users CASCADE | |
| 88 | + | ||
| 89 | + | ### tower_sessions.session | |
| 90 | + | HTTP session storage (tower-sessions-sqlx-store). Schema `tower_sessions`, PK is TEXT `id`, stores BYTEA `data` with `expiry_date`. | |
| 91 | + | ||
| 92 | + | --- | |
| 93 | + | ||
| 94 | + | ## Projects & Content | |
| 95 | + | ||
| 96 | + | ### projects | |
| 97 | + | Creator projects — music releases, software, podcasts, books, etc. | |
| 98 | + | ||
| 99 | + | | Column | Type | Notes | | |
| 100 | + | |--------|------|-------| | |
| 101 | + | | id | UUID PK | | | |
| 102 | + | | user_id | UUID FK → users CASCADE | | | |
| 103 | + | | slug | TEXT | URL path segment | | |
| 104 | + | | title | TEXT | | | |
| 105 | + | | project_type | TEXT | 'music', 'software', 'podcast', etc. | | |
| 106 | + | | is_public | BOOL | | | |
| 107 | + | | category_id | UUID FK → project_categories SET NULL | | | |
| 108 | + | | mt_community_id | UUID | Links to Multithreaded forum | | |
| 109 | + | | features | TEXT[] | Feature flags per project | | |
| 110 | + | ||
| 111 | + | **Constraint:** UNIQUE(user_id, slug). | |
| 112 | + | **Indexes:** user_id, is_public, category_id, title trigram (GIN), description trigram (GIN). | |
| 113 | + | **Trigger:** `update_projects_updated_at`. | |
| 114 | + | ||
| 115 | + | ### items | |
| 116 | + | Products/content within projects. The central commerce entity — holds pricing, audio, text, licensing. | |
| 117 | + | ||
| 118 | + | | Column | Type | Notes | | |
| 119 | + | |--------|------|-------| | |
| 120 | + | | id | UUID PK | | | |
| 121 | + | | project_id | UUID FK → projects CASCADE | | | |
| 122 | + | | title / slug | TEXT | UNIQUE(project_id, slug) | | |
| 123 | + | | item_type | TEXT | 'article', 'audio', 'download', 'video', etc. | | |
| 124 | + | | price_cents | INT | 0 = free. CHECK >= 0 | | |
| 125 | + | | pwyw_enabled | BOOL | Pay what you want | | |
| 126 | + | | pwyw_min_cents | INT | Floor for PWYW | | |
| 127 | + | | body / word_count / reading_time_minutes | TEXT / INT / INT | Text content | | |
| 128 | + | | audio_url / audio_s3_key / duration_seconds | TEXT / TEXT / FLOAT | Audio content | | |
| 129 | + | | video_s3_key / video_duration_seconds | TEXT / FLOAT | Video content | | |
| 130 | + | | enable_license_keys | BOOL | DRM gate | | |
| 131 | + | | custom_license_text | TEXT | License shown on download | | |
| 132 | + | | sales_count / play_count / download_count | INT | Denormalized counters | | |
| 133 | + | | web_only | BOOL | Prevents download (streaming only) | | |
| 134 | + | ||
| 135 | + | **Indexes:** project_id, is_public, sales_count, tsvector search (title+description+body), title trigram, desc trigram, (project_id, slug). | |
| 136 | + | **Trigger:** `update_items_updated_at`. | |
| 137 | + | ||
| 138 | + | ### versions | |
| 139 | + | Downloadable file versions per item (software releases, audio stems). | |
| 140 | + | ||
| 141 | + | - **FK:** item_id → items CASCADE | |
| 142 | + | - **Key columns:** version_number, file_url, s3_key, file_size_bytes, is_current, download_count | |
| 143 | + | - **Constraint:** UNIQUE WHERE is_current = true (only one current version per item) | |
| 144 | + | ||
| 145 | + | ### chapters | |
| 146 | + | Audio/podcast chapter markers. Sorted by `start_seconds`. | |
| 147 | + | ||
| 148 | + | - **FK:** item_id → items CASCADE | |
| 149 | + | ||
| 150 | + | ### content_insertions | |
| 151 | + | Reusable audio clips (ads, intros, outros) uploaded by creators. | |
| 152 | + | ||
| 153 | + | - **FK:** user_id → users CASCADE | |
| 154 | + | - **Key columns:** title, media_type, storage_key, duration_ms, file_size | |
| 155 | + | ||
| 156 | + | ### content_insertion_placements | |
| 157 | + | Where insertions attach to items. Position is 'pre_roll', 'mid_roll', or 'post_roll'. | |
| 158 | + | ||
| 159 | + | - **FK:** item_id → items CASCADE, insertion_id → content_insertions CASCADE | |
| 160 | + | - **Constraint:** UNIQUE(item_id, insertion_id, position, offset_ms) | |
| 161 | + | ||
| 162 | + | ### item_sections | |
| 163 | + | Tabbed content blocks within items (e.g., "Ingredients", "Instructions", "Changelog"). | |
| 164 | + | ||
| 165 | + | - **FK:** item_id → items CASCADE | |
| 166 | + | - **Constraint:** UNIQUE(item_id, slug) | |
| 167 | + | ||
| 168 | + | ### bundle_items | |
| 169 | + | Associates items into bundle-type items. CHECK(bundle_id != item_id) prevents self-reference. | |
| 170 | + | ||
| 171 | + | - **PK:** (bundle_id, item_id) | |
| 172 | + | - **FK:** both → items CASCADE | |
| 173 | + | ||
| 174 | + | --- | |
| 175 | + | ||
| 176 | + | ## Tags & Taxonomy | |
| 177 | + | ||
| 178 | + | ### tags | |
| 179 | + | Hierarchical tag system. `path` uses dot-notation for materialized paths (e.g., `music.electronic.ambient`). | |
| 180 | + | ||
| 181 | + | | Column | Type | Notes | | |
| 182 | + | |--------|------|-------| | |
| 183 | + | | id | UUID PK | | | |
| 184 | + | | parent_id | UUID FK → tags CASCADE | Self-referential hierarchy | | |
| 185 | + | | name | TEXT | Display name | | |
| 186 | + | | slug | TEXT UNIQUE | URL-safe | | |
| 187 | + | | path | TEXT | Materialized path (dot-notation) | | |
| 188 | + | ||
| 189 | + | **Indexes:** parent_id, slug, name trigram (GIN), path (prefix queries). | |
| 190 | + | ||
| 191 | + | ### item_tags | |
| 192 | + | Many-to-many. `is_primary` marks the main tag for an item (UNIQUE WHERE is_primary). | |
| 193 | + | ||
| 194 | + | - **PK:** (item_id, tag_id), both CASCADE | |
| 195 | + | ||
| 196 | + | ### labels | |
| 197 | + | Platform-curated promises (e.g., "DRM-free", "Lossless audio"). Includes definition, examples, and non-examples. | |
| 198 | + | ||
| 199 | + | - **Key columns:** slug UNIQUE, display_name, definition, examples, nonexamples | |
| 200 | + | ||
| 201 | + | ### project_labels | |
| 202 | + | Projects adopt platform labels (creator commits to the promise). | |
| 203 | + | ||
| 204 | + | - **PK:** (project_id, label_id), both CASCADE | |
| 205 | + | ||
| 206 | + | --- | |
| 207 | + | ||
| 208 | + | ## Commerce & Payments | |
| 209 | + | ||
| 210 | + | ### transactions | |
| 211 | + | One-off purchases. Status lifecycle: pending → completed / failed / refunded. | |
| 212 | + | ||
| 213 | + | | Column | Type | Notes | | |
| 214 | + | |--------|------|-------| | |
| 215 | + | | id | UUID PK | | | |
| 216 | + | | buyer_id | UUID FK → users CASCADE | | | |
| 217 | + | | seller_id | UUID FK → users SET NULL | Preserved if seller deletes account | | |
| 218 | + | | item_id | UUID FK → items SET NULL | Preserved if item deletes | | |
| 219 | + | | amount_cents | INT | CHECK >= 0 | | |
| 220 | + | | platform_fee_cents | INT | Always 0 (0% fee model) | | |
| 221 | + | | stripe_checkout_session_id | TEXT | Links to Stripe | | |
| 222 | + | | status | TEXT | 'pending', 'completed', 'failed', 'refunded' | | |
| 223 | + | | item_title / seller_username | TEXT | Denormalized for receipt display | | |
| 224 | + | ||
| 225 | + | **Constraint:** UNIQUE(buyer_id, item_id) WHERE status = 'completed' — prevents double purchase. | |
| 226 | + | **Indexes:** buyer_id, seller_id, item_id, status, stripe_session. | |
| 227 | + | ||
| 228 | + | ### subscription_tiers | |
| 229 | + | Per-project recurring tiers. Each tier has a Stripe product+price. | |
| 230 | + | ||
| 231 | + | - **FK:** project_id → projects CASCADE | |
| 232 | + | - **Key columns:** name, price_cents, stripe_product_id, stripe_price_id, is_active | |
| 233 | + | ||
| 234 | + | ### subscriptions | |
| 235 | + | Active subscriber records. UNIQUE(subscriber_id, project_id) WHERE status = 'active'. | |
| 236 | + | ||
| 237 | + | - **FK:** subscriber_id → users CASCADE, tier_id → subscription_tiers RESTRICT, project_id → projects CASCADE | |
| 238 | + | - **Note:** tier_id uses RESTRICT — cannot delete a tier that has active subscribers | |
| 239 | + | ||
| 240 | + | ### subscription_events | |
| 241 | + | Webhook events from Stripe. Keyed by `stripe_event_id` (UNIQUE) for idempotency. | |
| 242 | + | ||
| 243 | + | - **FK:** subscription_id → subscriptions SET NULL | |
| 244 | + | ||
| 245 | + | ### promo_codes | |
| 246 | + | Unified discount/free-access/free-trial codes. Purpose-specific CHECK constraints enforce valid field combinations. | |
| 247 | + | ||
| 248 | + | - **FK:** creator_id → users CASCADE; item_id, project_id, tier_id all → SET NULL on target delete | |
| 249 | + | - **Key columns:** code, code_purpose, discount_type, discount_value, trial_days, max_uses, use_count | |
| 250 | + | - **Constraint:** UNIQUE(creator_id, code) | |
| 251 | + | ||
| 252 | + | ### license_keys | |
| 253 | + | DRM keys for download-limited items. Tracks activation count vs max_activations. | |
| 254 | + | ||
| 255 | + | - **FK:** item_id → items CASCADE, owner_id → users CASCADE, transaction_id → transactions SET NULL | |
| 256 | + | - **Key columns:** key_code UNIQUE, max_activations, activation_count, revoked_at | |
| 257 | + | ||
| 258 | + | ### license_activations | |
| 259 | + | Per-machine activations. UNIQUE(license_key_id, machine_id) prevents double-activate on same machine. | |
| 260 | + | ||
| 261 | + | - **FK:** license_key_id → license_keys CASCADE | |
| 262 | + | ||
| 263 | + | --- | |
| 264 | + | ||
| 265 | + | ## Creator Tiers | |
| 266 | + | ||
| 267 | + | ### creator_subscriptions | |
| 268 | + | Platform subscription for creators (Basic $10, Small Files $20, Big Files $30, Streaming $40). One row per creator. | |
| 269 | + | ||
| 270 | + | - **FK:** user_id → users CASCADE (UNIQUE) | |
| 271 | + | - **Key columns:** tier, status, stripe_subscription_id UNIQUE, grace_enforced_at | |
| 272 | + | ||
| 273 | + | ### fan_plus_subscriptions | |
| 274 | + | Fan+ consumer subscription (discoverability + collection features). One row per user. | |
| 275 | + | ||
| 276 | + | - **FK:** user_id → users CASCADE (UNIQUE) | |
| 277 | + | - **Key columns:** status, stripe_subscription_id UNIQUE | |
| 278 | + | ||
| 279 | + | --- | |
| 280 | + | ||
| 281 | + | ## Email & Mailing Lists | |
| 282 | + | ||
| 283 | + | ### mailing_lists | |
| 284 | + | Per-project lists. Types: 'content' (new releases), 'devlog' (development updates), 'patches' (software patches). | |
| 285 | + | ||
| 286 | + | - **FK:** project_id → projects CASCADE | |
| 287 | + | - **Constraint:** UNIQUE(project_id, list_type) | |
| 288 | + | ||
| 289 | + | ### mailing_list_subscribers | |
| 290 | + | Supports both registered users and email-only subscribers. CHECK(user_id NOT NULL OR email NOT NULL). | |
| 291 | + | ||
| 292 | + | - **FK:** list_id → mailing_lists CASCADE, user_id → users CASCADE (nullable) | |
| 293 | + | - **Constraints:** UNIQUE(list_id, user_id), UNIQUE(list_id, email) WHERE email NOT NULL | |
| 294 | + | ||
| 295 | + | ### email_suppressions | |
| 296 | + | Hard bounces and spam complaints. Prevents sending to known-bad addresses. | |
| 297 | + | ||
| 298 | + | - **Key columns:** email UNIQUE (case-insensitive), reason ('HardBounce', 'SpamComplaint') | |
| 299 | + | ||
| 300 | + | ### email_signups | |
| 301 | + | Landing page "notify me" signups (pre-launch or feature waitlist). | |
| 302 | + | ||
| 303 | + | - **Key columns:** email UNIQUE, source | |
| 304 | + | ||
| 305 | + | --- | |
| 306 | + | ||
| 307 | + | ## Social & Community | |
| 308 | + | ||
| 309 | + | ### follows | |
| 310 | + | Polymorphic follow system — users can follow users, projects, or tags. | |
| 311 | + | ||
| 312 | + | - **FK:** follower_id → users CASCADE | |
| 313 | + | - **Key columns:** target_type ('user', 'project', 'tag'), target_id | |
| 314 | + | - **Constraint:** UNIQUE(follower_id, target_type, target_id) | |
| 315 | + | ||
| 316 | + | ### blog_posts | |
| 317 | + | Project-level blog posts (devlogs, announcements). Markdown source + rendered HTML. | |
| 318 | + | ||
| 319 | + | - **FK:** project_id → projects CASCADE, author_id → users (no cascade) | |
| 320 | + | - **Key columns:** title, slug, body_markdown, body_html, published_at, mt_thread_id | |
| 321 | + | - **Constraint:** UNIQUE(project_id, slug) | |
| 322 | + | ||
| 323 | + | ### custom_links | |
| 324 | + | Creator profile links (social, merch, website). Ordered by sort_order. | |
| 325 | + | ||
| 326 | + | - **FK:** user_id → users CASCADE | |
| 327 | + | - **Trigger:** `update_custom_links_updated_at` | |
| 328 | + | ||
| 329 | + | --- | |
| 330 | + | ||
| 331 | + | ## Collections | |
| 332 | + | ||
| 333 | + | ### collections | |
| 334 | + | User-curated lists (playlists, reading lists, favorites). | |
| 335 | + | ||
| 336 | + | - **FK:** user_id → users CASCADE | |
| 337 | + | - **Constraint:** UNIQUE(user_id, slug) | |
| 338 | + | ||
| 339 | + | ### collection_items | |
| 340 | + | Items in collections. Ordered by `position`. | |
| 341 | + | ||
| 342 | + | - **PK:** (collection_id, item_id), both CASCADE | |
| 343 | + | ||
| 344 | + | --- | |
| 345 | + | ||
| 346 | + | ## SyncKit | |
| 347 | + | ||
| 348 | + | ### sync_apps | |
| 349 | + | Registered SyncKit applications (GO, BB, AF, third-party). | |
| 350 | + | ||
| 351 | + | - **FK:** creator_id → users CASCADE; project_id → projects SET NULL, item_id → items SET NULL | |
| 352 | + | - **Key columns:** name, api_key UNIQUE, slug, is_active, redirect_uris TEXT[] | |
| 353 | + | ||
| 354 | + | ### sync_devices | |
| 355 | + | User devices per app. Last-seen tracking for device management UI. | |
| 356 | + | ||
| 357 | + | - **FK:** app_id → sync_apps CASCADE, user_id → users CASCADE | |
| 358 | + | - **Constraint:** UNIQUE(app_id, user_id, device_name) | |
| 359 | + | ||
| 360 | + | ### sync_keys | |
| 361 | + | E2E encryption keys per app/user pair. Key rotation via `key_version`. | |
| 362 | + | ||
| 363 | + | - **PK:** (app_id, user_id), both CASCADE | |
| 364 | + | - **Key columns:** key_version, encrypted_key | |
| 365 | + | ||
| 366 | + | ### sync_log | |
| 367 | + | Changelog of data operations. Sequential `seq` (BIGSERIAL) enables cursor-based pull. | |
| 368 | + | ||
| 369 | + | - **FK:** app_id, user_id, device_id — all CASCADE | |
| 370 | + | - **Key columns:** table_name, operation ('INSERT'/'UPDATE'/'DELETE'), row_id, data (JSONB), client_timestamp | |
| 371 | + | - **Index:** (app_id, user_id, seq) — the primary pull query path | |
| 372 | + | ||
| 373 | + | ### sync_blobs | |
| 374 | + | File blobs for SyncKit (content-hashed dedup). Used when `sync_files=true` on a VFS. | |
| 375 | + | ||
| 376 | + | - **FK:** app_id, user_id — both CASCADE | |
| 377 | + | - **Constraint:** UNIQUE(app_id, user_id, hash) | |
| 378 | + | - **Key columns:** hash, s3_key, size_bytes | |
| 379 | + | ||
| 380 | + | --- | |
| 381 | + | ||
| 382 | + | ## Git Integration | |
| 383 | + | ||
| 384 | + | ### git_repos | |
| 385 | + | Bare git repositories. Displayed via the `/source/` browser (G1, git2-based). | |
| 386 | + | ||
| 387 | + | - **FK:** user_id → users CASCADE, project_id → projects SET NULL | |
| 388 | + | - **Constraint:** UNIQUE(user_id, name) | |
| 389 | + | ||
| 390 | + | ### ssh_keys | |
| 391 | + | SSH public keys for git push access. | |
| 392 | + | ||
| 393 | + | - **FK:** user_id → users CASCADE | |
| 394 | + | - **Constraint:** UNIQUE(user_id, fingerprint) | |
| 395 | + | ||
| 396 | + | ### issues | |
| 397 | + | Lightweight issue tracker per repo. Sequential `number` per repo. | |
| 398 | + | ||
| 399 | + | - **FK:** repo_id → git_repos CASCADE, author_user_id → users CASCADE | |
| 400 | + | - **Constraint:** UNIQUE(repo_id, number) | |
| 401 | + | - **Indexed on:** (repo_id, status), author | |
| 402 | + | ||
| 403 | + | ### issue_comments | |
| 404 | + | - **FK:** issue_id → issues CASCADE, author_user_id → users CASCADE | |
| 405 | + | ||
| 406 | + | ### issue_labels | |
| 407 | + | Per-repo label definitions with color. | |
| 408 | + | ||
| 409 | + | - **FK:** repo_id → git_repos CASCADE | |
| 410 | + | - **Constraint:** UNIQUE(repo_id, name) | |
| 411 | + | ||
| 412 | + | ### issue_label_assignments | |
| 413 | + | - **PK:** (issue_id, label_id), both CASCADE | |
| 414 | + | ||
| 415 | + | --- | |
| 416 | + | ||
| 417 | + | ## OTA Updates | |
| 418 | + | ||
| 419 | + | ### ota_releases | |
| 420 | + | App versions published for over-the-air updates (Tauri-compatible protocol). | |
| 421 | + | ||
| 422 | + | - **FK:** app_id → sync_apps CASCADE | |
| 423 | + | - **Constraint:** UNIQUE(app_id, version) | |
| 424 | + | ||
| 425 | + | ### ota_artifacts | |
| 426 | + | Binary artifacts per release. One per target/arch combination. | |
| 427 | + | ||
| 428 | + | - **FK:** release_id → ota_releases CASCADE | |
| 429 | + | - **Constraint:** UNIQUE(release_id, target, arch) | |
| 430 | + | - **Key columns:** s3_key, file_size | |
| 431 | + | ||
| 432 | + | ### ota_build_configs | |
| 433 | + | Automated build configuration per app. | |
| 434 | + | ||
| 435 | + | - **FK:** app_id → sync_apps CASCADE (UNIQUE), repo_id → git_repos CASCADE | |
| 436 | + | - **Key columns:** build_command, artifact_path, signing_key_path, targets TEXT[], enabled | |
| 437 | + | ||
| 438 | + | ### ota_builds | |
| 439 | + | Individual build runs. Status lifecycle: pending → building → succeeded / failed. | |
| 440 | + | ||
| 441 | + | - **FK:** config_id → ota_build_configs CASCADE, release_id → ota_releases SET NULL | |
| 442 | + | ||
| 443 | + | --- | |
| 444 | + | ||
| 445 | + | ## Remaining Tables | |
| 446 | + | ||
| 447 | + | ### custom_domains | |
| 448 | + | Creator vanity domains. Verified via DNS TXT record. | |
| 449 | + | ||
| 450 | + | - **FK:** user_id → users CASCADE | |
| 451 | + | - **Key columns:** domain UNIQUE, verified, verification_token | |
| 452 | + | ||
| 453 | + | ### oauth_authorization_codes | |
| 454 | + | OAuth PKCE codes for SyncKit SDK clients. Short-lived (5 min), single-use. | |
| 455 | + | ||
| 456 | + | - **FK:** app_id → sync_apps CASCADE, user_id → users CASCADE | |
| 457 | + | - **Key columns:** code UNIQUE, code_challenge, redirect_uri, expires_at, used_at | |
| 458 | + | ||
| 459 | + | ### creator_waves / creator_waitlist / invite_codes | |
| 460 | + | Creator onboarding pipeline: waves (batches), waitlist (applications), invite codes (referrals). | |
| 461 | + | ||
| 462 | + | - creator_waves: wave_number UNIQUE | |
| 463 | + | - creator_waitlist: user_id → users CASCADE (UNIQUE), wave_id → creator_waves SET NULL | |
| 464 | + | - invite_codes: creator_id → users CASCADE, code UNIQUE | |
| 465 | + | ||
| 466 | + | ### download_fingerprints | |
| 467 | + | Watermark tracking for paid downloads. Records fingerprint ID, IP, UA per download. | |
| 468 | + | ||
| 469 | + | ### streaming_sessions | |
| 470 | + | Active streaming sessions with IP binding and concurrency tracking. | |
| 471 | + | ||
| 472 | + | ### reports | |
| 473 | + | User abuse reports. Status: open → resolved/dismissed. | |
| 474 | + | ||
| 475 | + | ### media_files | |
| 476 | + | User media library for embedding in markdown content. S3-backed. | |
| 477 | + | ||
| 478 | + | - **FK:** user_id → users CASCADE | |
| 479 | + | - **Constraint:** UNIQUE(user_id, folder, filename) | |
| 480 | + | ||
| 481 | + | ### import_jobs | |
| 482 | + | Bulk import from external platforms (Patreon, Ko-fi, Gumroad). Tracks progress rows. | |
| 483 | + | ||
| 484 | + | ### project_categories | |
| 485 | + | Taxonomy for project categorization. Referenced by projects.category_id. | |
| 486 | + | ||
| 487 | + | --- | |
| 488 | + | ||
| 489 | + | ## Cascade Summary | |
| 490 | + | ||
| 491 | + | **CASCADE (delete parent → delete children):** Most FK relationships. Deleting a user cascades to all their projects, items, content, sync data, sessions, keys, etc. | |
| 492 | + | ||
| 493 | + | **SET NULL (delete parent → null the FK):** Used where the child record should survive: transactions keep seller/item info (denormalized title/username), sync_apps keep project/item links optional, git repos keep project association optional. | |
| 494 | + | ||
| 495 | + | **RESTRICT (prevent parent delete):** subscription_tiers — cannot delete a tier that has active subscribers. | |
| 496 | + | ||
| 497 | + | ## Search Infrastructure | |
| 498 | + | ||
| 499 | + | | Target | Index Type | Columns | | |
| 500 | + | |--------|-----------|---------| |
Lines truncated
| @@ -0,0 +1,89 @@ | |||
| 1 | + | # Smoke Test Checklist — MNW Server | |
| 2 | + | ||
| 3 | + | Pre-release manual verification. Run after every production deploy. | |
| 4 | + | ||
| 5 | + | ## Prerequisites | |
| 6 | + | ||
| 7 | + | - Fresh deploy completed (`deploy.sh` or `deploy.sh --quick`) | |
| 8 | + | - `systemctl status makenotwork` shows active | |
| 9 | + | - `curl -s http://127.0.0.1:3000` returns HTML | |
| 10 | + | ||
| 11 | + | ## Core Flows | |
| 12 | + | ||
| 13 | + | ### Auth | |
| 14 | + | - [ ] Sign up with new account (valid email, password meets requirements) | |
| 15 | + | - [ ] Log out | |
| 16 | + | - [ ] Log in with the new account | |
| 17 | + | - [ ] Password reset: request reset, verify email arrives (or check Postmark Activity) | |
| 18 | + | - [ ] Rate limiting: 6+ rapid login attempts returns 429 | |
| 19 | + | ||
| 20 | + | ### Creator Setup | |
| 21 | + | - [ ] Enable creator mode on account | |
| 22 | + | - [ ] Set creator profile (name, bio, avatar) | |
| 23 | + | - [ ] Stripe Connect onboarding link works (or verify Stripe Dashboard shows pending account) | |
| 24 | + | ||
| 25 | + | ### Content | |
| 26 | + | - [ ] Create a new project | |
| 27 | + | - [ ] Add a text item to the project | |
| 28 | + | - [ ] Upload an audio file (< 500 MB limit) | |
| 29 | + | - [ ] Upload an image (< 10 MB limit) | |
| 30 | + | - [ ] Verify uploaded files are accessible via download URL | |
| 31 | + | - [ ] Edit item metadata (title, description, tags) | |
| 32 | + | - [ ] Delete an item | |
| 33 | + | ||
| 34 | + | ### Purchase Flow | |
| 35 | + | - [ ] Create a free item, verify it's downloadable without payment | |
| 36 | + | - [ ] Create a paid item, verify checkout redirects to Stripe | |
| 37 | + | - [ ] Complete test checkout (use Stripe test card `4242 4242 4242 4242`) | |
| 38 | + | - [ ] Verify purchase receipt email arrives (or check Postmark) | |
| 39 | + | - [ ] Verify buyer can download after purchase | |
| 40 | + | ||
| 41 | + | ### Fan+ | |
| 42 | + | - [ ] Subscribe to a creator's Fan+ tier (test card) | |
| 43 | + | - [ ] Verify Fan+ badge appears on subscriber profile | |
| 44 | + | - [ ] Cancel subscription, verify access revokes at period end | |
| 45 | + | ||
| 46 | + | ### Email Verification | |
| 47 | + | - [ ] Change email address on account | |
| 48 | + | - [ ] Verification email arrives (check Postmark Activity if needed) | |
| 49 | + | - [ ] Click verification link, confirm email updates | |
| 50 | + | ||
| 51 | + | ### SyncKit | |
| 52 | + | - [ ] Authenticate via `/api/sync/auth` with valid credentials | |
| 53 | + | - [ ] Register a device via `/api/sync/devices` | |
| 54 | + | - [ ] Push a small changelog batch | |
| 55 | + | - [ ] Pull changes back, verify data matches | |
| 56 | + | - [ ] Verify JWT expiration (7-day max) | |
| 57 | + | ||
| 58 | + | ### Git Browser | |
| 59 | + | - [ ] Navigate to `/source/` — repository list loads | |
| 60 | + | - [ ] Click a repo, browse files | |
| 61 | + | - [ ] View a file's content | |
| 62 | + | - [ ] View commit log | |
| 63 | + | ||
| 64 | + | ### Public Pages | |
| 65 | + | - [ ] Homepage loads (`/`) | |
| 66 | + | - [ ] Creator profile page loads (`/@username`) | |
| 67 | + | - [ ] Project page loads | |
| 68 | + | - [ ] Documentation pages load (`/docs/...`) | |
| 69 | + | - [ ] RSS feed loads (`/@username/rss`) | |
| 70 | + | ||
| 71 | + | ## Infrastructure Checks | |
| 72 | + | ||
| 73 | + | - [ ] `systemctl status makenotwork` — active, no recent restarts | |
| 74 | + | - [ ] `systemctl status caddy` — active | |
| 75 | + | - [ ] `systemctl status postgresql` — active | |
| 76 | + | - [ ] `journalctl -u makenotwork --since "10 min ago"` — no errors | |
| 77 | + | - [ ] `curl -s https://makenot.work` — responds with 200 (through Cloudflare) | |
| 78 | + | - [ ] DB backup exists: `ls -lt /opt/makenotwork/backups/ | head -3` | |
| 79 | + | ||
| 80 | + | ## Webhook Verification | |
| 81 | + | ||
| 82 | + | - [ ] Stripe Dashboard → Webhooks → recent deliveries show 200 responses | |
| 83 | + | - [ ] If testing: `stripe trigger checkout.session.completed` → server processes event | |
| 84 | + | ||
| 85 | + | ## Post-Deploy Spot Checks | |
| 86 | + | ||
| 87 | + | - [ ] Static assets load (CSS, JS, fonts) — no broken styles | |
| 88 | + | - [ ] Custom error pages work: visit a non-existent URL, verify 404 page renders | |
| 89 | + | - [ ] File upload S3 connectivity: upload a small test file |
| @@ -0,0 +1,144 @@ | |||
| 1 | + | # Test Plan — MNW Server | |
| 2 | + | ||
| 3 | + | ## Overview | |
| 4 | + | ||
| 5 | + | ~3,200 tests total. ~66,000 lines of test code. Unit tests (no DB) + integration tests (real PostgreSQL). | |
| 6 | + | ||
| 7 | + | ## Test Architecture | |
| 8 | + | ||
| 9 | + | **Unit tests:** Inline `#[cfg(test)]` modules in source files. No DB needed. | |
| 10 | + | ||
| 11 | + | **Integration tests:** `tests/workflows/` directory. Each test gets a fresh PostgreSQL database via the test harness. Tests are grouped by feature domain. | |
| 12 | + | ||
| 13 | + | **Test harness:** `tests/harness/` provides `TestHarness` — spins up a real app instance with its own database, HTTP client, and helper methods (signup, login, create project, etc.). | |
| 14 | + | ||
| 15 | + | **DB creation pattern:** Each integration test calls `TestHarness::new().await`, which: | |
| 16 | + | 1. Connects to the `postgres` database (from `TEST_DATABASE_URL`) | |
| 17 | + | 2. Creates a unique `mnw_test_{uuid}` database | |
| 18 | + | 3. Runs all sqlx migrations | |
| 19 | + | 4. Boots the full app router | |
| 20 | + | 5. Drops the test database on `TestHarness::drop()` | |
| 21 | + | ||
| 22 | + | **CI:** `server/deploy/run-ci.sh` runs on astra. Executes `cargo check`, `cargo test`, `cargo clippy`, `cargo audit`. | |
| 23 | + | ||
| 24 | + | ## Running Tests | |
| 25 | + | ||
| 26 | + | ```bash | |
| 27 | + | # Unit tests only (no DB needed) | |
| 28 | + | cargo test --lib | |
| 29 | + | ||
| 30 | + | # Integration tests (needs PostgreSQL) | |
| 31 | + | TEST_DATABASE_URL="postgres:///postgres" cargo test --test integration | |
| 32 | + | ||
| 33 | + | # All tests | |
| 34 | + | TEST_DATABASE_URL="postgres:///postgres" cargo test | |
| 35 | + | ||
| 36 | + | # Filtered | |
| 37 | + | TEST_DATABASE_URL="postgres:///postgres" cargo test --test integration auth:: | |
| 38 | + | ||
| 39 | + | # On astra (recommended for integration tests) | |
| 40 | + | ssh 100.106.221.39 | |
| 41 | + | /home/max/staging/run-tests.sh # All | |
| 42 | + | /home/max/staging/run-tests.sh auth:: # Filtered | |
| 43 | + | ``` | |
| 44 | + | ||
| 45 | + | **Important:** Use `--test-threads=8` (or set `RUST_TEST_THREADS=8`). Astra has 96 cores — default parallelism overwhelms PostgreSQL with too many simultaneous `CREATE DATABASE` calls. | |
| 46 | + | ||
| 47 | + | **Orphaned test DBs:** If tests are killed mid-run, `mnw_test_*` databases remain. The `run-tests.sh` script cleans these up automatically. Manual cleanup: | |
| 48 | + | ```bash | |
| 49 | + | psql -t -c "SELECT datname FROM pg_database WHERE datname LIKE 'mnw_test_%';" postgres \ | |
| 50 | + | | xargs -I{} psql -c "DROP DATABASE IF EXISTS \"{}\";" postgres | |
| 51 | + | ``` | |
| 52 | + | ||
| 53 | + | ## What's Covered | |
| 54 | + | ||
| 55 | + | ### Integration Tests (`tests/workflows/`) | |
| 56 | + | ||
| 57 | + | | Domain | Test File(s) | What's Tested | | |
| 58 | + | |--------|-------------|---------------| | |
| 59 | + | | **Auth** | `auth.rs`, `adversarial_auth.rs` | Signup, login, logout, session management, password validation, rate limiting, brute force protection | | |
| 60 | + | | **Account** | `account.rs`, `account_deletion.rs` | Profile update, email change, password change, full account deletion cascade | | |
| 61 | + | | **Creator** | `creator.rs`, `creator_media.rs` | Creator onboarding, profile setup, media uploads | | |
| 62 | + | | **Content** | `content.rs`, `content_insertions.rs` | Item CRUD, content types, insertions, ordering | | |
| 63 | + | | **Payments** | `fan_plus.rs` | Fan+ subscriptions, Stripe checkout flows | | |
| 64 | + | | **Collections** | `collections.rs` | Collection CRUD, item membership | | |
| 65 | + | | **Categories** | `categories.rs` | Category tree management | | |
| 66 | + | | **Blog** | `blog.rs` | Blog post CRUD, rendering | | |
| 67 | + | | **Broadcast** | `broadcast.rs` | Email broadcast campaigns | | |
| 68 | + | | **Chapters** | `chapters.rs` | Chapter management for content | | |
| 69 | + | | **Contacts** | `contacts.rs` | Creator contact list | | |
| 70 | + | | **Custom Domains** | `custom_domains.rs` | Domain verification, routing | | |
| 71 | + | | **Custom Links** | `custom_links.rs` | Creator link management | | |
| 72 | + | | **Exports** | `exports.rs` | Data export functionality | | |
| 73 | + | | **Follows** | `follows.rs` | Creator follow/unfollow | | |
| 74 | + | | **Analytics** | `analytics.rs` | View tracking, download counts | | |
| 75 | + | | **Appeal** | `appeal.rs` | Content moderation appeals | | |
| 76 | + | | **Admin** | `admin.rs` | Admin panel operations | | |
| 77 | + | | **Builds** | `builds.rs` | Software build/release management | | |
| 78 | + | | **Git Browser** | `git_browser.rs`, `git_issues.rs`, `git_project.rs` | Repo browsing, issue tracking, project settings | | |
| 79 | + | | **SyncKit** | `synckit.rs` | Auth, devices, push/pull, key management, validation | | |
| 80 | + | | **Fingerprinting** | `fingerprinting.rs` | Content fingerprinting, license templates | | |
| 81 | + | | **Adversarial** | `adversarial.rs`, `adversarial_business.rs`, `adversarial_input.rs` | XSS, injection, oversized inputs, permission boundaries, business logic abuse | | |
| 82 | + | ||
| 83 | + | ### Unit Tests (inline) | |
| 84 | + | ||
| 85 | + | | Module | Tests | What's Tested | | |
| 86 | + | |--------|-------|---------------| | |
| 87 | + | | `types/mod.rs` | 14 | Type conversions, enum parsing, ID generation | | |
| 88 | + | | `payments/webhooks.rs` | 22 | Webhook signature verification, event routing, idempotency | | |
| 89 | + | | `payments/checkout.rs` | 12 | Checkout session creation, price calculation, discount logic | | |
| 90 | + | | `fingerprint/license_templates.rs` | 8 | License text generation, template variable substitution | | |
| 91 | + | | `csrf.rs` | — | CSRF token generation and validation | | |
| 92 | + | | `auth.rs` | — | Password hashing, session token generation | | |
| 93 | + | | `config.rs` | — | Configuration parsing, env var handling | | |
| 94 | + | | `rss.rs` | — | RSS feed generation | | |
| 95 | + | | `scanning/*` | — | ClamAV, YARA, hash lookup, content type detection | | |
| 96 | + | ||
| 97 | + | ## What's Intentionally Not Tested | |
| 98 | + | ||
| 99 | + | | Area | Reason | | |
| 100 | + | |------|--------| | |
| 101 | + | | Stripe live mode | Would charge real money. Test mode webhooks are tested. | | |
| 102 | + | | Postmark delivery | Would send real emails. Email composition logic is tested; delivery is mocked. | | |
| 103 | + | | Cloudflare CDN | Infrastructure layer, not application logic. | | |
| 104 | + | | TLS/certificate handling | Caddy handles this; tested manually. | | |
| 105 | + | | Systemd integration | Deployment concern, not application logic. | | |
| 106 | + | | Real S3 uploads | Integration tests mock S3 or use test endpoints. | | |
| 107 | + | | ClamAV/YARA scanning | Requires running daemons; scanning logic is unit-tested. | | |
| 108 | + | ||
| 109 | + | ## Adding New Tests | |
| 110 | + | ||
| 111 | + | ### Integration test pattern | |
| 112 | + | ```rust | |
| 113 | + | #[tokio::test] | |
| 114 | + | async fn my_new_test() { | |
| 115 | + | let mut h = TestHarness::new().await; | |
| 116 | + | let user_id = h.signup("testuser", "test@example.com", "Password1!").await; | |
| 117 | + | ||
| 118 | + | // Use h.client for HTTP requests | |
| 119 | + | let resp = h.client.get("/api/some-endpoint").await; | |
| 120 | + | assert_eq!(resp.status, 200); | |
| 121 | + | ||
| 122 | + | // Or use h.db for direct SQL | |
| 123 | + | let row = sqlx::query("SELECT ...").fetch_one(&h.db).await.unwrap(); | |
| 124 | + | } | |
| 125 | + | ``` | |
| 126 | + | ||
| 127 | + | ### Unit test pattern | |
| 128 | + | ```rust | |
| 129 | + | #[cfg(test)] | |
| 130 | + | mod tests { | |
| 131 | + | use super::*; | |
| 132 | + | ||
| 133 | + | #[test] | |
| 134 | + | fn my_unit_test() { | |
| 135 | + | // No DB, no async, pure logic | |
| 136 | + | } | |
| 137 | + | } | |
| 138 | + | ``` | |
| 139 | + | ||
| 140 | + | ## Key Paths | |
| 141 | + | ||
| 142 | + | - `tests/harness/` — TestHarness, TestClient, helper methods | |
| 143 | + | - `tests/workflows/` — All integration test files | |
| 144 | + | - `deploy/run-ci.sh` — CI script (runs on astra) |
| @@ -0,0 +1,178 @@ | |||
| 1 | + | # Troubleshooting — MNW Server | |
| 2 | + | ||
| 3 | + | ## Service Won't Start | |
| 4 | + | ||
| 5 | + | ``` | |
| 6 | + | Check logs: journalctl -u makenotwork -n 50 --no-pager | |
| 7 | + | ``` | |
| 8 | + | ||
| 9 | + | | Symptom | Cause | Fix | | |
| 10 | + | |---------|-------|-----| | |
| 11 | + | | "DATABASE_URL environment variable is required" | Missing env var | Check `/opt/makenotwork/.env` has `DATABASE_URL` | | |
| 12 | + | | "SIGNING_SECRET is required in production" | HOST=0.0.0.0 or HTTPS HOST_URL without secret | Set `SIGNING_SECRET` to a random string in `.env` | | |
| 13 | + | | "Invalid HOST address" / "Invalid PORT number" | Malformed HOST or PORT | HOST must be valid IP (default 127.0.0.1), PORT must be integer (default 3000) | | |
| 14 | + | | Startup hangs then fails | PostgreSQL unreachable | `systemctl status postgresql`, verify `DATABASE_URL` connection string | | |
| 15 | + | | "Failed to run migrations" | Migration error or DB permissions | Connect manually: `psql $DATABASE_URL -c "SELECT 1"`, check migration SQL | | |
| 16 | + | | "Failed to migrate session store" | tower_sessions table issue | Usually resolves on retry. If persistent, check DB user has CREATE TABLE permission | | |
| 17 | + | | Startup succeeds but features missing | Optional services not configured | Stripe, Postmark, S3, Git browser, SyncKit all degrade gracefully if env vars missing | | |
| 18 | + | ||
| 19 | + | ## 502 Errors | |
| 20 | + | ||
| 21 | + | Caddy serves `/opt/makenotwork/error-pages/502.html` when the app is unreachable. | |
| 22 | + | ||
| 23 | + | 1. **Is the process running?** | |
| 24 | + | ```bash | |
| 25 | + | systemctl status makenotwork --no-pager | |
| 26 | + | ``` | |
| 27 | + | - Not running → `systemctl restart makenotwork`, check logs for crash cause | |
| 28 | + | - Running but not responding → check port: `curl -s http://127.0.0.1:3000` | |
| 29 | + | ||
| 30 | + | 2. **Is Caddy running?** | |
| 31 | + | ```bash | |
| 32 | + | systemctl status caddy --no-pager | |
| 33 | + | ``` | |
| 34 | + | - Not running → `systemctl restart caddy` | |
| 35 | + | ||
| 36 | + | 3. **Is PostgreSQL running?** | |
| 37 | + | ```bash | |
| 38 | + | systemctl status postgresql --no-pager | |
| 39 | + | sudo -u makenotwork psql makenotwork -c "SELECT 1" | |
| 40 | + | ``` | |
| 41 | + | - Not running → `systemctl restart postgresql`, then `systemctl restart makenotwork` | |
| 42 | + | ||
| 43 | + | 4. **Port conflict?** | |
| 44 | + | ```bash | |
| 45 | + | lsof -i :3000 | |
| 46 | + | ``` | |
| 47 | + | - Another process → kill it or change `PORT` in `.env` | |
| 48 | + | ||
| 49 | + | ## Slow Queries | |
| 50 | + | ||
| 51 | + | **Symptoms:** Pages load slowly, "timeout acquiring connection" in logs, high PostgreSQL CPU. | |
| 52 | + | ||
| 53 | + | **Diagnostics:** | |
| 54 | + | ```bash | |
| 55 | + | # Enable slow query logging | |
| 56 | + | sudo -u postgres psql -c "ALTER SYSTEM SET log_min_duration_statement = 1000;" | |
| 57 | + | sudo -u postgres psql -c "SELECT pg_reload_conf();" | |
| 58 | + | ||
| 59 | + | # Check active queries | |
| 60 | + | sudo -u postgres psql -c "SELECT pid, now() - pg_stat_activity.query_start AS duration, query FROM pg_stat_activity WHERE state = 'active' ORDER BY duration DESC LIMIT 5;" | |
| 61 | + | ``` | |
| 62 | + | ||
| 63 | + | **Known patterns:** | |
| 64 | + | - Discover search with very short terms → triggers trigram scan. The `pg_trgm` extension + GIN index mitigate this. | |
| 65 | + | - Tag hierarchy queries → EXISTS subqueries on items with many tags. | |
| 66 | + | - Connection pool exhaustion → default is 25 connections, 3s acquire timeout. If all busy, new requests fail after 3s. | |
| 67 | + | ||
| 68 | + | ## Stripe Webhook Failures | |
| 69 | + | ||
| 70 | + | **Symptoms:** Purchases not completing, subscriptions not updating. | |
| 71 | + | ||
| 72 | + | 1. **Check Stripe Dashboard → Webhooks** for failed deliveries | |
| 73 | + | 2. **Check server logs:** | |
| 74 | + | ```bash | |
| 75 | + | journalctl -u makenotwork --since "1 hour ago" | grep -i stripe | |
| 76 | + | ``` | |
| 77 | + | ||
| 78 | + | | Log Message | Cause | Fix | | |
| 79 | + | |-------------|-------|-----| | |
| 80 | + | | "Missing Stripe signature" | Request missing `Stripe-Signature` header | Webhook URL misconfigured in Stripe Dashboard | | |
| 81 | + | | "Invalid payload encoding" | Non-UTF8 body | Stripe endpoint URL wrong (hitting wrong service) | | |
| 82 | + | | Signature verification error | `STRIPE_WEBHOOK_SECRET` mismatch | Copy exact secret from Stripe Dashboard → Webhooks, update `.env`, restart | | |
| 83 | + | | Event type not handled (debug log) | Unhandled event type | Expected — only specific events are processed | | |
| 84 | + | | "Stripe not configured" | Missing `STRIPE_SECRET_KEY` | Set env var in `.env`, restart | | |
| 85 | + | ||
| 86 | + | **Test locally:** | |
| 87 | + | ```bash | |
| 88 | + | stripe listen --forward-to localhost:3000/stripe/webhook | |
| 89 | + | stripe trigger checkout.session.completed | |
| 90 | + | ``` | |
| 91 | + | ||
| 92 | + | ## Email Not Sending | |
| 93 | + | ||
| 94 | + | **Symptoms:** Password resets, purchase receipts, or verification emails not arriving. | |
| 95 | + | ||
| 96 | + | 1. **Is Postmark configured?** | |
| 97 | + | - Check `.env` for `POSTMARK_TOKEN`. If missing, emails log to stdout (dev mode). | |
| 98 | + | ||
| 99 | + | 2. **Is the recipient suppressed?** | |
| 100 | + | ```sql | |
| 101 | + | SELECT * FROM email_suppressions WHERE email = 'user@example.com'; | |
| 102 | + | ``` | |
| 103 | + | - If found, remove: `DELETE FROM email_suppressions WHERE email = 'user@example.com';` | |
| 104 | + | ||
| 105 | + | 3. **Check Postmark Dashboard → Activity** for delivery status | |
| 106 | + | ||
| 107 | + | 4. **Check server logs:** | |
| 108 | + | ```bash | |
| 109 | + | journalctl -u makenotwork --since "1 hour ago" | grep -i email | |
| 110 | + | ``` | |
| 111 | + | ||
| 112 | + | | Log Message | Cause | Fix | | |
| 113 | + | |-------------|-------|-----| | |
| 114 | + | | "email skipped (suppressed)" | Recipient on suppression list | Remove from `email_suppressions` table | | |
| 115 | + | | "Failed to send email" | Postmark API error (timeout, auth, invalid address) | Check Postmark Dashboard for details, verify token | | |
| 116 | + | | Emails logged to console | `POSTMARK_TOKEN` not set | Set env var, restart | | |
| 117 | + | ||
| 118 | + | ## Sync Failures (SyncKit) | |
| 119 | + | ||
| 120 | + | **Symptoms:** Desktop apps can't push/pull data. | |
| 121 | + | ||
| 122 | + | 1. **Is SyncKit configured?** | |
| 123 | + | - Check `.env` for `SYNCKIT_JWT_SECRET`. If missing, endpoints return 503. | |
| 124 | + | ||
| 125 | + | 2. **JWT issues:** | |
| 126 | + | ||
| 127 | + | | Error | Cause | Fix | | |
| 128 | + | |-------|-------|-----| | |
| 129 | + | | 401 Unauthorized | Token expired (7-day max) or bad signature | Client should re-authenticate via `/api/synckit/auth` | | |
| 130 | + | | "Unknown app" | API key invalid or app inactive | Check `sync_apps` table: `SELECT * FROM sync_apps WHERE api_key = '...'` | | |
| 131 | + | | "Unknown device" | Device not registered | Client should call `POST /api/sync/devices` first | | |
| 132 | + | ||
| 133 | + | 3. **Push failures:** | |
| 134 | + | ||
| 135 | + | | Error | Cause | Fix | | |
| 136 | + | |-------|-------|-----| | |
| 137 | + | | "Maximum 500 changes per push" | Batch too large | Client should split into ≤500-change batches | | |
| 138 | + | | "Table name validation failed" | Invalid chars in table name | Use alphanumeric + underscores only, max 100 chars | | |
| 139 | + | | "DELETE operations should not include data" | Data payload on DELETE op | Client bug — set `data: null` for DELETEs | | |
| 140 | + | ||
| 141 | + | 4. **Blob storage:** | |
| 142 | + | - Check `SYNCKIT_S3_*` env vars for the separate SyncKit bucket | |
| 143 | + | - If S3 unreachable, blob up/download fails but changelog sync still works | |
| 144 | + | ||
| 145 | + | ## Git Browser Errors | |
| 146 | + | ||
| 147 | + | **Symptoms:** Source browser pages return 404 or 500. | |
| 148 | + | ||
| 149 | + | 1. **Is the git browser configured?** | |
| 150 | + | - Check `.env` for `GIT_REPOS_PATH`. If missing, all git routes return 404. | |
| 151 | + | ||
| 152 | + | 2. **Repository not found:** | |
| 153 | + | ```bash | |
| 154 | + | ls /opt/git/ # Check bare repos exist | |
| 155 | + | ``` | |
| 156 | + | - Repos must be bare (`git init --bare`) | |
| 157 | + | - Path structure: `$GIT_REPOS_PATH/{owner}/{repo}.git/` | |
| 158 | + | ||
| 159 | + | 3. **File too large (>1MB):** Intentional limit. Large files show truncation message. | |
| 160 | + | ||
| 161 | + | 4. **Repo corruption:** | |
| 162 | + | ```bash | |
| 163 | + | cd /opt/git/owner/repo.git && git fsck --full | |
| 164 | + | ``` | |
| 165 | + | ||
| 166 | + | ## Resource Limits | |
| 167 | + | ||
| 168 | + | | Resource | Limit | What Happens | | |
| 169 | + | |----------|-------|-------------| | |
| 170 | + | | DB connections | 25 max | "timeout acquiring connection" after 3s wait | | |
| 171 | + | | Memory | 512M (systemd MemoryMax) | Process killed by OOM, auto-restarts | | |
| 172 | + | | File descriptors | 65535 (LimitNOFILE) | "too many open files" | | |
| 173 | + | | File upload: audio | 500 MB | 413 Payload Too Large | | |
| 174 | + | | File upload: image | 10 MB | 413 Payload Too Large | | |
| 175 | + | | File upload: video | 20 GB | 413 Payload Too Large | | |
| 176 | + | | Login rate limit | 2/sec, burst 5 | 429 Too Many Requests | | |
| 177 | + | | API rate limit | 2/sec, burst 10 | 429 Too Many Requests | | |
| 178 | + | | SyncKit rate limit | 10/sec, burst 30 | 429 Too Many Requests | |