Skip to main content

max / makenotwork

Code review remediation, media library, SyncKit SSE, docs expansion Security: constant_time_compare for ServiceAuth token check. Clippy: all 19 warnings resolved (let-chains, collapsible_if, too_many_arguments). Media library: migration 057, db module, storage routes, dashboard tab. SyncKit: SSE subscribe endpoint, selective sync. Doc site: 10 new guide pages, search JS, doc coverage matrix. Load test harness fix (sync_notify). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-04-12 23:45 UTC
Commit: 7d533509bae310541ecedb96416aa6773eee1cb8
Parent: 19f67b5
82 files changed, +3524 insertions, -173 deletions
M Cargo.lock +3 -1
@@ -1804,6 +1804,7 @@ dependencies = [
1804 1804 "ammonia",
1805 1805 "pulldown-cmark",
1806 1806 "regex",
1807 + "regex-lite",
1807 1808 "serde",
1808 1809 "toml",
1809 1810 "tracing",
@@ -3372,7 +3373,7 @@ dependencies = [
3372 3373
3373 3374 [[package]]
3374 3375 name = "makenotwork"
3375 - version = "0.3.21"
3376 + version = "0.3.22"
3376 3377 dependencies = [
3377 3378 "anyhow",
3378 3379 "argon2",
@@ -3412,6 +3413,7 @@ dependencies = [
3412 3413 "tempfile",
3413 3414 "thiserror 2.0.18",
3414 3415 "tokio",
3416 + "tokio-stream",
3415 3417 "totp-rs",
3416 3418 "tower",
3417 3419 "tower-http",
M Cargo.toml +2 -1
@@ -14,6 +14,7 @@ axum-extra = { version = "0.10.3", features = ["cookie", "form", "typed-header"]
14 14 serde = { version = "1.0.228", features = ["derive"] }
15 15 serde_json = "1.0.149"
16 16 tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread", "net", "signal"] }
17 + tokio-stream = { version = "0.1", features = ["sync"] }
17 18 tower = "0.5.3"
18 19 tower-http = { version = "0.6.8", features = ["trace", "fs", "limit", "request-id", "propagate-header", "set-header"] }
19 20 tracing = "0.1.44"
@@ -80,7 +81,7 @@ thiserror = "2.0.18"
80 81 anyhow = "1.0.101"
81 82
82 83 # Markdown rendering + documentation engine
83 - docengine = { path = "../Shared/docengine", features = ["doc-loader", "frontmatter"] }
84 + docengine = { path = "../Shared/docengine", features = ["doc-loader", "directives", "frontmatter", "media-urls"] }
84 85
85 86 # Tag standard
86 87 tagtree = { path = "../Shared/tagtree" }
@@ -1,5 +1,5 @@
1 1 #!/bin/bash
2 - # Generate rustdoc for library crates (synckit-client, docengine, tagtree).
2 + # Generate rustdoc for shared library crates.
3 3 # Output goes to rustdoc-out/ (relative to MNW/).
4 4 # Run from the MNW directory.
5 5
@@ -13,7 +13,7 @@ fi
13 13 OUT_DIR="$(pwd)/rustdoc-out"
14 14 SHARED_DIR="$(cd ../Shared && pwd)"
15 15
16 - CRATES=("synckit-client" "docengine" "tagtree")
16 + CRATES=("synckit-client" "docengine" "tagtree" "s3-storage" "theme-common")
17 17
18 18 rm -rf "$OUT_DIR"
19 19 mkdir -p "$OUT_DIR"
M docs/audit.md +20 -15
@@ -4,16 +4,16 @@ Run after major features, monthly during active development, or when concerned a
4 4
5 5 ## Audit Scope
6 6
7 - | Project | Path | Stack |
8 - |---------|------|-------|
9 - | MakeNotWork | MNW/ | Rust/Axum/PostgreSQL, Askama, HTMX |
10 - | SyncKit Client SDK | Shared/synckit-client/ | Rust, reqwest, ChaCha20-Poly1305, Argon2, keychain |
11 - | PoM | pom/ | Rust/Axum/SQLite, MCP (rmcp), reqwest |
12 - | audiofiles | Apps/audiofiles/ | Rust, eframe, egui, cpal, rusqlite |
13 - | Balanced Breakfast | Apps/balanced_breakfast/ | Rust/Tauri 2, SQLite, Rhai plugins, vanilla JS |
14 - | GoingsOn | Apps/goingson/ | Rust/Tauri 2, SQLite, vanilla JS, IMAP/SMTP |
15 - | Multithreaded | multithreaded/ | Rust/Axum/PostgreSQL, Askama, HTMX, MNW OAuth |
16 - | TagTree | Shared/tagtree/ | Rust library crate, no_std compatible, serde, criterion benchmarks |
7 + | Harness ID | Project | Path | Stack |
8 + |------------|---------|------|-------|
9 + | `mnw` | MakeNotWork | MNW/ | Rust/Axum/PostgreSQL, Askama, HTMX |
10 + | `synckit` | SyncKit Client SDK | Shared/synckit-client/ | Rust, reqwest, ChaCha20-Poly1305, Argon2, keychain |
11 + | `pom` | PoM | pom/ | Rust/Axum/SQLite, MCP (rmcp), reqwest |
12 + | `af` | audiofiles | Apps/audiofiles/ | Rust, eframe, egui, cpal, rusqlite |
13 + | `bb` | Balanced Breakfast | Apps/balanced_breakfast/ | Rust/Tauri 2, SQLite, Rhai plugins, vanilla JS |
14 + | `go` | GoingsOn | Apps/goingson/ | Rust/Tauri 2, SQLite, vanilla JS, IMAP/SMTP |
15 + | `mt` | Multithreaded | multithreaded/ | Rust/Axum/PostgreSQL, Askama, HTMX, MNW OAuth |
16 + | `tagtree` | TagTree | Shared/tagtree/ | Rust library crate, no_std compatible, serde, criterion benchmarks |
17 17
18 18 ---
19 19
@@ -41,6 +41,8 @@ Run after major features, monthly during active development, or when concerned a
41 41
42 42 ## Module-Level Assessment
43 43
44 + Use `get_project(id)` from the codebase harness to get the full module inventory for each project — all source files with extracted types, exports, and line counts. This replaces manual filesystem scanning.
45 +
44 46 Grade each dimension **per module** (crate, directory module, or route group with ~100+ lines), then roll up to project-level. Weight by risk and size — an untested 2,000-line file drags Testing down more than an untested 50-line wrapper.
45 47
46 48 **Cold spots:** any module graded B or below, or 2+ letter grades below the project median. Include a Module Heatmap in the audit review.
@@ -63,6 +65,7 @@ Grade each dimension **per module** (crate, directory module, or route group wit
63 65 - [ ] No secrets, database files, or large binaries tracked
64 66
65 67 ### Documentation Consistency
68 + - [ ] `startup_check` shows no stale docs (all hashes current)
66 69 - [ ] File paths in docs still exist
67 70 - [ ] Code references (structs, endpoints, env vars) match codebase
68 71 - [ ] Feature/pricing descriptions match across CLAUDE.md, public docs, and code
@@ -82,10 +85,12 @@ Grade each dimension **per module** (crate, directory module, or route group wit
82 85
83 86 ## Execution Order
84 87
85 - 1. Run the core audit — module heatmaps, scorecards, cold spots per project (parallelizable)
86 - 2. Write/update `audit_review.md` per project (reads previous review only now)
87 - 3. File action items in each project's `todo.md` (critical = current phase, architectural = deferred)
88 - 4. Update `audit_history.md` with run summary
88 + 0. **Harness startup** — call `startup_check` to get doc freshness report, version mismatches, and project overview. Call `list_projects` for current versions and test counts. Flag any stale docs before beginning.
89 + 1. **Core audit** — for each project, call `get_project(id)` to get its module map, then grade per module. Module heatmaps, scorecards, cold spots (parallelizable across projects).
90 + 2. **Write/update** `audit_review.md` per project (reads previous review only now)
91 + 3. **File action items** in each project's `todo.md` (critical = current phase, architectural = deferred)
92 + 4. **Update** `audit_history.md` with run summary
93 + 5. **Harness update** — for each audited project: `update_test_count(id, count)` with latest test counts. `mark_verified(path)` on all docs that were reviewed. If versions changed, `record_deploy(id, version)`. Repopulate if module structure changed significantly: `cd _meta/harness/server && node dist/populate.js`
89 94
90 95 ---
91 96
@@ -123,5 +128,5 @@ Each project's `docs/<short-name>/audit_review.md` should contain: overall grade
123 128 ## Post-Session Cleanup
124 129
125 130 ```bash
126 - find /Users/max/Git -name "target" -type d -maxdepth 4 -exec rm -rf {} +
131 + find /Users/max/Code -name "target" -type d -maxdepth 4 -exec rm -rf {} +
127 132 ```
@@ -0,0 +1,126 @@
1 + # MNW Server — Code Review
2 +
3 + **Date:** 2026-04-12
4 + **Version:** 0.3.22
5 + **Reviewer:** Claude (Opus 4.6)
6 + **Scope:** Core MNW server crate — all Rust source, routes, auth, payments, scanning, sync, DB, tests. Excludes nested ecosystem projects (multithreaded/, pom/, mnw-cli/).
7 +
8 + ## Summary
9 +
10 + MNW is an Axum 0.8-based creator platform backend (~66,600 LOC Rust, 237 source files) serving the makenot.work marketplace. Handles user auth (password + WebAuthn passkeys + OAuth), Stripe Connect payments, file scanning (6-layer pipeline), SyncKit device sync, git hosting, blog/docs rendering, and a full admin CLI. PostgreSQL via compile-time-checked sqlx. 57 migrations. 1,268 tests (660 unit + 606 integration).
11 +
12 + **Overall: A** — exceptional security posture, comprehensive test coverage, clean architecture. 13 dependency vulnerabilities are all transitive via yara-x (blocked on upstream fix). All code-level findings resolved.
13 +
14 + ---
15 +
16 + ## Findings
17 +
18 + ### [HIGH] 13 cargo audit vulnerabilities via yara-x transitive dependencies
19 +
20 + yara-x 1.14.0 pulls wasmtime 40.0.4, which has 11 CVEs (all published 2026-04-09):
21 + - **2 critical (severity 9.0):** RUSTSEC-2026-0096 (sandbox escape on aarch64 Cranelift), RUSTSEC-2026-0095 (Winch sandbox escape)
22 + - **7 medium (4.1–6.9):** OOB reads/writes in string transcoding, segfault on f64x2.splat, table.grow/fill issues, flags lifting panic
23 + - **2 low (2.3):** pooling allocator data leakage, Winch host data leakage
24 +
25 + Additionally: 1 intaglio advisory (RUSTSEC-2026-0078, symbol confusion, fix available at >=1.13.3) and 1 rsa advisory (RUSTSEC-2023-0071, Marvin Attack, severity 5.9, no fix available — transitive via yara-x + sqlx-mysql).
26 +
27 + All wasmtime CVEs require wasmtime >=42.0.2. Since wasmtime is a transitive dep of yara-x, the fix is blocked on yara-x releasing a version with wasmtime 42+. **Mitigating factor:** YARA rules are developer-controlled, not user-supplied, significantly reducing attack surface. No sandbox escape is reachable from user input.
28 +
29 + 7 additional allowed warnings (bincode unmaintained x2, instant unmaintained, lru unsound IterMut, rand unsound with custom logger x3) — all transitive, theoretical-only impact.
30 +
31 + ### ~~[MEDIUM] ServiceAuth uses non-constant-time comparison (auth.rs:225)~~ — Done
32 +
33 + Fixed: now uses `constant_time_compare()`.
34 +
35 + ### ~~[MEDIUM] 19 clippy warnings~~ — Done
36 +
37 + 19 collapsible_if warnings fixed using Rust 2024 let chains across 12 source files. 1 additional `too_many_arguments` suppressed. 1 `while let` → `for` loop conversion. 1 pre-existing load test compilation error fixed (missing `sync_notify` field). 0 warnings remaining.
38 +
39 + ### [INFO] helpers.rs (767 lines) and pricing.rs (705 lines) — within guideline
40 +
41 + Initially flagged as over 500 lines, but on closer inspection: helpers.rs has ~387 lines of branching logic + 380 lines of tests; pricing.rs has ~298 lines of branching logic + 405 lines of tests. Both are within the 500-line branching guideline. No split needed.
42 +
43 + ### [LOW] scheduler.rs at 635 lines
44 +
45 + Background job scheduler with multiple task types. Borderline — some length is flat task definitions. Monitor for growth.
46 +
47 + ### [LOW] bin/mnw-admin.rs at 1,240 lines
48 +
49 + Standalone admin CLI binary. Contains 15+ subcommand handlers plus SSH git-auth management commands. Each handler is independent (flat dispatch). Exempt as a CLI tool with flat command dispatch.
50 +
51 + ### [INFO] Several route files approaching 500-line limit
52 +
53 + - routes/api/license_keys.rs (684) — route handlers + validation
54 + - routes/pages/dashboard/tabs/user.rs (654) — dashboard UI
55 + - routes/pages/public/health.rs (767) — health check probes (self-contained, exempt)
56 +
57 + ### [INFO] Flat data files are exempt and correctly structured
58 +
59 + Large files that are flat data (no branching logic) and correctly exempt from the 500-line guideline:
60 + - db/models.rs (2,172) — struct definitions
61 + - wordlist.rs (2,056) — static word list
62 + - db/enums.rs (1,286) — enum definitions + impl_str_enum! expansions
63 + - templates/public.rs (954) — Askama template structs
64 + - db/items.rs (924) — SQL queries
65 + - types/mod.rs (908) — type definitions
66 + - storage.rs (786) — ~200 lines flat const data + branching logic
67 + - templates/partials.rs (762) — template structs
68 + - db/users.rs (650), db/creator_tiers.rs (633), db/analytics.rs (612), db/transactions.rs (588) — SQL queries
69 + - types/conversions.rs (580) — From/Into implementations
70 +
71 + ---
72 +
73 + ## Strengths
74 +
75 + - **Zero SQL injection surface.** All 57 migrations and every query use compile-time-checked sqlx macros. No raw SQL string interpolation anywhere in the codebase.
76 + - **6-layer file scanning pipeline.** Content-type verification, structural analysis, ZIP bomb detection, YARA rules, ClamAV, and MalwareBazaar hash check. Fail-closed: any scanner failure blocks the upload.
77 + - **1,268 tests.** 660 unit + 606 integration, including 61 adversarial tests (XSS, path traversal, injection payloads). All tests run against real PostgreSQL (each integration test creates/drops its own database).
78 + - **Comprehensive auth.** Argon2id password hashing (46 MiB, 2 iterations, 1 thread) + WebAuthn passkeys + OAuth (GitHub, Google) + CSRF synchronizer tokens (constant-time) + HMAC-SHA256 email verification with password-hash binding (invalidated on password change).
79 + - **Rate limiting with 5 tiers.** Auth (strict), API (standard), upload (per-user quota), webhook (lenient), public (moderate). Via tower-governor.
80 + - **DashMap session cache.** 30-second TTL, fail-closed (cache miss = DB lookup). No unbounded growth.
81 + - **Stripe Connect Direct Charges.** Webhook signature verification (v1 + v2). Proper idempotency. Creator payouts via Connect.
82 + - **SyncKit device sync.** JWT-authenticated (HS256, 7-day expiry), changelog-based delta sync, E2E encryption (server stores only encrypted blobs).
83 + - **Clean architecture.** Routes, DB, types, templates, and business logic cleanly separated. 121 route files across well-organized directory modules.
84 + - **Graceful degradation.** Scanner failures, payment webhook retries, email delivery failures — all handled without crashing. Errors logged, not silently swallowed.
85 +
86 + ## Security Checklist
87 +
88 + | Check | Status |
89 + |-------|--------|
90 + | SQL injection | Pass — compile-time sqlx, zero raw interpolation |
91 + | XSS | Pass — Askama auto-escaping, CSP headers |
92 + | CSRF | Pass — synchronizer token pattern, constant-time compare |
93 + | Auth bypass | Pass — middleware chain, session validation, suspended user rejection |
94 + | Path traversal | Pass — filename sanitization, no directory traversal |
95 + | Timing attacks | Pass — all secret comparisons use constant_time_compare |
96 + | File upload safety | Pass — 6-layer scanning, fail-closed |
97 + | Secret exposure | Pass — env-loaded (.env), never logged |
98 + | Rate limiting | Pass — 5 tiers, per-route configuration |
99 + | Password storage | Pass — Argon2id (46 MiB, 2 iterations) |
100 + | Webhook verification | Pass — Stripe signature v1+v2, Postmark token check |
101 + | Session management | Pass — DashMap cache with TTL, fail-closed |
102 +
103 + ## Metrics
104 +
105 + | Metric | Value |
106 + |--------|-------|
107 + | Rust source LOC | ~66,600 |
108 + | Source files | 237 |
109 + | Route files | 121 |
110 + | Unit tests | 660 |
111 + | Integration tests | 606 |
112 + | Total tests | 1,266 |
113 + | Clippy warnings | 0 |
114 + | Dependency vulnerabilities | 13 (2 critical, all transitive) |
115 + | Dependency warnings | 7 (all transitive) |
116 + | SQL migrations | 57 |
117 + | API endpoints | ~120 |
118 +
119 + ## Action Items
120 +
121 + 1. **[HIGH]** Upgrade yara-x when a version with wasmtime >=42.0.2 is released (fixes 12 of 13 vulnerabilities) — Deferred to Dependencies in todo.md.
122 + 2. ~~**[MEDIUM]** Fix ServiceAuth to use `constant_time_compare()` in auth.rs~~ — Done.
123 + 3. ~~**[MEDIUM]** Fix clippy warnings using let chains~~ — Done. 0 warnings.
124 + 4. ~~**[MEDIUM]** Split helpers.rs and pricing.rs~~ — Not needed. Both under 500 lines of branching logic.
125 + 5. **[LOW]** Monitor scheduler.rs (635), git/mod.rs (613), license_keys.rs (684) for growth — Deferred.
126 + 6. **[LOW]** Consider splitting bin/mnw-admin.rs git-auth commands into separate module — Deferred.
@@ -0,0 +1,159 @@
1 + # Feature-to-Docs Coverage Matrix
2 +
3 + Three documentation levels for every feature. Source: codebase harness DB (727 modules, 638 key files, 49 docs).
4 +
5 + **Legend:** Y = covered, P = partial, N = missing, -- = not applicable
6 +
7 + ## MNW Server (237 modules, 66,815 LOC)
8 +
9 + ### Creator Platform Features
10 +
11 + | Feature | Code Modules | Inline (//!) | Rustdoc | Public Doc |
12 + |---------|-------------|:---:|:---:|:---:|
13 + | **Auth (login/signup/sessions)** | routes/auth.rs, auth.rs, db/sessions.rs, db/auth.rs | Y | -- | guide/01-getting-started |
14 + | **Passkeys (WebAuthn)** | routes/api/passkeys.rs, db/passkeys.rs | Y | -- | guide/security |
15 + | **TOTP (2FA)** | routes/api/totp.rs, db/totp.rs | Y | -- | guide/security |
16 + | **User profiles** | routes/api/users/profile.rs, db/users.rs | Y | -- | guide/profile |
17 + | **Projects** | routes/api/projects.rs, db/projects.rs | Y | -- | guide/projects |
18 + | **Items (CRUD)** | routes/api/items/*.rs, db/items.rs | Y | -- | guide/items |
19 + | **Item sections (tabbed content)** | routes/api/items/sections.rs, db/item_sections.rs | Y | -- | N |
20 + | **Chapters** | routes/api/items/chapters.rs, db/chapters.rs | Y | -- | N |
21 + | **Bundles** | routes/api/items/bundles.rs, db/bundles.rs | Y | -- | N |
22 + | **Versions (file releases)** | routes/api/items/versions.rs, db/versions.rs | Y | -- | guide/files |
23 + | **Tags (hierarchical)** | routes/api/tags.rs, db/tags.rs | Y | -- | guide/tags |
24 + | **Collections** | routes/api/collections.rs, db/collections.rs | Y | -- | guide/collections |
25 + | **Blog** | routes/api/blog.rs, db/blog_posts.rs, pages/blog.rs | Y | -- | guide/blog |
26 + | **Custom links** | routes/api/links.rs, db/custom_links.rs | Y | -- | N |
27 + | **Labels** | routes/api/labels.rs, db/labels.rs | Y | -- | N |
28 + | **Categories** | routes/api/categories.rs, db/categories.rs | Y | -- | N |
29 + | **Follows** | routes/api/follows.rs, db/follows.rs | Y | -- | N |
30 + | **Discover** | pages/public/discover.rs, db/discover.rs | Y | -- | N |
31 + | **RSS feeds** | rss.rs, pages/feeds.rs | Y | -- | guide/rss |
32 +
33 + ### Payments & Commerce
34 +
35 + | Feature | Code Modules | Inline (//!) | Rustdoc | Public Doc |
36 + |---------|-------------|:---:|:---:|:---:|
37 + | **Stripe Connect** | routes/stripe/*.rs, payments/*.rs | Y | -- | guide/03-selling, guide/payouts |
38 + | **Checkout (items)** | routes/stripe/checkout/item.rs | Y | -- | guide/03-selling |
39 + | **Checkout (subscriptions)** | routes/stripe/checkout/subscriptions.rs | Y | -- | guide/tiers |
40 + | **Subscription tiers** | routes/api/subscriptions.rs, db/subscriptions.rs | Y | -- | guide/tiers |
41 + | **License keys** | routes/api/license_keys.rs, db/license_keys.rs | Y | -- | developer/license-keys |
42 + | **Promo codes** | routes/api/promo_codes.rs, db/promo_codes.rs | Y | -- | guide/promo-codes |
43 + | **Pricing models** | pricing.rs | Y | -- | guide/pricing |
44 + | **Transactions** | db/transactions.rs | Y | -- | guide/03-selling |
45 + | **Fan+ subscription** | db/fan_plus.rs | Y | -- | guide/fan-plus |
46 + | **Creator tiers (platform)** | db/creator_tiers.rs | Y | -- | guide/tiers |
47 + | **Analytics** | db/analytics.rs | Y | -- | guide/analytics |
48 + | **Exports (data portability)** | routes/api/exports.rs | Y | -- | guide/export |
49 +
50 + ### File Storage & Security
51 +
52 + | Feature | Code Modules | Inline (//!) | Rustdoc | Public Doc |
53 + |---------|-------------|:---:|:---:|:---:|
54 + | **S3 storage (upload/download)** | storage.rs, routes/storage/*.rs | Y | -- | guide/files |
55 + | **Media files** | routes/storage/media.rs, db/media_files.rs | Y | -- | N |
56 + | **Content insertions** | routes/api/content_insertions.rs, db/content_insertions.rs | Y | -- | N |
57 + | **File scanning (ClamAV/YARA)** | scanning/*.rs | Y | -- | tech/content-protection |
58 + | **Content fingerprinting** | fingerprint/*.rs | Y | -- | tech/content-protection |
59 + | **Video upload/playback** | (migration 053, FileType::Video) | Y | -- | N |
60 +
61 + ### Git & Code
62 +
63 + | Feature | Code Modules | Inline (//!) | Rustdoc | Public Doc |
64 + |---------|-------------|:---:|:---:|:---:|
65 + | **Git source browser** | routes/git/*.rs, git/*.rs | Y | -- | guide/git |
66 + | **Git issues (email-first)** | routes/git_issues/*.rs, db/issues.rs | Y | -- | N |
67 + | **SSH keys** | routes/api/ssh_keys.rs, db/ssh_keys.rs | Y | -- | N |
68 + | **Patches (git send-email)** | routes/postmark/patches.rs, db/patches.rs | Y | -- | N |
69 + | **Git repos** | db/git_repos.rs | Y | -- | N |
70 +
71 + ### Platform Infrastructure
72 +
73 + | Feature | Code Modules | Inline (//!) | Rustdoc | Public Doc |
74 + |---------|-------------|:---:|:---:|:---:|
75 + | **OAuth2 PKCE provider** | routes/oauth.rs, db/oauth.rs | Y | -- | developer/oauth |
76 + | **Email system** | email/*.rs | Y | -- | N |
77 + | **CSRF protection** | csrf.rs | Y | -- | tech/security |
78 + | **Custom domains** | routes/custom_domain.rs, routes/api/domains.rs, db/custom_domains.rs | Y | -- | guide/custom-domains |
79 + | **Mailing lists** | db/mailing_lists.rs | Y | -- | guide/mailing-lists |
80 + | **Import system** | import/*.rs, routes/api/imports.rs, db/imports.rs | Y | -- | guide/migration |
81 + | **Waitlist** | routes/api/users/waitlist.rs, db/waitlist.rs | Y | -- | N |
82 + | **Invites** | routes/api/users/invites.rs, db/invites.rs | Y | -- | N |
83 + | **Reports/moderation** | routes/api/reports.rs, db/reports.rs | Y | -- | legal/moderation |
84 + | **Health monitoring** | pages/public/health.rs, monitor.rs, db/health.rs, db/monitor.rs | Y | -- | tech/monitoring |
85 + | **Scheduler** | scheduler.rs | Y | -- | N |
86 + | **Admin panel** | routes/admin/*.rs, bin/mnw-admin.rs | Y | -- | N |
87 + | **Onboarding emails** | email/templates/onboarding.rs | Y | -- | N |
88 + | **MT client integration** | mt_client.rs | Y | -- | support/forums |
89 + | **DocEngine rendering** | markdown.rs, routes/pages/public/docs.rs | Y | -- | N |
90 +
91 + ### SyncKit Server
92 +
93 + | Feature | Code Modules | Inline (//!) | Rustdoc | Public Doc |
94 + |---------|-------------|:---:|:---:|:---:|
95 + | **SyncKit auth (JWT)** | synckit_auth.rs, routes/synckit/auth.rs | Y | -- | developer/synckit |
96 + | **SyncKit apps** | routes/synckit/apps.rs, db/synckit.rs | Y | -- | developer/synckit |
97 + | **SyncKit push/pull** | routes/synckit/sync.rs | Y | -- | developer/synckit |
98 + | **SyncKit blobs** | routes/synckit/blobs.rs | Y | -- | developer/synckit |
99 + | **SyncKit SSE push** | routes/synckit/subscribe.rs | Y | -- | developer/sse |
100 + | **OTA updates** | routes/ota.rs, db/ota.rs | Y | -- | developer/ota |
101 + | **Build pipeline** | routes/builds.rs, db/builds.rs, build_runner.rs | Y | -- | N |
102 +
103 + ---
104 +
105 + ## SyncKit Client SDK (13 modules, 5,431 LOC)
106 +
107 + | Feature | Code Module | Inline (//!) | Rustdoc | Public Doc |
108 + |---------|------------|:---:|:---:|:---:|
109 + | **Auth (OAuth PKCE + email/pw)** | client/auth.rs | Y | Y | developer/synckit |
110 + | **Push/pull sync** | client/sync.rs | Y | Y | developer/synckit |
111 + | **Blob upload/download** | client/blob.rs | Y | Y | developer/synckit |
112 + | **E2E encryption** | crypto.rs, client/encryption.rs | Y | Y | developer/synckit |
113 + | **OS keychain** | keystore.rs | Y | Y | N |
114 + | **Conflict resolution** | conflict.rs | Y | Y | N |
115 + | **SSE notifications** | client/subscribe.rs | Y | Y | N |
116 + | **Pull filtering** | types.rs (PullFilter) | Y | Y | N |
117 +
118 + ---
119 +
120 + ## Shared Libraries
121 +
122 + | Library | Modules | Inline (//!) | Rustdoc | Public Doc |
123 + |---------|---------|:---:|:---:|:---:|
124 + | **docengine** | 13 (2,259 LOC) | P (23%) | Y | N |
125 + | **tagtree** | 1 (1,871 LOC) | Y | Y | N |
126 + | **theme-common** | 1 (585 LOC) | Y | Y | N |
127 + | **s3-storage** | 1 (287 LOC) | Y | Y | N |
128 +
129 + ---
130 +
131 + ## Gap Summary
132 +
133 + ### Level 1: Inline Code Docs (//!)
134 + **Overall: 98% module coverage for MNW.** Nearly every module has `//!` headers. Scheduler and monitor expanded from single-line to full 4-5 line descriptions (2026-04-11). All other projects range from 23% (docengine) to 100% (tagtree).
135 +
136 + ### Level 2: Rustdoc
137 + **Generated for 5 crates: synckit-client, docengine, tagtree, s3-storage, theme-common.** Hosted at `/rustdoc`. s3-storage and theme-common added 2026-04-11. MNW server itself is not a library crate so rustdoc is less relevant — it's an application binary.
138 +
139 + ### Level 3: Public-Facing Docs (site-docs/)
140 + **56 docs across 7 categories.** 10 new docs added 2026-04-11. Strong coverage of core creator workflows plus developer integration points.
141 +
142 + **Remaining undocumented features:**
143 +
144 + | Feature | Priority | Reason |
145 + |---------|----------|--------|
146 + | Bundles | Medium | Commerce feature, no guide |
147 + | Item sections | Low | New feature, relatively self-explanatory UI |
148 + | Custom links | Low | Simple feature |
149 + | Build pipeline | Low | Internal/advanced developer feature |
150 + | Video playback | Low | New, deferred post-beta |
151 + | Chapters | Low | Self-explanatory UI |
152 + | Git issues (email-first) | Medium | Unique differentiator, no user-facing docs |
153 + | Discover page | Low | Self-explanatory UI |
154 +
155 + ### Cross-Reference Links (Level Linking)
156 +
157 + 11 source modules now have `//! See also:` lines linking to their corresponding public docs (added 2026-04-11). `developer/synckit.md` links to `/rustdoc/synckit_client/` (Level 3 -> Level 2).
158 +
159 + **Remaining:** Add "Source: `src/routes/...`" notes in public docs for developer-facing pages.
@@ -0,0 +1,230 @@
1 + # Filetype Transcoding Matrix
2 +
3 + Reference for MNW's ingest pipeline. Defines which formats can be safely converted, what saves space, and how conversions map to creator tiers.
4 +
5 + ---
6 +
7 + ## Design Principle
8 +
9 + - **BigFiles / Streaming tiers**: Store original file. Lossless preservation is the feature.
10 + - **SmallFiles tier**: Store a high-quality transparent encode. Humans can't tell the difference.
11 + - Originals are always kept for BigFiles+. SmallFiles stores only the optimized version.
12 + - Never transcode lossy-to-lossy. Never transcode lossy-to-lossless (wastes space, no quality gain).
13 +
14 + ---
15 +
16 + ## Audio Formats
17 +
18 + ### Format Properties
19 +
20 + | Format | Type | MB/min (stereo) | Notes |
21 + |--------|------|-----------------|-------|
22 + | WAV | Uncompressed PCM | ~10 (16-bit/44.1k), ~33 (24-bit/96k) | Baseline reference |
23 + | AIFF | Uncompressed PCM | ~10 (16-bit/44.1k) | WAV equivalent (Apple, big-endian) |
24 + | FLAC | Lossless compressed | ~5-6 (16-bit/44.1k) | ~50% of WAV. Bit-perfect. Open, royalty-free |
25 + | ALAC | Lossless compressed | ~5-6 (16-bit/44.1k) | ~50% of WAV. 5-10% larger than FLAC |
26 + | MP3 | Lossy | ~2.3 (320k), ~1.4 (192k), ~0.9 (128k) | Oldest, most compatible |
27 + | AAC | Lossy | ~1.9 (256k), ~1.0 (128k) | ~20-30% more efficient than MP3 at same bitrate |
28 + | OGG Vorbis | Lossy | ~1.2 (160k), ~1.0 (128k) | Open-source. Comparable to AAC |
29 + | Opus | Lossy | ~1.0 (128k), ~0.7 (96k) | Best lossy codec. Transparent at 128 kbps |
30 +
31 + ### Transparency Thresholds (indistinguishable from lossless)
32 +
33 + | Codec | Transparent bitrate | Conservative bitrate |
34 + |-------|--------------------|--------------------|
35 + | Opus | 128 kbps | 160 kbps |
36 + | AAC | 192 kbps | 256 kbps |
37 + | OGG Vorbis | 160 kbps | 192 kbps |
38 + | MP3 | 256 kbps | 320 kbps |
39 +
40 + ### Audio Conversion Matrix
41 +
42 + Source format on the left, target on the top. Each cell describes what happens.
43 +
44 + | Source | FLAC | Opus 128 | AAC 256 | MP3 320 |
45 + |--------|------|----------|---------|---------|
46 + | **WAV** | Lossless, saves ~50% | Transparent, saves ~90% | Transparent, saves ~81% | Transparent, saves ~77% |
47 + | **AIFF** | Lossless, saves ~50% | Transparent, saves ~90% | Transparent, saves ~81% | Transparent, saves ~77% |
48 + | **FLAC** | No-op | Transparent, saves ~83% | Transparent, saves ~68% | Transparent, saves ~61% |
49 + | **ALAC** | Lossless, saves ~5-10% | Transparent, saves ~83% | Transparent, saves ~68% | Transparent, saves ~61% |
50 + | **MP3** | NEVER (bigger file, no quality gain) | NEVER (lossy-to-lossy) | NEVER (lossy-to-lossy) | No-op |
51 + | **AAC** | NEVER (bigger file, no quality gain) | NEVER (lossy-to-lossy) | No-op | NEVER (lossy-to-lossy) |
52 + | **OGG** | NEVER (bigger file, no quality gain) | NEVER (lossy-to-lossy) | NEVER (lossy-to-lossy) | NEVER (lossy-to-lossy) |
53 + | **Opus** | NEVER (bigger file, no quality gain) | No-op | NEVER (lossy-to-lossy) | NEVER (lossy-to-lossy) |
54 +
55 + ### Audio Decision Tree
56 +
57 + ```
58 + Is the source lossless (WAV/AIFF/FLAC/ALAC)?
59 + YES:
60 + Tier = BigFiles/Streaming?
61 + -> Store as FLAC (lossless, ~50% smaller than WAV/AIFF)
62 + -> Keep original alongside if WAV/AIFF (creator can re-download)
63 + Tier = SmallFiles?
64 + -> Store as Opus 128 kbps (transparent, ~90% smaller than WAV)
65 + -> Alternatively AAC 256 kbps for broader device compat
66 + NO (source is lossy MP3/AAC/OGG/Opus):
67 + -> Store as-is. No conversion possible without degradation.
68 + -> File is already small. No space savings available.
69 + ```
70 +
71 + ---
72 +
73 + ## Video Formats
74 +
75 + ### Codec Properties
76 +
77 + | Codec | Container | MB/min (1080p, visually lossless) | Encoding speed | Browser support |
78 + |-------|-----------|-----------------------------------|----------------|-----------------|
79 + | H.264 | MP4 | 34-45 | Fast (baseline) | Universal |
80 + | H.265/HEVC | MP4 | 17-27 | 10-20x slower | Safari only (licensing) |
81 + | VP9 | WebM | 18-24 | 10-20x slower | Chrome, Firefox, Edge, Safari 14+ |
82 + | AV1 | MP4/WebM | 11-17 | 10-50x slower | Chrome, Firefox, Edge; Safari M3+ only |
83 + | ProRes | MOV | 100-200 | Fast (decode-optimized) | Safari only |
84 +
85 + ### Video Transparency Thresholds (CRF values, lower = better)
86 +
87 + | Codec | Transparent CRF | Notes |
88 + |-------|----------------|-------|
89 + | H.264 (x264) | CRF 18 | Well-established threshold |
90 + | H.265 (x265) | CRF 24 | Equivalent to x264 CRF 18 |
91 + | VP9 (libvpx) | CRF 18 | Range 0-63 |
92 + | AV1 (SVT-AV1) | CRF 23 | Range 0-63, equivalent to x264 CRF 19 |
93 +
94 + ### Compression Savings (same perceived quality, relative to H.264)
95 +
96 + | Codec | Size vs H.264 | Best for |
97 + |-------|---------------|----------|
98 + | H.265 | 40-50% smaller | Native apps, Apple ecosystem |
99 + | VP9 | 40-50% smaller | Web delivery (royalty-free) |
100 + | AV1 | 60-70% smaller | Web VOD (royalty-free, best compression) |
101 +
102 + ### Video Conversion Matrix
103 +
104 + | Source | H.264 CRF 18 | VP9 CRF 18 | AV1 CRF 23 |
105 + |--------|--------------|------------|-------------|
106 + | **MOV (ProRes)** | Transparent, saves ~80% | Transparent, saves ~85% | Transparent, saves ~90% |
107 + | **MOV (H.264)** | Remux only (no re-encode) | AVOID (lossy-to-lossy) | AVOID (lossy-to-lossy) |
108 + | **MP4 (H.264)** | No-op | AVOID (lossy-to-lossy) | AVOID (lossy-to-lossy) |
109 + | **MP4 (H.265)** | NEVER (bigger, lossy-to-lossy) | AVOID (lossy-to-lossy) | AVOID (lossy-to-lossy) |
110 + | **WebM (VP9)** | NEVER (bigger, lossy-to-lossy) | No-op | AVOID (lossy-to-lossy) |
111 +
112 + Note: "AVOID" means technically possible but always degrades quality. "NEVER" means it also increases file size.
113 +
114 + ### Video Decision Tree
115 +
116 + ```
117 + Is the source lossless/production (ProRes, uncompressed)?
118 + YES:
119 + Tier = BigFiles/Streaming?
120 + -> Re-encode to H.264 CRF 18 in MP4 (universal playback)
121 + -> Optionally also generate VP9/AV1 for web streaming
122 + -> Keep original? Only if tier storage allows it
123 + Tier = SmallFiles?
124 + -> Re-encode to VP9 CRF 18 in WebM (transparent, ~85% smaller)
125 + -> Or AV1 CRF 23 (best compression, but slow encode)
126 + NO (source is already lossy H.264/H.265/VP9):
127 + Can we remux without re-encoding? (e.g., H.264 in MOV -> H.264 in MP4)
128 + YES -> Remux (zero quality loss, trivial CPU cost)
129 + NO -> Store as-is. No conversion possible without degradation.
130 + ```
131 +
132 + ---
133 +
134 + ## Image Formats (for completeness)
135 +
136 + | Source | WebP (lossy 90) | WebP (lossless) | Notes |
137 + |--------|----------------|-----------------|-------|
138 + | **PNG** | Transparent, saves ~70-90% | Saves ~25-35% | Lossless-to-lossless safe |
139 + | **JPEG** | Marginal savings, quality loss | Larger than original | Store as-is |
140 + | **WebP** | No-op | No-op | Already optimal |
141 + | **GIF** | Transparent, saves ~50-80% | Saves ~20-40% | Can also convert to WebP anim |
142 +
143 + Cover images and media library images are small (10 MB cap). Savings are negligible at platform scale. Not worth the complexity.
144 +
145 + ---
146 +
147 + ## Tier-Based Storage Strategy
148 +
149 + ### SmallFiles ($20/mo) — "Transparent quality, efficient storage"
150 +
151 + | Upload type | Stored as | Savings vs original |
152 + |-------------|-----------|-------------------|
153 + | WAV/AIFF audio | Opus 128 kbps (or AAC 256) | ~90% smaller |
154 + | FLAC/ALAC audio | Opus 128 kbps (or AAC 256) | ~83% smaller |
155 + | Lossy audio (MP3/AAC/OGG) | As-is | None (already small) |
156 + | ProRes/uncompressed video | VP9 CRF 18 | ~85% smaller |
157 + | Lossy video (H.264/VP9) | As-is | None |
158 +
159 + Fan downloads: the stored version (transparent quality).
160 +
161 + ### BigFiles ($30/mo) — "Lossless preservation"
162 +
163 + | Upload type | Stored as | Savings vs original |
164 + |-------------|-----------|-------------------|
165 + | WAV/AIFF audio | FLAC + original preserved | ~50% on the FLAC copy |
166 + | FLAC/ALAC audio | As-is (already lossless compressed) | None needed |
167 + | Lossy audio (MP3/AAC/OGG) | As-is | None |
168 + | ProRes/uncompressed video | H.264 CRF 18 + original preserved | ~80% on the delivery copy |
169 + | Lossy video (H.264/VP9) | As-is | None |
170 +
171 + Fan downloads: choice of original lossless or delivery format.
172 +
173 + ### Streaming ($40/mo) — "Lossless + adaptive streaming"
174 +
175 + Same as BigFiles, plus:
176 + - Multiple quality tiers generated for adaptive streaming (HLS/DASH)
177 + - E.g., audio: FLAC + Opus 128 + Opus 64
178 + - E.g., video: original + 1080p VP9 + 720p VP9 + 480p VP9
179 +
180 + ---
181 +
182 + ## Storage Impact Estimates
183 +
184 + Per 4-minute stereo song (16-bit/44.1kHz source):
185 +
186 + | Strategy | Size | vs WAV original |
187 + |----------|------|-----------------|
188 + | Store WAV (current, no transcode) | ~40 MB | baseline |
189 + | Store FLAC (BigFiles ingest) | ~22 MB | 45% smaller |
190 + | Store Opus 128 (SmallFiles ingest) | ~3.8 MB | 90% smaller |
191 + | Store AAC 256 (SmallFiles alt) | ~7.7 MB | 81% smaller |
192 +
193 + Per 10-minute 1080p video (H.264 source at 8 Mbps):
194 +
195 + | Strategy | Size | Notes |
196 + |----------|------|-------|
197 + | Store as-is | ~600 MB | Already lossy, no savings |
198 + | Store ProRes original as VP9 | ~200 MB | Only saves if source is ProRes |
199 +
200 + Key insight: **the biggest wins are on lossless audio uploads (WAV/AIFF)**. Lossy uploads (which are likely the majority at launch) can't be improved.
201 +
202 + ---
203 +
204 + ## Implementation Notes
205 +
206 + ### Required tooling
207 + - `ffmpeg` / `ffprobe` on the server for all transcoding
208 + - Probe uploaded file on confirm to detect actual codec (M4A can be AAC or ALAC)
209 + - Transcoding is async (background job after upload confirm, not blocking)
210 +
211 + ### Probing before deciding
212 + - M4A: must probe to distinguish AAC (lossy, store as-is) from ALAC (lossless, can optimize)
213 + - MOV: must probe to distinguish ProRes (lossless, can optimize) from H.264 (lossy, store as-is)
214 + - OGG: almost always Vorbis (lossy), but could theoretically be FLAC-in-OGG
215 +
216 + ### What NOT to do
217 + - Never transcode lossy-to-lossy (MP3 -> AAC, H.264 -> VP9, etc.)
218 + - Never transcode lossy-to-lossless (MP3 -> FLAC) — bigger file, identical quality
219 + - Never re-encode at a higher bitrate thinking it improves quality (it doesn't)
220 + - Never delete the only copy before transcoding completes successfully
221 +
222 + ---
223 +
224 + ## Key Paths
225 +
226 + - Upload/validation logic: `src/storage.rs`
227 + - Upload endpoints: `src/routes/storage/uploads.rs`
228 + - Creator tier enforcement: `src/db/creator_tiers.rs`
229 + - Tier definitions: `src/db/enums.rs` (CreatorTier enum)
230 + - Phase 14D in todo: `docs/todo.md`
M docs/todo.md +91 -32
@@ -1,9 +1,9 @@
1 1 # Makenotwork TODO
2 2
3 3 ## Status
4 - Done: All pre-beta phases + frontend audit + content fingerprinting + bundled license text + video upload/playback + maintainability splits + S3 storage extraction + item sections + Phase 13D-A import system. Active: None. Next: Deploy v0.3.22, then post-beta features below.
4 + Done: All pre-beta phases + frontend audit + content fingerprinting + bundled license text + video upload/playback + maintainability splits + S3 storage extraction + item sections + Phase 13D-A import system + tag taxonomy expansion (migration 056) + OTA slug dashboard UI + codebase harness (MCP + SQLite index) + doc coverage remediation (10 new docs, rustdoc expansion, cross-linking) + content seeding (tags, blog posts, changelog project, collection) + doc deploy (all 55 pages verified) + DocEngine custom directives (extensible alerts + code tabs) + SyncKit/OTA tests pass (20/20 on astra). Active: Creator setup (Stripe), manual testing. Next: Soft launch.
5 5
6 - Live at makenot.work. v0.3.21. Audit grade A. Stripe + Postmark live. All platform integrations (I1-I5) deployed.
6 + Live at makenot.work. v0.3.22 (deployed 2026-04-10). Audit grade A. Stripe + Postmark live. All platform integrations (I1-I5) deployed. 34 tags in taxonomy.
7 7
8 8 **Scope:** Sections tagged `(pre-beta)` ship before initial beta. Untagged sections are post-beta.
9 9
@@ -11,6 +11,19 @@ Completed phases archived in `docs/archive/mnw_todo_done.md`.
11 11
12 12 ---
13 13
14 + ## Code Review Remediation (2026-04-12)
15 +
16 + ### Done
17 + - [x] Fix ServiceAuth to use `constant_time_compare()` (auth.rs:225)
18 + - [x] Fix 19 clippy warnings: collapsible_if → let chains (12 files), too_many_arguments (1), while→for (1), load test fix (1)
19 + - [x] helpers.rs (767) and pricing.rs (705) — confirmed within 500-line branching guideline (inflated by test suites, not branching logic)
20 +
21 + ### Deferred
22 + - [ ] Monitor scheduler.rs (635), git/mod.rs (613), license_keys.rs (684) for growth
23 + - [ ] Consider splitting bin/mnw-admin.rs git-auth commands into separate module
24 +
25 + ---
26 +
14 27 ## External Blockers
15 28
16 29 | Blocker | Status | Blocks |
@@ -49,9 +62,9 @@ Copy and configuration pre-generated in `docs/content_seed.md`.
49 62 - [x] Set cover image
50 63 - [x] Upload signed+notarized DMG
51 64 - [ ] Create subscription tier: "Cloud Sync" ($3/mo) — not yet created
52 - - [ ] Add tags (planned: productivity, tasks, email, calendar, macos, desktop, rust)
53 - - [ ] Write launch blog post
54 - - [ ] Register OTA slug `goingson`
65 + - [x] Add tags (productivity, tasks, email, calendar, macos, desktop, rust)
66 + - [x] Write launch blog post
67 + - [x] Register OTA slug `goingson`
55 68
56 69 #### Project: audiofiles (one-time purchase)
57 70 - [x] Create project (slug: audiofiles)
@@ -60,10 +73,10 @@ Copy and configuration pre-generated in `docs/content_seed.md`.
60 73 - [x] Set cover image
61 74 - [x] Upload signed+notarized plugin bundle
62 75 - [ ] Enable license keys (test activation flow)
63 - - [ ] Add tags (planned: audio, samples, plugin, clap, vst3, music-production, macos, daw)
64 - - [ ] Write launch blog post
76 + - [x] Add tags (audio, samples, plugin, clap, vst3, music-production, macos, daw)
77 + - [x] Write launch blog post
65 78 - [ ] Create a test discount code (e.g. LAUNCH50, 50% off)
66 - - [ ] Register OTA slug `audiofiles`
79 + - [x] Register OTA slug `audiofiles`
67 80
68 81 #### Project: Balanced Breakfast
69 82 - [x] Create project (slug: balanced-breakfast)
@@ -71,26 +84,41 @@ Copy and configuration pre-generated in `docs/content_seed.md`.
71 84 - [x] Create content item
72 85 - [x] Set cover image
73 86 - [x] Upload signed+notarized DMG
74 - - [ ] Add tags (planned: rss, feeds, reader, macos, desktop, rust)
75 - - [ ] Write launch blog post
76 - - [ ] Register OTA slug `balanced-breakfast`
87 + - [x] Add tags (rss, feeds, reader, macos, desktop, rust)
88 + - [x] Write launch blog post
89 + - [x] Register OTA slug `balanced-breakfast`
77 90
78 91 #### Project: Changelog (platform development log)
79 92 - [x] `/changelog` and `/changelog/{post_slug}` route aliases (`routes/pages/blog.rs`, `constants.rs`)
80 - - [ ] Create project (slug: changelog, type: blog)
81 - - [ ] Write description
82 - - [ ] Write "What this is" blog post
93 + - [x] Create project (slug: changelog, type: blog)
94 + - [x] Write description
95 + - [x] Write "What this is" blog post
83 96
84 97 #### Cross-Project
85 - - [ ] Create "All Apps" collection (GO + AF + BB)
98 + - [x] Create "All Apps" collection (GO + AF + BB)
86 99 - [ ] Add custom links (source code link, support@makenot.work — currently profile has Twitter/Mastodon/htpy.app)
87 - - [ ] Verify all 4 projects appear on `/discover` (currently 3 — changelog blog missing)
100 + - [x] Verify all 4 projects appear on `/discover`
88 101 - [ ] Test free download flow (GO), PWYW flow (BB), purchase flow (AF), subscription flow (GO)
89 102 - [ ] Test discount code on AF purchase
90 103 - [ ] Test license key delivery after AF purchase
91 104 - [ ] Capture screenshots for docs (dashboard, audio player, discover, pricing, git browser)
92 105
93 106 ### Documentation
107 +
108 + #### Done
109 + - [x] Rustdoc expansion: added s3-storage and theme-common to `deploy/generate-rustdoc.sh` (now 5 crates)
110 + - [x] Inline doc expansion: scheduler.rs, monitor.rs `//!` headers expanded to 4-5 lines
111 + - [x] Cross-linking: 11 source files now have `//! See also:` linking to public docs
112 + - [x] New guide docs: security, blog, collections, export, promo-codes, git, custom-domains, mailing-lists, fan-plus
113 + - [x] New developer doc: sse (SyncKit SSE push notifications)
114 + - [x] SUBCATEGORIES updated in `docs.rs` with new slugs + "Advanced" subcategory
115 +
116 + #### Remaining
117 + - [x] Register `tech` section in `main.rs` DocLoader config (7 tech/ docs exist but weren't served)
118 + - [x] Deploy docs to production (`deploy.sh --config` — rsync site-docs/, static/, rustdoc)
119 + - [x] Verify all 55 doc pages render at `makenot.work/docs/{slug}`
120 + - [x] Verify all internal links resolve (no 404s)
121 + - [ ] Review new docs against live UI for accuracy (button labels, navigation paths)
94 122 - [ ] liability.md legal review (has [PENDING LEGAL REVIEW] placeholders)
95 123 - [ ] dmca-counter.md designated agent address (needs DMCA agent registration)
96 124
@@ -241,8 +269,8 @@ Per-item license configuration: creators pick from 7 presets or write custom ter
241 269
242 270 Competitive context: Gumroad has per-product affiliates with configurable rates. Bandcamp and itch.io have no affiliate programs. This is a moderate gap — most valuable for software and course creators.
243 271
244 - ### Phase 13B: Labels — Remaining
245 - - [ ] Publish/update reminder: show applied labels summary when publishing
272 + ### Phase 13B: Labels
273 + - [x] Publish/update reminder: show applied labels summary when publishing
246 274
247 275 ### Phase 13D: Creator Platform Import System
248 276 Migration 055. 28 unit tests + 8 integration tests. Three-phase build: A (infra + CSV), B (Substack + Ghost), C (Gumroad + Bandcamp + Lemon Squeezy + Patreon).
@@ -284,14 +312,42 @@ Migration 053. 6 integration tests.
284 312 - [x] Scanning: content-type verification for video
285 313 - [x] Data export includes video fields
286 314
287 - ### Phase 14E: Video Streaming Tier Features (post-beta)
288 - Streaming tier ($40/mo) premium features. Trigger: >100 creators with video content.
289 - - [ ] Server-side transcoding (ffmpeg): upload any format, serve optimized MP4/WebM
315 + ### Phase 14E: Media Transcoding Pipeline (post-beta)
316 + Tier-based ingest transcoding. Reference: `docs/filetype_matrix.md`.
317 +
318 + Core rule: never transcode lossy-to-lossy or lossy-to-lossless. Only optimize lossless sources.
319 +
320 + #### Phase 14E-1: Probe + Detect Infrastructure
321 + - [ ] Add `ffprobe` to production server (detect codec inside M4A/MOV containers)
322 + - [ ] Probe uploaded files at confirm time: extract codec, bitrate, sample rate, duration, dimensions
323 + - [ ] Classify uploads as lossless (WAV/AIFF/FLAC/ALAC/ProRes) or lossy (MP3/AAC/OGG/Opus/H.264/VP9)
324 + - [ ] Auto-populate `duration_seconds`, `video_width`, `video_height` from probe data
325 + - [ ] Store detected codec + source classification in DB (new columns or metadata JSON)
326 +
327 + #### Phase 14E-2: Audio Transcoding (SmallFiles tier)
328 + - [ ] Background job: lossless audio uploads -> Opus 128 kbps (SmallFiles) or FLAC (BigFiles+)
329 + - [ ] WAV/AIFF -> FLAC saves ~50%; WAV/AIFF -> Opus 128 saves ~90%
330 + - [ ] FLAC/ALAC -> Opus 128 saves ~83% (SmallFiles only)
331 + - [ ] Lossy uploads (MP3/AAC/OGG/Opus): store as-is, no conversion
332 + - [ ] Track `original_s3_key` + `delivery_s3_key` per file (originals kept for BigFiles+)
333 +
334 + #### Phase 14E-3: Fan Download Format Choice
335 + - [ ] Fan chooses download format at purchase time (original lossless, FLAC, MP3 320, AAC 256)
336 + - [ ] Pre-generate common formats on first request, cache in S3
337 + - [ ] SmallFiles: delivery format only (transparent Opus/AAC). BigFiles+: original + delivery formats
338 +
339 + #### Phase 14E-4: Video Transcoding
340 + - [ ] ProRes/uncompressed MOV -> H.264 CRF 18 MP4 (universal playback)
341 + - [ ] Optionally generate VP9 for web streaming (40-50% smaller than H.264)
342 + - [ ] Lossy video (H.264/VP9/H.265): store as-is, no re-encode
343 + - [ ] Remux where possible (H.264 in MOV -> H.264 in MP4, zero quality loss)
344 + - [ ] Auto-generated thumbnails (ffmpeg frame extraction at configurable timestamp)
345 +
346 + #### Phase 14E-5: Streaming Tier Features
290 347 - [ ] Adaptive bitrate streaming (HLS/DASH) — multiple quality levels per video
291 - - [ ] Background lossless re-encoding: re-mux uploaded videos to more efficient containers, optimize keyframes, strip unused metadata. No quality loss, just smaller files
348 + - [ ] Audio: FLAC + Opus 128 + Opus 64 quality ladder
349 + - [ ] Video: original + 1080p + 720p + 480p quality ladder (VP9 or AV1)
292 350 - [ ] Bandwidth metering + per-tier bandwidth caps
293 - - [ ] Auto-generated thumbnails (ffmpeg frame extraction at configurable timestamp)
294 - - [ ] Video duration auto-detection on upload (ffprobe)
295 351
296 352 ### Phase 14B: Embeddable Widgets
297 353 - [ ] Embed endpoint /embed/i/{uuid} — embeddable buy button, audio player, 30-sec preview
@@ -300,10 +356,8 @@ Streaming tier ($40/mo) premium features. Trigger: >100 creators with video cont
300 356
301 357 Competitive context: Gumroad has overlay + inline + WordPress plugin. Bandcamp has customizable player widget. itch.io has purchase widget + playable game embed. This is a significant gap for creators selling from their own sites.
302 358
303 - ### Phase 14D: Audio Format Transcoding
304 - - [ ] Upload lossless (WAV/AIFF/FLAC), serve in multiple formats (MP3 320, FLAC, AAC, OGG)
305 - - [ ] Fan chooses download format at purchase time
306 - - [ ] Server-side transcoding pipeline (ffmpeg or similar)
359 + ### Phase 14D: Audio Format Transcoding — Merged into 14E
360 + Superseded by Phase 14E (Media Transcoding Pipeline), which covers audio + video transcoding with tier-based strategy. See `docs/filetype_matrix.md` for the full format compatibility matrix.
307 361
308 362 Competitive context: Bandcamp accepts WAV/AIFF/FLAC only, then transcodes to 8 download formats. MNW currently serves files as uploaded — a musician uploading WAV can't offer MP3 to fans who want it.
309 363
@@ -384,8 +438,8 @@ $8/mo consumer subscription. $5 monthly credit, `+` badge, platform polls, dev c
384 438 - [ ] Dev community as private MT community (invite-only, restricted to Fan+ subscribers — reuse MT private communities feature)
385 439
386 440 ### DocEngine — Remaining
387 - Extraction done (2026-03-21). `Shared/docengine/`, 100 tests, all 5 projects migrated.
388 - - [ ] Custom section directives (callouts, warnings, code tabs)
441 + Extraction done (2026-03-21). `Shared/docengine/`, 141 tests, all 5 projects migrated.
442 + - [x] Custom section directives: extensible `[!TYPE]` alerts (any uppercase word) + `[!TABS]` code tabs with language-labelled tab bar
389 443 - [ ] Full-text search index (build at load time, JSON endpoint for client-side search)
390 444 - [ ] Versioned docs (directory per version, version switcher)
391 445
@@ -434,9 +488,10 @@ Archive policy: items on platform 12+ months stay hosted if creator cancels.
434 488 - [ ] Verify Postmark DKIM selector `20170907043118pm` in Postmark dashboard (returned empty in Run 6 DNS check)
435 489
436 490 ## Dependencies (blocked on upstream)
491 + - [ ] Monitor yara-x for wasmtime >=42.0.2 (11 CVEs including 2 critical — RUSTSEC-2026-0095, -0096)
437 492 - [ ] Monitor aws-sdk-s3 for lru fix (RUSTSEC-2026-0002)
438 493 - [ ] Monitor async-stripe for instant fix (RUSTSEC-2024-0384)
439 - - [ ] rsa (RUSTSEC-2023-0071) via sqlx-mysql — non-issue (MNW uses Postgres)
494 + - [ ] rsa (RUSTSEC-2023-0071) via sqlx-mysql + yara-x — no fix available
440 495
441 496 ## Deferred
442 497 - [ ] Corporate structure: holding company (Makecreative Holdings LLC) with subsidiary LLCs for liability isolation if itsall.work launches. Setup checklist, Stripe contingency plan, entity details documented.
@@ -470,10 +525,14 @@ MNW/src/
470 525 import/ (CSV converter, pipeline, intermediate format)
471 526 MNW/tests/
472 527 integration.rs, harness/, workflows/*.rs
473 - MNW/migrations/ (001-055)
528 + MNW/migrations/ (001-056)
474 529 MNW/templates/
475 530 MNW/deploy/
476 531 MNW/site-docs/public/, MNW/site-docs/unpublished/
532 + _meta/harness/ (codebase index: SQLite DB + MCP server)
533 + codebase.db, server/src/{index,populate,schema}.ts
534 + MNW/docs/audit.md (cross-project audit instructions, harness-integrated)
535 + MNW/docs/filetype_matrix.md (format compatibility, transcoding rules, tier strategy)
477 536 ```
478 537
479 538 ## Deps
@@ -0,0 +1,15 @@
1 + -- Media library: user-scoped files for embedding in markdown content.
2 + CREATE TABLE media_files (
3 + id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4 + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
5 + folder VARCHAR(100) NOT NULL DEFAULT '',
6 + filename VARCHAR(255) NOT NULL,
7 + s3_key VARCHAR(500) NOT NULL UNIQUE,
8 + content_type VARCHAR(100) NOT NULL,
9 + file_size_bytes BIGINT NOT NULL DEFAULT 0,
10 + media_type VARCHAR(10) NOT NULL CHECK (media_type IN ('image', 'video')),
11 + created_at TIMESTAMPTZ NOT NULL DEFAULT now()
12 + );
13 +
14 + CREATE INDEX idx_media_files_user_folder ON media_files(user_id, folder);
15 + CREATE UNIQUE INDEX idx_media_files_user_folder_name ON media_files(user_id, folder, filename);
@@ -0,0 +1,87 @@
1 + # SyncKit SSE Push Notifications
2 +
3 + SyncKit provides a Server-Sent Events (SSE) endpoint that notifies connected clients in real time when new data is available. Instead of polling, your app can subscribe to a persistent connection and pull only when something changes.
4 +
5 + ## Endpoint
6 +
7 + ```
8 + GET /api/sync/subscribe?app_id={app_id}
9 + Authorization: Bearer <token>
10 + ```
11 +
12 + Requires a valid SyncKit JWT token. The `app_id` query parameter specifies which sync app to subscribe to.
13 +
14 + ## Event Format
15 +
16 + When another device pushes changes, all other connected devices for the same app and user receive a `changed` event:
17 +
18 + ```
19 + event: changed
20 + data:
21 + ```
22 +
23 + The event carries no payload. Data content is always encrypted end-to-end and never transmitted over the SSE channel. On receiving a `changed` event, the client should initiate a standard pull to fetch the actual changes.
24 +
25 + ## Keepalive
26 +
27 + The server sends a comment line every 30 seconds to keep the connection alive and prevent proxy timeouts:
28 +
29 + ```
30 + : keepalive
31 + ```
32 +
33 + This is a standard SSE comment and is ignored by conforming clients.
34 +
35 + ## Client Integration
36 +
37 + ### Basic Flow
38 +
39 + 1. Authenticate and obtain a SyncKit JWT token
40 + 2. Open an SSE connection to `/api/sync/subscribe?app_id={app_id}`
41 + 3. On receiving a `changed` event, call the pull endpoint to fetch new changes
42 + 4. Reconnect on connection drop (most SSE libraries handle this automatically)
43 +
44 + ### With the Rust SDK
45 +
46 + The `synckit-client` crate provides `SyncNotifyStream` for SSE integration:
47 +
48 + ```rust
49 + let stream = client.subscribe_notifications().await?;
50 +
51 + // In your sync loop:
52 + while let Some(notification) = stream.next().await {
53 + match notification {
54 + SyncNotification::Changed => {
55 + client.pull().await?;
56 + }
57 + SyncNotification::Keepalive => {}
58 + }
59 + }
60 + ```
61 +
62 + The SDK handles reconnection, keepalive parsing, and token refresh automatically.
63 +
64 + ### Selective Sync
65 +
66 + Combine SSE notifications with `PullFilter` to pull only specific tables or time ranges:
67 +
68 + ```rust
69 + let filter = PullFilter::new()
70 + .table("tasks")
71 + .since(last_sync_time);
72 +
73 + let changes = client.pull_filtered(filter).await?;
74 + ```
75 +
76 + This reduces bandwidth and processing when your app only needs a subset of changes.
77 +
78 + ## Design Notes
79 +
80 + - One SSE connection per (app, user) pair
81 + - The broadcast channel buffers up to 16 messages. If a client falls behind, missed events are skipped — the next pull will catch up regardless.
82 + - SSE works through standard HTTP proxies and load balancers. The 30-second keepalive interval is chosen to stay within common proxy timeout defaults.
83 +
84 + ## See Also
85 +
86 + - [SyncKit Cloud Sync](./synckit.md) — Full sync API reference
87 + - [OAuth2 PKCE](./oauth.md) — Authentication for SyncKit apps
@@ -0,0 +1,46 @@
1 + # Blog Posts
2 +
3 + Every project can have a blog for updates, announcements, behind-the-scenes writing, and anything you want to share with your audience. Blog posts are separate from items — they are free-form writing, not releases.
4 +
5 + ## Creating a Blog Post
6 +
7 + 1. Go to your project dashboard
8 + 2. Click "New Blog Post"
9 + 3. Enter a title and write your content in Markdown
10 + 4. Choose to publish immediately or schedule for later
11 +
12 + Blog posts support full Markdown: headings, lists, links, images, code blocks, and inline formatting.
13 +
14 + ## Scheduling
15 +
16 + Set a future publish date and time to schedule a post. Scheduled posts are not visible to the public until their publish time arrives. The background scheduler checks periodically and publishes them automatically.
17 +
18 + From the dashboard you can see all scheduled posts and their publish dates. Edit or reschedule at any time before publication.
19 +
20 + ## Discussion Threads
21 +
22 + When a blog post is published, a discussion thread is automatically created on Multithreaded (the integrated forum). A link to the thread appears at the bottom of the blog post, and readers can comment there.
23 +
24 + ## The Changelog
25 +
26 + The platform changelog at `/changelog` is a special blog tied to the Makenot.work project itself. It uses the same blog system. If you navigate to `/changelog`, you are reading the platform's own blog.
27 +
28 + Your project blogs live at `/p/your-project/blog`.
29 +
30 + ## RSS
31 +
32 + Blog posts are included in your project's blog RSS feed at `/p/your-project/blog/rss`. Anyone subscribed to that feed gets notified when you publish a new post.
33 +
34 + The blog RSS feed is separate from the project's item RSS feed at `/p/your-project/rss`.
35 +
36 + ## Visibility
37 +
38 + - **Published posts** are visible to anyone
39 + - **Draft posts** are visible only to you (the project owner)
40 + - **Scheduled posts** are visible only to you until their publish time
41 +
42 + ## See Also
43 +
44 + - [RSS Feeds](./rss.md) — How RSS works for items and blog posts
45 + - [Content & Items](./02-content.md) — Creating and managing items
46 + - [Projects](./projects.md) — Project setup and management
@@ -0,0 +1,46 @@
1 + # Collections
2 +
3 + Collections let you group items across projects into curated lists. Use them for "Best of" compilations, themed bundles, staff picks, or any other grouping that cuts across your normal project structure.
4 +
5 + ## Creating a Collection
6 +
7 + 1. Go to your dashboard
8 + 2. Click "New Collection"
9 + 3. Enter a title, an optional description, and a URL slug
10 + 4. Choose whether the collection is public or private
11 +
12 + You can create up to 100 collections.
13 +
14 + ## Adding Items
15 +
16 + Add any of your published (public) items to a collection:
17 +
18 + 1. Open the collection from your dashboard
19 + 2. Click "Add Item"
20 + 3. Select from your published items
21 +
22 + Each collection can hold up to 1,000 items. Only public items can be added — draft or unlisted items are not eligible.
23 +
24 + You can also add items to collections from the item edit page, which shows a checklist of your existing collections.
25 +
26 + ## Reordering
27 +
28 + Items within a collection can be reordered by drag-and-drop on the collection edit page. The order you set is the order fans see.
29 +
30 + ## Public URLs
31 +
32 + Public collections are visible at your profile. Fans can browse the collection and access any item in it. Private collections are only visible to you.
33 +
34 + ## Editing and Deleting
35 +
36 + From the collection edit page you can:
37 +
38 + - **Update** the title, description, or visibility
39 + - **Remove** individual items without affecting the items themselves
40 + - **Delete** the entire collection (items are not deleted, only the grouping)
41 +
42 + ## See Also
43 +
44 + - [Content & Items](./02-content.md) — Creating and managing items
45 + - [Tags](./tags.md) — Organizing items with tags
46 + - [Projects](./projects.md) — Project-level organization
@@ -0,0 +1,53 @@
1 + # Custom Domains
2 +
3 + You can point your own domain at your Makenot.work profile so fans reach you at `yourdomain.com` instead of `makenot.work/u/username`.
4 +
5 + ## Setup
6 +
7 + ### 1. Add Your Domain
8 +
9 + 1. Go to Settings > Domain
10 + 2. Enter your domain (e.g., `shop.yourdomain.com` or `yourdomain.com`)
11 + 3. Click "Add"
12 +
13 + Domain names must be valid hostnames: letters, numbers, hyphens, and at least one TLD. You cannot add `makenot.work` or its subdomains.
14 +
15 + ### 2. Verify Ownership
16 +
17 + Add a DNS TXT record to prove you own the domain:
18 +
19 + | Record Type | Host | Value |
20 + |-------------|------|-------|
21 + | TXT | `_mnw-verify.yourdomain.com` | `mnw-verify-{verification-code}` |
22 +
23 + The verification code is shown on the settings page after adding your domain.
24 +
25 + After adding the DNS record, return to Settings > Domain and click "Verify." The platform checks via DNS-over-HTTPS (Cloudflare resolver) so propagation is usually fast.
26 +
27 + ### 3. SSL Certificate
28 +
29 + Once verified, SSL is handled automatically. The platform uses on-demand TLS — a certificate is issued for your domain the first time a visitor connects. No manual certificate configuration needed.
30 +
31 + ## DNS Configuration
32 +
33 + Point your domain to Makenot.work by adding a CNAME record:
34 +
35 + | Record Type | Host | Value |
36 + |-------------|------|-------|
37 + | CNAME | `yourdomain.com` (or subdomain) | `makenot.work` |
38 +
39 + If your DNS provider does not allow a CNAME on the apex domain, use their CNAME flattening or ALIAS record feature, or use a subdomain like `shop.yourdomain.com`.
40 +
41 + ## Managing Your Domain
42 +
43 + From Settings > Domain you can:
44 +
45 + - **View** your current domain and verification status
46 + - **Remove** your domain to revert to the default `makenot.work/u/username` URL
47 +
48 + One domain per account. To change domains, remove the current one and add the new one.
49 +
50 + ## See Also
51 +
52 + - [Profile](./profile.md) — Editing your public profile
53 + - [Getting Started](./01-getting-started.md) — Initial account setup
@@ -0,0 +1,60 @@
1 + # Data Export
2 +
3 + You can export all of your data from Makenot.work at any time. No lock-in, no friction — your data is yours.
4 +
5 + ## What You Can Export
6 +
7 + | Export | Format | Contents |
8 + |--------|--------|----------|
9 + | Projects & Items | JSON | All projects, items, tags, chapters, versions, license keys, promo codes, and blog posts |
10 + | Sales | CSV | Date, item, amount, status, and buyer email for every sale |
11 + | Purchases | CSV | Date, item, amount, and status for everything you have bought |
12 + | Followers & Subscribers | CSV | Usernames, display names, types, status, and subscription dates |
13 + | Content Files | ZIP | All uploaded files (audio, video, covers, versions, insertion clips) with a manifest |
14 +
15 + ## How to Export
16 +
17 + 1. Go to Settings > Data
18 + 2. Choose the export type
19 + 3. Click "Export"
20 +
21 + JSON and CSV exports download immediately. Content file exports are assembled into a ZIP archive and provided as a temporary download link (valid for one hour).
22 +
23 + ## Content File Export
24 +
25 + The content file export bundles all files you have uploaded to the platform. It includes a `README.txt` manifest listing every file and its original context.
26 +
27 + Limits:
28 +
29 + - Maximum 500 files per export
30 + - Maximum 2 GB total size
31 +
32 + If you have more content than these limits allow, export in batches by project.
33 +
34 + ## Export Format Details
35 +
36 + ### Projects JSON
37 +
38 + The JSON export is a single file containing a complete snapshot of your creative work:
39 +
40 + - Projects with descriptions, settings, and metadata
41 + - Items with all fields: title, description, pricing, visibility, tags, chapters, versions
42 + - Blog posts with full Markdown content and publish status
43 + - License keys associated with each item
44 + - Promo codes you have created
45 +
46 + This format is designed to be readable and portable. You can parse it with any JSON tool.
47 +
48 + ### CSV Exports
49 +
50 + CSV files use standard comma-separated format with a header row. They open directly in any spreadsheet application.
51 +
52 + ## Platform Portability
53 +
54 + Data export is a core commitment. If you ever decide to leave, you take everything with you — content files, metadata, sales history, and audience data. Nothing is held hostage.
55 +
56 + ## See Also
57 +
58 + - [Content & Items](./02-content.md) — What gets exported
59 + - [Selling & Audience](./03-selling.md) — Sales and revenue data
60 + - [Migration](./migration.md) — Moving to Makenot.work from another platform
@@ -0,0 +1,40 @@
1 + # Fan+ Subscription
2 +
3 + Fan+ is a platform-wide subscription for fans who want to support the Makenot.work ecosystem and get access to premium features.
4 +
5 + ## What Fans Get
6 +
7 + Fan+ subscribers receive:
8 +
9 + - **Credit system** — Monthly credits that can be used toward purchases on the platform
10 + - **Premium access** — Features and perks that are gated behind Fan+ membership
11 + - **Platform support** — Direct contribution to keeping the platform running with 0% creator fees
12 +
13 + Fan+ is separate from creator subscription tiers. A fan can subscribe to individual creators *and* be a Fan+ member — they serve different purposes.
14 +
15 + ## How It Works
16 +
17 + ### Subscribing
18 +
19 + Fans subscribe to Fan+ from their account settings. Payment is handled through Stripe on a monthly billing cycle.
20 +
21 + ### Managing the Subscription
22 +
23 + From account settings, fans can:
24 +
25 + - View their current subscription status and billing period
26 + - See when the current period started and when it renews
27 + - Cancel at any time
28 +
29 + ### Cancellation
30 +
31 + When a fan cancels, they retain Fan+ benefits until the end of their current billing period. There are no partial refunds — the subscription simply does not renew.
32 +
33 + ## For Creators
34 +
35 + Fan+ subscriptions are managed entirely by the platform. As a creator, you do not need to configure anything. The `is_fan_plus` status is visible in the user's session data, so platform features can check it, but it does not affect your pricing, payouts, or audience management.
36 +
37 + ## See Also
38 +
39 + - [Selling & Audience](./03-selling.md) — Creator monetization overview
40 + - [Pricing Models](./pricing.md) — Creator subscription tiers (separate from Fan+)
@@ -0,0 +1,55 @@
1 + # Git Source Browser
2 +
3 + Makenot.work includes a built-in git source browser. Host your project's source code alongside your releases — fans and collaborators can browse code, read commit history, and view diffs without leaving the platform.
4 +
5 + ## Creating a Repository
6 +
7 + Repositories are created by pushing to the platform via SSH. There is no web UI for creating empty repos — push and the repository appears automatically.
8 +
9 + ### SSH Setup
10 +
11 + 1. Go to Settings > SSH Keys
12 + 2. Add your public key
13 + 3. Push to your repo:
14 +
15 + ```
16 + git remote add mnw git@ssh.makenot.work:username/repo-name.git
17 + git push mnw main
18 + ```
19 +
20 + The remote URL format is `git@ssh.makenot.work:username/repo-name.git`.
21 +
22 + ## Browsing Code
23 +
24 + Your repository is available at `/git/username/repo-name`. The source browser provides:
25 +
26 + - **Tree view** — Browse files and directories at any ref (branch, tag, or commit)
27 + - **File view** — Read individual files with syntax highlighting
28 + - **Commit log** — Paginated history (25 commits per page) for any branch
29 + - **Commit detail** — Full diff with inline additions and deletions (capped at 100 files / 10,000 lines)
30 + - **Blame view** — Per-line commit attribution
31 + - **Per-file history** — Commit log filtered to a single file's changes
32 +
33 + The repository overview shows the tree at HEAD and renders your README if one exists.
34 +
35 + ## Visibility
36 +
37 + Repositories can be public or private:
38 +
39 + - **Public** — Anyone can browse the source. Listed on your profile and the explore page.
40 + - **Private** — Only you (the owner) can view. Does not appear in listings.
41 +
42 + Set visibility from the repository settings page.
43 +
44 + ## Explore Page
45 +
46 + The public explore page at `/git` lists all public repositories across the platform, paginated 15 per page. It serves as a discovery feed for source-available projects.
47 +
48 + ## Linking Releases
49 +
50 + If your repository is associated with a project, releases published through the platform are linked in the repository view. Readers can navigate between the source and the downloadable release.
51 +
52 + ## See Also
53 +
54 + - [Content & Items](./02-content.md) — Publishing releases
55 + - [Projects](./projects.md) — Project setup
@@ -0,0 +1,62 @@
1 + # Mailing Lists
2 +
3 + Each project on Makenot.work has built-in mailing lists for communicating with your audience. No third-party email service needed.
4 +
5 + ## List Types
6 +
7 + Every project automatically gets two mailing lists:
8 +
9 + | List | Purpose |
10 + |------|---------|
11 + | **Content** | New releases and content updates |
12 + | **Devlog** | Development updates and behind-the-scenes |
13 +
14 + Lists are created automatically when you create a project. No setup required.
15 +
16 + ## Subscribers
17 +
18 + People subscribe to your mailing lists by following your project. When someone follows a project, they are subscribed to its content list by default.
19 +
20 + Subscriber requirements:
21 +
22 + - Verified email address
23 + - Account not suspended
24 + - Email not suppressed (bounced or complained)
25 +
26 + The subscriber cap is 10,000 per list.
27 +
28 + ## Sending a Broadcast
29 +
30 + To send a broadcast email to all your followers:
31 +
32 + 1. Go to your dashboard
33 + 2. Click "Broadcast"
34 + 3. Write a subject (up to 200 characters) and body (up to 5,000 characters)
35 + 4. Send
36 +
37 + Broadcasts are plain-text emails. Each email includes an unsubscribe link.
38 +
39 + Rate limit: one broadcast per 24 hours.
40 +
41 + ## Automatic Notifications
42 +
43 + Beyond manual broadcasts, the platform sends automatic emails through mailing lists when:
44 +
45 + - You publish a new item (content list)
46 + - You publish a blog post (content list)
47 +
48 + These are triggered by the scheduler and do not count against your broadcast rate limit.
49 +
50 + ## Unsubscribing
51 +
52 + Fans can unsubscribe from individual lists via the link in any email, or by unfollowing your project (which unsubscribes from all of that project's lists).
53 +
54 + ## Importing Subscribers
55 +
56 + You can import external email addresses (people without Makenot.work accounts) to your mailing lists. This is useful when migrating from another platform.
57 +
58 + ## See Also
59 +
60 + - [RSS Feeds](./rss.md) — Alternative subscription method
61 + - [Projects](./projects.md) — Project setup and management
62 + - [Selling & Audience](./03-selling.md) — Building your audience
@@ -0,0 +1,73 @@
1 + # Promo Codes
2 +
3 + Promo codes let you offer discounts, free trials, and free access to your content. Create them from your dashboard and share them with fans, press, collaborators, or anyone you choose.
4 +
5 + ## Code Types
6 +
7 + ### Discount Codes
8 +
9 + Reduce the price of an item, project, or subscription tier.
10 +
11 + - **Percentage discount** — 1% to 100% off
12 + - **Fixed discount** — A specific dollar amount off the price
13 +
14 + ### Free Trial Codes
15 +
16 + Give temporary access to subscription content.
17 +
18 + - Trial length: 1 to 365 days
19 + - Fan subscribes at the end of the trial or access expires
20 +
21 + ### Free Access Codes
22 +
23 + Grant permanent free access to a specific item. These use auto-generated word-based codes (easier to share verbally or in print).
24 +
25 + When a fan claims a free access code for an item that has license keys enabled, a license key is automatically generated for them.
26 +
27 + ## Creating a Promo Code
28 +
29 + 1. Go to your dashboard > Promo Codes
30 + 2. Click "Create Code"
31 + 3. Choose the type (Discount, Free Trial, or Free Access)
32 + 4. Set the parameters:
33 + - **Discount**: percentage or fixed amount
34 + - **Free Trial**: number of days
35 + - **Free Access**: select the item
36 + 5. Optionally set a scope, expiry date, and usage limit
37 + 6. Click "Create"
38 +
39 + Discount and free trial codes use custom alphanumeric codes you define. Free access codes are auto-generated with memorable word combinations.
40 +
41 + ## Scope
42 +
43 + Codes can be scoped to different levels:
44 +
45 + | Scope | Effect |
46 + |-------|--------|
47 + | Creator-wide | Applies to anything you sell |
48 + | Project | Applies to items within a specific project |
49 + | Item | Applies to a single item only |
50 + | Tier | Applies to a specific subscription tier |
51 +
52 + ## Expiry and Limits
53 +
54 + - **Expiry date** — Optional. Set a date (YYYY-MM-DD) after which the code stops working. Expires at end of day UTC.
55 + - **Usage limit** — Optional. Set the maximum number of times the code can be redeemed.
56 +
57 + Codes with no expiry and no limit work indefinitely.
58 +
59 + ## Managing Codes
60 +
61 + From your dashboard you can:
62 +
63 + - **List** all your promo codes with redemption counts
64 + - **Delete** a code to deactivate it immediately
65 +
66 + ## How Fans Use Codes
67 +
68 + Fans enter the promo code during checkout. For free access codes, fans can claim at a dedicated claim page without going through checkout.
69 +
70 + ## See Also
71 +
72 + - [Selling & Audience](./03-selling.md) — Pricing models and monetization overview
73 + - [Pricing Models](./pricing.md) — Detailed pricing guide
@@ -0,0 +1,85 @@
1 + # Account Security
2 +
3 + Makenot.work supports two-factor authentication (2FA) to protect your account. You can use passkeys, a TOTP authenticator app, or both.
4 +
5 + ## Passkeys
6 +
7 + Passkeys use the WebAuthn standard to let you log in with a fingerprint, face scan, hardware key, or device PIN — no password needed during login.
8 +
9 + ### Setting Up a Passkey
10 +
11 + 1. Go to Settings > Security
12 + 2. Click "Add Passkey"
13 + 3. Follow your browser/device prompt to create the credential
14 + 4. Give it a name (e.g., "MacBook Touch ID", "YubiKey")
15 +
16 + You can register up to 20 passkeys per account.
17 +
18 + ### Logging In with a Passkey
19 +
20 + On the login page, click "Use Passkey" instead of entering your password. Your browser will prompt you to verify with the registered device. Passkey login is inherently two-factor — it proves both identity and device possession — so you will not be prompted for a TOTP code.
21 +
22 + ### Managing Passkeys
23 +
24 + From Settings > Security you can:
25 +
26 + - **List** all registered passkeys with their creation dates
27 + - **Rename** a passkey for easier identification
28 + - **Delete** a passkey (requires password confirmation)
29 +
30 + If you lose access to all your passkeys, you can still log in with your password (plus TOTP if enabled).
31 +
32 + ## TOTP Authenticator App
33 +
34 + Time-based One-Time Password (TOTP) adds a six-digit rotating code from an authenticator app as a second factor after your password.
35 +
36 + ### Setting Up TOTP
37 +
38 + 1. Go to Settings > Security
39 + 2. Click "Enable Authenticator App"
40 + 3. Scan the QR code with your authenticator app (or enter the secret manually)
41 + 4. Enter the six-digit code from your app to confirm setup
42 + 5. Save your backup codes immediately
43 +
44 + Compatible apps include 1Password, Bitwarden, Authy, Google Authenticator, and any TOTP-compliant app (RFC 6238, SHA-1, 6 digits, 30-second interval).
45 +
46 + ### Logging In with TOTP
47 +
48 + 1. Enter your email and password as usual
49 + 2. When prompted, enter the current six-digit code from your authenticator app
50 +
51 + If you registered a passkey, passkey login bypasses the TOTP step entirely.
52 +
53 + ### Disabling TOTP
54 +
55 + Go to Settings > Security and click "Disable Authenticator App." You will need to confirm your password.
56 +
57 + ## Backup Codes
58 +
59 + When you enable TOTP, you receive 10 single-use backup codes. Each code is 8 characters and can be used exactly once in place of a TOTP code.
60 +
61 + **Store backup codes securely.** If you lose access to your authenticator app and have no backup codes, account recovery requires contacting support.
62 +
63 + ### Regenerating Backup Codes
64 +
65 + Go to Settings > Security > "Regenerate Backup Codes." This invalidates all previous codes and generates a fresh set of 10. You must confirm your password.
66 +
67 + ## Login Notifications
68 +
69 + If you have multiple active sessions and a new login occurs, you will receive an email notification. This is automatic — no configuration needed.
70 +
71 + ## Account Lockout
72 +
73 + After 5 consecutive failed password attempts, your account is locked for 15 minutes. Passkey authentication is not affected by password lockout.
74 +
75 + ## Recommendations
76 +
77 + - Enable at least one 2FA method (passkeys are the strongest option)
78 + - Register multiple passkeys on different devices so you are never locked out
79 + - If using TOTP, save your backup codes in a password manager or printed in a secure location
80 + - Use a unique, strong password even if you primarily log in with passkeys
81 +
82 + ## See Also
83 +
84 + - [Getting Started](./01-getting-started.md) — Account creation and initial setup
85 + - [Profile](./profile.md) — Editing your public profile
M src/auth.rs +2 -1
@@ -33,6 +33,7 @@ use crate::config::Config;
33 33 use crate::constants;
34 34 use crate::db::{self, UserId, UserSessionId, Username};
35 35 use crate::error::AppError;
36 + use crate::helpers::constant_time_compare;
36 37
37 38 /// Session key for storing user data
38 39 const USER_SESSION_KEY: &str = "user";
@@ -222,7 +223,7 @@ impl FromRequestParts<crate::AppState> for ServiceAuth {
222 223 .and_then(|v| v.strip_prefix("Bearer "))
223 224 .ok_or(AppError::Unauthorized)?;
224 225
225 - if header != expected {
226 + if !constant_time_compare(header, expected) {
226 227 return Err(AppError::Unauthorized);
227 228 }
228 229
M src/lib.rs +5 -1
M src/rss.rs +2
M src/storage.rs +46 -2