Skip to main content

max / makenotwork

Usability audit remediations and audit run 20 cleanup Usability improvements from full UX audit across complexity, learnability, discoverability, and feature completeness: Terminology and docs: - Rename "content insertions" to "dynamic clips" everywhere - Replace content-insertions.md with dynamic-clips.md (broader examples) - Update all cross-references in items, media-library, export, roadmap - Rename toast messages: "Insertion renamed" -> "Clip renamed", etc. Dashboard grouping and progressive disclosure: - Add .section-group-label CSS class for visual category dividers - Account tab: group into Security, Preferences, Data, Account Management - Payments tab: group into Payouts, Settings, History - Creator Plan tab: group into Usage, Outreach - Project Settings: replace "Danger Zone" with "Project Management" - Notification preferences: group by Security, Sales, Content, Platform - SyncKit/Cloud Sync: add as opt-in project feature (new CloudSync enum variant), gate tab visibility, add intro explaining the feature Navigation and discoverability: - Add keyboard shortcuts help button (?) in site header - Rename footer "What's New" to "Changelog" - Add mobile filter toggle on Discover page with active filter count - Link tier names to /docs/guide/tiers from Creator Plan tab - Rename "Plan" tab to "Creator Plan" Content management: - Add inline Publish button for draft items in content table - Show item titles in bulk delete confirmation dialog - Add success toast after bulk publish/unpublish/delete Onboarding and labeling: - Define Project/Item hierarchy in dashboard empty state - Change onboarding "Skip" to "Hide for now" with recovery toast - Rename "Export Portal" to "Export Your Data" - Rename "Import Data" to "Import from Another Platform" - Expand license keys help text with activation explanation Form and interaction improvements: - Restructure promo code form from cramped flex row to 2-column grid - Add copy-to-clipboard buttons for custom domain DNS records - Add DNS propagation note and setup guide link Pre-existing (audit run 20): - Extract media player to static JS/CSS, add video player template - Remove stale database_schema.md (schema.md is canonical) - Update docs, patterns, CONTRIBUTING, todo - Add earn-back credit program to roadmap and guarantees - Add chargeback protection fund doc - Bump docengine and synckit-client deps - Fix SyncKit crypto import Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-06 18:10 UTC
Commit: aa0751123d353902d2abf3bd98253da475e6f563
Parent: ad832b8
69 files changed, +2141 insertions, -1736 deletions
M CLAUDE.md +1 -9
@@ -6,15 +6,7 @@ Fair creator platform with 0% platform fee (only Stripe's ~3% processing fee). M
6 6
7 7 **No Emoji.** The diamond mark (Young Serif period glyph) is the only graphic element. No emoji anywhere.
8 8
9 - **Typography (Three-Tier System):**
10 - - **H1**: Young Serif (wordmark, page/section headings)
11 - - **H2/H3/meta**: IBM Plex Mono (subheadings, taglines, footer)
12 - - **Body**: Lato (paragraphs, lists, table content)
13 -
14 - **Colors:**
15 - - Background: warm beige `#ede8e1` — never pure white
16 - - Text: dark charcoal-brown `#3d3530` — never pure black
17 - - Accent: violet `#6c5ce7` — diamond mark only, used sparingly
9 + **Typography and colors:** See `_meta/docs/brand.md` for the three-tier type system (Young Serif / IBM Plex Mono / Lato), color palette (warm beige / charcoal-brown / violet), and the diamond mark rule.
18 10
19 11 **Platform Principles:**
20 12 - **0% platform fee** — Stripe's ~3% processing fee is the only cost
@@ -6,7 +6,6 @@ Forum-first community software with integrated E2E encrypted live chat.
6 6
7 7 - Rust (stable)
8 8 - PostgreSQL
9 - - Redis / Valkey
10 9
11 10 ## Build & Run
12 11
@@ -450,8 +450,7 @@ On Astra, use `--test-threads=8` (or the `RUST_TEST_THREADS=8` env var) to avoid
450 450 - **Rust 2024 edition** (Rust 1.85+). Uses `gen` keyword restrictions and other 2024 features.
451 451 - No `.unwrap()` in production code. Use `?`, `.ok_or()`, or `unwrap_or_default()`.
452 452 - Prefer `Option::and_then`/`map` over `if let Some`/`match` for simple transforms.
453 - - Keep route files under 500 lines. Split into directory modules when they grow beyond that.
454 - - Files with 500+ lines of branching logic should be split. Flat lists (SQL queries, type conversions, static data) are exempt.
453 + - File size guideline per root `CONTRIBUTING.md`: 500-line limit on branching logic, flat lists exempt. Route files follow the same rule — split into directory modules when they grow beyond 500 lines.
455 454
456 455 ## Dependencies
457 456
@@ -1,533 +0,0 @@
1 - # MNW Database Schema
2 -
3 - PostgreSQL schema reference. 50 migrations, 60+ tables. Migrations live in `migrations/` and auto-run on boot via sqlx.
4 -
5 - ## Domain Map
6 -
7 - | Domain | Tables | Purpose |
8 - |--------|--------|---------|
9 - | Auth & Users | 10 | Identity, sessions, passkeys, waitlist, creator tiers |
10 - | Content | 9 | Projects, items, versions, chapters, tags, blog posts |
11 - | Commerce | 9 | Transactions, subscriptions, license keys, promo codes |
12 - | Collections | 3 | User-curated collections and bundles |
13 - | Git & Issues | 8 | Repositories, SSH keys, issues, email threading |
14 - | SyncKit | 9 | Cloud sync, E2E encryption, OTA updates, build pipeline |
15 - | Email | 4 | Mailing lists, subscribers, file scanning, signups |
16 - | Moderation | 3 | Reports, platform labels, project label assignments |
17 - | Content Insertions | 2 | Reusable media clips with placement positions |
18 - | Social | 1 | Follows (users, projects, tags) |
19 - | Creator Growth | 1 | Invite codes |
20 - | Sessions | 1 | HTTP session store (tower-sessions) |
21 -
22 - ---
23 -
24 - ## Auth & Users
25 -
26 - ### users
27 - Primary user table. Handles identity, Stripe Connect, email verification, security, and creator access.
28 -
29 - | Column | Type | Notes |
30 - |--------|------|-------|
31 - | `id` | UUID PK | |
32 - | `username` | VARCHAR | Unique |
33 - | `email` | VARCHAR | Unique |
34 - | `password_hash` | VARCHAR | argon2 |
35 - | `display_name` | VARCHAR | |
36 - | `bio` | TEXT | |
37 - | `avatar_url` | VARCHAR | |
38 - | `stripe_account_id` | VARCHAR | Stripe Connect |
39 - | `stripe_onboarding_complete` | BOOL | |
40 - | `stripe_payouts_enabled` | BOOL | |
41 - | `stripe_charges_enabled` | BOOL | |
42 - | `email_verified` | BOOL | |
43 - | `email_verification_token` | VARCHAR | |
44 - | `totp_secret` | VARCHAR | TOTP 2FA |
45 - | `totp_enabled` | BOOL | |
46 - | `failed_login_attempts` | INT | Brute-force protection |
47 - | `locked_until` | TIMESTAMPTZ | |
48 - | `can_create_projects` | BOOL | Waitlist gate |
49 - | `creator_tier` | VARCHAR | Migration 036 |
50 - | `login_notification_enabled` | BOOL | |
51 - | `notify_release` | BOOL | Migration 015 |
52 - | `created_at`, `updated_at` | TIMESTAMPTZ | |
53 -
54 - ### user_passkeys
55 - WebAuthn/passkey credentials. Migration 006.
56 -
57 - - FK `user_id` -> users
58 - - `credential_json` (JSONB), `credential_id` (BYTEA, unique per user), `name`, `last_used_at`
59 -
60 - ### user_sessions
61 - Active login sessions.
62 -
63 - - FK `user_id` -> users
64 - - `user_agent`, `ip_address`, `created_at`, `last_active_at`
65 -
66 - ### login_tokens
67 - One-time tokens for passwordless login.
68 -
69 - - FK `user_id` -> users
70 - - `token_hash` (unique), `expires_at`, `used_at`
71 -
72 - ### backup_codes
73 - 2FA recovery codes.
74 -
75 - - FK `user_id` -> users
76 - - `code_hash`, `used_at`
77 -
78 - ### oauth_authorization_codes
79 - PKCE OAuth provider (used by Multithreaded).
80 -
81 - - FK `app_id` -> sync_apps, `user_id` -> users
82 - - `code` (unique), `code_challenge`, `code_challenge_method`, `redirect_uri`, `expires_at`, `used_at`
83 -
84 - ### email_suppressions
85 - Bounce/complaint tracking. Migration 015.
86 -
87 - - `email` (unique, case-insensitive), `reason` ('HardBounce', 'SpamComplaint')
88 -
89 - ### creator_waitlist
90 - Alpha access waitlist.
91 -
92 - - FK `user_id` (unique) -> users, `wave_id` -> creator_waves, `invited_by_user_id` -> users
93 - - `pitch`, `status` ('pending', 'approved', 'rejected'), `selection_method`, `admin_note`
94 -
95 - ### creator_waves
96 - Batch invitation waves.
97 -
98 - - `wave_number` (unique), `hand_picked_count`, `lottery_count`, `total_eligible`, `note`
99 -
100 - ### creator_subscriptions
101 - Creator tier billing. Migration 036.
102 -
103 - - FK `user_id` (unique) -> users
104 - - `stripe_subscription_id` (unique), `tier`, `status`, `current_period_start`, `current_period_end`
105 -
106 - ---
107 -
108 - ## Content
109 -
110 - ### projects
111 - Creator projects (albums, podcasts, books, software, etc.).
112 -
113 - | Column | Type | Notes |
114 - |--------|------|-------|
115 - | `id` | UUID PK | |
116 - | `user_id` | UUID FK | -> users |
117 - | `slug` | VARCHAR | Unique per user |
118 - | `title` | VARCHAR | |
119 - | `description` | TEXT | |
120 - | `project_type` | VARCHAR | music, podcast, blog, book, course, software, art, writing |
121 - | `category_id` | UUID FK | -> project_categories |
122 - | `cover_image_url` | VARCHAR | |
123 - | `is_public` | BOOL | |
124 - | `mt_community_id` | UUID | Multithreaded link (migration 037) |
125 - | `features` | TEXT[] | Platform capabilities (migration 046) |
126 - | `created_at`, `updated_at` | TIMESTAMPTZ | |
127 -
128 - Unique constraint: `(user_id, slug)`.
129 -
130 - ### items
131 - Individual content pieces within a project.
132 -
133 - | Column | Type | Notes |
134 - |--------|------|-------|
135 - | `id` | UUID PK | |
136 - | `project_id` | UUID FK | -> projects (CASCADE) |
137 - | `title` | VARCHAR | |
138 - | `description` | TEXT | |
139 - | `slug` | VARCHAR | Unique per project (migration 043) |
140 - | `price_cents` | INT | >= 0 |
141 - | `item_type` | VARCHAR | |
142 - | `is_public` | BOOL | |
143 - | `listed` | BOOL | Default true (migration 048) |
144 - | `body` | TEXT | Text content |
145 - | `word_count`, `reading_time_minutes` | INT | |
146 - | `audio_url`, `duration_seconds` | | Audio content |
147 - | `audio_s3_key`, `cover_s3_key` | VARCHAR | S3 storage keys |
148 - | `enable_license_keys` | BOOL | |
149 - | `default_max_activations` | INT | |
150 - | `sales_count`, `play_count`, `download_count` | INT | Denormalized counters |
151 - | `pwyw_enabled` | BOOL | Pay-what-you-want |
152 - | `pwyw_min_cents` | INT | |
153 - | `publish_at` | TIMESTAMPTZ | Scheduled publishing (migration 011) |
154 - | `scan_status` | VARCHAR | File scanning (migration 004) |
155 - | `mt_thread_id` | UUID | Multithreaded link (migration 037) |
156 - | `sort_order` | INT | >= 0 |
157 - | `created_at`, `updated_at` | TIMESTAMPTZ | |
158 -
159 - ### versions
160 - File versions for downloadable items.
161 -
162 - - FK `item_id` -> items (CASCADE)
163 - - `version_number`, `changelog`, `file_url`, `file_size_bytes`, `file_name`, `s3_key`
164 - - `download_count` (>= 0), `is_current` (unique constraint: one current per item)
165 - - `scan_status` (migration 004)
166 -
167 - ### chapters
168 - Audio chapter markers.
169 -
170 - - FK `item_id` -> items (CASCADE)
171 - - `title`, `start_seconds`, `sort_order`
172 -
173 - ### tags
174 - Hierarchical tag system.
175 -
176 - - Self-referential: `parent_id` -> tags
177 - - `name`, `slug` (unique), `sort_order`, `path` (denormalized hierarchy, migration 038)
178 -
179 - ### item_tags
180 - Many-to-many item-tag association.
181 -
182 - - PK `(item_id, tag_id)`
183 - - `is_primary` (one primary tag per item)
184 -
185 - ### blog_posts
186 - Project blog posts with markdown.
187 -
188 - - FK `project_id` -> projects (CASCADE), `author_id` -> users
189 - - `title`, `slug`, `body_markdown`, `body_html`
190 - - `published_at`, `publish_at` (scheduled, migration 011)
191 - - `mt_thread_id` (migration 037)
192 - - Unique: `(project_id, slug)`
193 -
194 - ### project_categories
195 - Browsable project categories.
196 -
197 - - `name`, `slug` (both unique)
198 -
199 - ### custom_links
200 - User profile links.
201 -
202 - - FK `user_id` -> users
203 - - `url`, `title`, `description`, `sort_order`
204 -
205 - ---
206 -
207 - ## Commerce
208 -
209 - ### transactions
210 - Purchase records.
211 -
212 - - FK `buyer_id` -> users, `seller_id` -> users (nullable), `item_id` -> items (nullable), `project_id` (migration 047, nullable)
213 - - `amount_cents` (>= 0), `platform_fee_cents` (>= 0), `currency`
214 - - `status`: 'pending', 'completed', 'failed', 'refunded'
215 - - `stripe_payment_intent_id`, `stripe_checkout_session_id`
216 - - `item_title`, `seller_username` -- denormalized (preserved after deletion)
217 - - `share_contact`
218 - - View: **purchases** (distinct on buyer_id, item_id, completed)
219 -
220 - ### subscription_tiers
221 - Subscription plans within projects (or items, migration 047).
222 -
223 - - FK `project_id` -> projects, `item_id` (nullable) -> items
224 - - `name`, `description`, `price_cents` (> 0), `sort_order`, `is_active`
225 - - `stripe_product_id`, `stripe_price_id`
226 -
227 - ### subscriptions
228 - Active subscriber records.
229 -
230 - - FK `subscriber_id` -> users, `tier_id` -> subscription_tiers, `project_id` -> projects, `item_id` (nullable)
231 - - `stripe_subscription_id`, `stripe_customer_id`, `status`
232 - - `current_period_start`, `current_period_end`, `canceled_at`
233 -
234 - ### subscription_events
235 - Stripe webhook event log.
236 -
237 - - `stripe_event_id` (unique), `event_type`, `payload` (JSONB)
238 -
239 - ### license_keys
240 - Software license keys.
241 -
242 - - FK `item_id` -> items, `owner_id` -> users, `transaction_id` -> transactions (nullable)
243 - - `key_code` (unique), `max_activations`, `activation_count`, `revoked_at`
244 -
245 - ### license_activations
246 - Machine activations for license keys.
247 -
248 - - FK `license_key_id` -> license_keys
249 - - `machine_id`, `label`, `is_active`
250 - - Unique: `(license_key_id, machine_id)`
251 -
252 - ### promo_codes
253 - Unified promotion system (replaces old discount_codes + download_codes). Migration 019.
254 -
255 - - FK `creator_id` -> users, `item_id` (nullable), `project_id` (nullable), `tier_id` (nullable)
256 - - `code` (unique per creator, case-insensitive)
257 - - `code_purpose`: 'discount', 'free_access', 'free_trial'
258 - - Discount fields: `discount_type` ('percentage', 'fixed'), `discount_value`, `min_price_cents`
259 - - Trial fields: `trial_days`
260 - - `max_uses`, `use_count`, `expires_at`
261 - - `is_platform_wide` (migration 031)
262 - - CHECK constraints enforce field consistency by purpose
263 -
264 - ### fan_plus_subscriptions
265 - Consumer-side platform subscription. Migration 031.
266 -
267 - - FK `user_id` (unique) -> users
268 - - `stripe_subscription_id` (unique), `status`, `current_period_start`, `current_period_end`
269 -
270 - ### contact_revocations
271 - Buyer-seller contact sharing opt-out. Migration 016.
272 -
273 - - PK `(buyer_id, seller_id)`
274 - - `revoked_at`
275 -
276 - ---
277 -
278 - ## Collections & Bundles
279 -
280 - ### collections
281 - User-curated collections (playlists, reading lists). Migration 032.
282 -
283 - - FK `user_id` -> users
284 - - `slug`, `title`, `description`, `is_public`
285 - - Unique: `(user_id, slug)`
286 -
287 - ### collection_items
288 - Items within a collection. Migration 032.
289 -
290 - - PK `(collection_id, item_id)`
291 - - `position`, `added_at`
292 -
293 - ### bundle_items
294 - Items grouped into a bundle (parent item). Migration 048.
295 -
296 - - PK `(bundle_id, item_id)` -- both FK to items
297 - - `sort_order`, `added_at`
298 - - CHECK: `bundle_id != item_id`
299 -
300 - ---
301 -
302 - ## Git & Issues
303 -
304 - ### git_repos
305 - Git repositories (bare repos on disk, browsed via `git2`). Migration 020.
306 -
307 - - FK `user_id` -> users, `project_id` -> projects (nullable)
308 - - `name`
309 - - Unique: `(user_id, name)`
310 -
311 - ### ssh_keys
312 - User SSH public keys for git push. Migration 024.
313 -
314 - - FK `user_id` -> users
315 - - `public_key`, `fingerprint`, `label`
316 - - Unique: `(user_id, fingerprint)`
317 -
318 - ### issues
319 - Email-first issue tracker. Migration 027.
320 -
321 - - FK `repo_id` -> git_repos, `author_user_id` -> users
322 - - `number` (unique per repo), `title`, `body_markdown`, `body_html`
323 - - `status`: 'open', 'closed'
324 -
325 - ### issue_comments
326 - Comments on issues. Migration 027.
327 -
328 - - FK `issue_id` -> issues, `author_user_id` -> users
329 - - `body_markdown`, `body_html`
330 -
331 - ### issue_labels, issue_label_assignments
332 - Label definitions and assignments. Migration 027.
333 -
334 - - Labels: `name`, `color` (hex), unique per repo
335 - - Assignments: PK `(issue_id, label_id)`
336 -
337 - ### issue_message_ids
338 - Email Message-ID bridging for issue threading. Migration 045.
339 -
340 - - FK `issue_id` -> issues
341 - - `message_id` (unique)
342 -
343 - ### patch_message_ids
344 - Email Message-ID bridging for patch discussions. Migration 044.
345 -
346 - - FK `project_id` -> projects
347 - - `message_id` (unique), `thread_id` (Multithreaded thread ID)
348 -
349 - ---
350 -
351 - ## SyncKit
352 -
353 - ### sync_apps
354 - Registered applications.
355 -
356 - - FK `creator_id` -> users
357 - - `name`, `api_key` (unique, 64-char), `slug` (unique, nullable, migration 033), `is_active`
358 -
359 - ### sync_devices
360 - Per-user device registrations.
361 -
362 - - FK `app_id` -> sync_apps, `user_id` -> users
363 - - `device_name`, `platform`, `last_seen_at`
364 - - Unique: `(app_id, user_id, device_name)`
365 -
366 - ### sync_keys
367 - E2E encryption keys (server never has plaintext).
368 -
369 - - PK `(app_id, user_id)`
370 - - `key_version`, `encrypted_key`
371 -
372 - ### sync_log
373 - Changelog entries for push/pull sync.
374 -
375 - - PK `seq` (BIGSERIAL)
376 - - FK `app_id`, `user_id`, `device_id`
377 - - `table_name`, `operation` ('INSERT', 'UPDATE', 'DELETE'), `row_id`, `client_timestamp`, `data` (JSONB)
378 -
379 - ### sync_blobs
380 - Content-addressed file storage for sync. Migration 012.
381 -
382 - - FK `app_id`, `user_id`
383 - - `hash` (content hash), `s3_key`, `size_bytes`
384 - - Unique: `(app_id, user_id, hash)`
385 -
386 - ### ota_releases
387 - OTA update releases. Migration 033.
388 -
389 - - FK `app_id` -> sync_apps
390 - - `version`, `notes`, `signature`, `pub_date`
391 - - Unique: `(app_id, version)`
392 -
393 - ### ota_artifacts
394 - Per-platform binaries for OTA releases. Migration 033.
395 -
396 - - FK `release_id` -> ota_releases
397 - - `target`, `arch`, `s3_key`, `file_size`
398 - - Unique: `(release_id, target, arch)`
399 -
400 - ### ota_build_configs
401 - Automated build configuration (one per app). Migration 034.
402 -
403 - - FK `app_id` -> sync_apps, `repo_id` -> git_repos
404 - - `build_command`, `artifact_path`, `signing_key_path`, `targets` (TEXT[]), `enabled`
405 -
406 - ### ota_builds
407 - Build execution records. Migration 034.
408 -
409 - - FK `config_id` -> ota_build_configs, `app_id`, `release_id` (nullable)
410 - - `version`, `tag`, `status`, `log`, `error_message`, `triggered_by` (default 'tag')
411 -
412 - ---
413 -
414 - ## Email & Scanning
415 -
416 - ### mailing_lists
417 - Per-project mailing lists. Migration 039.
418 -
419 - - FK `project_id` -> projects
420 - - `list_type`: 'content', 'devlog', 'patches'
421 - - Unique: `(project_id, list_type)`
422 -
423 - ### mailing_list_subscribers
424 - List memberships. Migration 039.
425 -
426 - - FK `list_id` -> mailing_lists, `user_id` -> users
427 - - Unique: `(list_id, user_id)`
428 -
429 - ### file_scan_results
430 - ClamAV/YARA scan results for uploaded files. Migration 004.
431 -
432 - - `s3_key`, `scan_status`, `scan_layers` (JSONB), `sha256`, `file_size_bytes`
433 -
434 - ### email_signups
435 - Landing page email collection. Migration 050.
436 -
437 - - `email` (unique), `source` (default 'landing')
438 -
439 - ---
440 -
441 - ## Moderation
442 -
443 - ### reports
444 - User-submitted reports. Migration 030.
445 -
446 - - FK `reporter_user_id`, `resolved_by` (nullable) -> users
447 - - `target_type`, `target_id` (polymorphic), `report_type`, `reason`, `status`, `admin_notes`
448 -
449 - ### labels
450 - Platform-wide creator commitment labels. Migration 029. Examples: drm-free, no-tracking, solo-dev, accessible.
451 -
452 - - `slug` (unique), `display_name`, `definition`, `examples`, `nonexamples`, `sort_order`
453 -
454 - ### project_labels
455 - Label assignments to projects. Migration 029.
456 -
457 - - PK `(project_id, label_id)`
458 - - `confirmed_at`
459 -
460 - ---
461 -
462 - ## Other
463 -
464 - ### content_insertions, content_insertion_placements
465 - Reusable media clips (e.g., intros, ads) with per-item placement. Migration 007.
466 -
467 - - Insertions: FK `user_id`, `title`, `media_type`, `storage_key`, `duration_ms`, `file_size`, `mime_type`
468 - - Placements: FK `item_id`, `insertion_id`, `position` ('pre_roll', 'mid_roll', 'post_roll'), `offset_ms`
469 -
470 - ### follows
471 - Polymorphic follow system.
472 -
473 - - FK `follower_id` -> users
474 - - `target_type` ('user', 'project', 'tag'), `target_id` (no FK, polymorphic)
475 - - Unique: `(follower_id, target_type, target_id)`
476 -
477 - ### invite_codes
478 - Alpha access invite codes. Migration 009.
479 -
480 - - FK `creator_id` -> users, `redeemed_by_id` (nullable) -> users
481 - - `code` (unique), `redeemed_at`
482 -
483 - ### tower_sessions.session
484 - HTTP session store (managed by tower-sessions-sqlx-store).
485 -
486 - - PK `id` (TEXT), `data` (BYTEA), `expiry_date`
487 -
488 - ---
489 -
490 - ## Design Patterns
491 -
492 - - **Polymorphic targeting:** `follows` and `reports` use `(target_type, target_id)` instead of per-type FK columns
493 - - **Denormalized counters:** `items.sales_count`/`play_count`/`download_count` for fast reads
494 - - **Denormalized display fields:** `transactions.item_title`/`seller_username` survive deletion
495 - - **E2E encryption:** `sync_keys.encrypted_key` is opaque to the server
496 - - **Unified promo codes:** Single table with CHECK constraints instead of separate discount/download tables
497 - - **Message-ID bridging:** `issue_message_ids` and `patch_message_ids` link email threads to platform objects
498 - - **Scheduled publishing:** `publish_at` on items and blog_posts for future release
499 - - **Content-addressed blobs:** `sync_blobs` deduplicates by `(app_id, user_id, hash)`
500 -
Lines truncated
@@ -4,21 +4,15 @@ Server-rendered HTML via Askama templates, HTMX for interactivity, hand-authored
4 4
5 5 ## Design System
6 6
7 - ### Typography (Three Tiers)
7 + Typography and brand colors are defined in `_meta/docs/brand.md` (three-tier type system, base palette, diamond mark rule). The server extends the base palette with CSS custom properties:
8 8
9 - | Tier | Font | Usage |
10 - |------|------|-------|
11 - | H1 | Young Serif | Wordmark, page/section headings |
12 - | H2/H3/meta | IBM Plex Mono | Subheadings, taglines, footer, code |
13 - | Body | Lato | Paragraphs, lists, table content, buttons |
14 -
15 - ### Colors
9 + ### Extended Colors (CSS Variables)
16 10
17 11 | Variable | Value | Usage |
18 12 |----------|-------|-------|
19 - | `--background` | `#ede8e1` (warm beige) | Page background. Never pure white. |
20 - | `--detail` | `#3d3530` (charcoal-brown) | Body text. Never pure black. |
21 - | `--highlight` | `#6c5ce7` (violet) | Diamond mark only. Used sparingly. |
13 + | `--background` | `#ede8e1` | Page background (brand beige) |
14 + | `--detail` | `#3d3530` | Body text (brand charcoal-brown) |
15 + | `--highlight` | `#6c5ce7` | Diamond mark only (brand violet) |
22 16 | `--light-background` | `#f4f0eb` | Cards, elevated surfaces |
23 17 | `--surface-muted` | `#ddd7c5` | Secondary buttons, status boxes |
24 18 | `--border` | `#d0cbb8` | Borders, dividers |
@@ -30,7 +24,6 @@ Server-rendered HTML via Askama templates, HTMX for interactivity, hand-authored
30 24
31 25 ### Rules
32 26
33 - - **No emoji.** The diamond mark (`.dot` class, Young Serif period glyph) is the only graphic element.
34 27 - **No pure white/black.** Use `--background` and `--detail` instead.
35 28 - **Accent color is for the dot only.** Do not use `--highlight` for buttons, links, or borders.
36 29
@@ -0,0 +1,193 @@
1 + # Chargeback Protection Fund
2 +
3 + *Internal planning document. Post-beta feature — not yet scheduled.*
4 +
5 + A mutual protection pool where participating creators contribute a small percentage of sales revenue into a shared fund that reimburses chargeback losses. MNW takes a small maintenance cut. No creator bears the full cost of a chargeback alone.
6 +
7 + ---
8 +
9 + ## Why This Matters
10 +
11 + A single chargeback on a $15 sale costs the creator ~$31:
12 +
13 + | Component | Amount |
14 + |-----------|--------|
15 + | Lost transaction amount | $15.00 |
16 + | Stripe dispute fee (non-refundable) | $15.00 |
17 + | Stripe processing fee (non-refundable) | ~$0.74 |
18 + | **Total loss** | **~$30.74** |
19 +
20 + Fighting and losing adds another $15. That is 2x-3x the original sale.
21 +
22 + Stripe offers Chargeback Protection at 0.4% of all transactions, but it covers fraud chargebacks only. "Product not as described" and "unrecognized" chargebacks — the types most likely on a content platform — are excluded. A platform-managed pool covers all types.
23 +
24 + No platform currently offers a mutual pool among creators. Every existing model either absorbs costs as a service (Shopify, PayPal), charges a flat fee for fraud-only coverage (Stripe), or passes costs through to sellers (Etsy, Gumroad, Patreon). A cooperative model where creators pool risk is genuinely differentiated and fits the MNW narrative.
25 +
26 + ---
27 +
28 + ## How It Works
29 +
30 + 1. Participating creators opt in
31 + 2. A percentage of each fan transaction is withheld into the pool
32 + 3. When a chargeback hits, the pool reimburses the creator (transaction amount + dispute fee + processing fee)
33 + 4. MNW takes a small maintenance percentage of pool contributions
34 + 5. Per-creator caps prevent a single bad actor from draining the fund
35 + 6. Experience rating adjusts contributions over time based on chargeback history
36 +
37 + ---
38 +
39 + ## Economics
40 +
41 + ### Assumptions
42 +
43 + - Average fan transaction: $15
44 + - Chargeback rate for curated digital platform: 0.15% (well below industry average of ~0.5% for digital goods)
45 + - Cost per chargeback: $30.74
46 +
47 + Industry context: Visa's VAMP program penalizes at 1.5%. Mastercard flags at 1.5% + 100 chargebacks/month. Safe operating range is below 0.5%. A curated platform with digital delivery and no shipping disputes should be well below that.
48 +
49 + ### Break-Even Contribution Rate
50 +
51 + ```
52 + Break-even = (chargeback_rate x cost_per_chargeback) / avg_transaction
53 + = (0.0015 x $30.74) / $15
54 + = 0.307%
55 + ```
56 +
57 + With safety margins:
58 +
59 + | Safety Margin | Contribution Rate |
60 + |---------------|-------------------|
61 + | 2x | 0.6% |
62 + | 2.5x (recommended) | 0.75% |
63 + | 3x | 0.9% |
64 +
65 + ### What Creators Pay at 0.75%
66 +
67 + | Monthly Sales | Pool Contribution | Annual Cost |
68 + |---------------|-------------------|-------------|
69 + | $100 | $0.75 | $9 |
70 + | $500 | $3.75 | $45 |
71 + | $2,000 | $15.00 | $180 |
72 + | $10,000 | $75.00 | $900 |
73 +
74 + One prevented chargeback per year pays for itself for any creator doing $500+/month in sales.
75 +
76 + ### Pool Revenue by Scale
77 +
78 + Assumes average creator does $500/month in fan sales.
79 +
80 + | Creators | Monthly GMV | Chargebacks/mo (expected) | Monthly Loss | Pool Income (0.75%) | Surplus | MNW 5% Cut |
81 + |----------|-------------|---------------------------|--------------|---------------------|---------|------------|
82 + | 30 | $15,000 | 1.5 | $46 | $112 | +$66 | $5.63 |
83 + | 100 | $50,000 | 5 | $154 | $375 | +$221 | $18.75 |
84 + | 300 | $150,000 | 15 | $461 | $1,125 | +$664 | $56.25 |
85 + | 1,000 | $500,000 | 50 | $1,537 | $3,750 | +$2,213 | $187.50 |
86 +
87 + MNW's cut is intentionally negligible. This is a trust and differentiation play, not a revenue stream.
88 +
89 + ### Variance Risk
90 +
91 + At small scale, chargebacks follow a Poisson distribution. With 30 creators and 1,000 transactions/month (expected: 1.5 chargebacks/month):
92 +
93 + | Chargebacks in a month | Probability | Pool solvent from contributions alone? |
94 + |------------------------|-------------|----------------------------------------|
95 + | 0 | 22.3% | Yes (surplus) |
96 + | 1 | 33.5% | Yes |
97 + | 2 | 25.1% | Yes |
98 + | 3 | 12.6% | Yes (barely) |
99 + | 4 | 4.7% | Draws on reserves |
100 + | 5+ | 1.8% | Needs reserves |
101 +
102 + At 0.75% income ($112/mo) and ~$31/chargeback, current contributions cover 3 chargebacks/month. A 4th draws from reserves. This is manageable with a buildup period.
103 +
104 + ---
105 +
106 + ## Operating Rules
107 +
108 + ### Launch Prerequisites
109 +
110 + - Minimum 50 participating creators before activating payouts
111 + - 3-month reserve buildup period (collect contributions, no payouts)
112 + - Reserve target: 6 months of expected claims before surplus distribution
113 + - IBNR buffer: hold 120 days of reserves at all times (chargebacks can arrive 60-120 days after a transaction)
114 +
115 + ### Per-Creator Caps
116 +
117 + | Creator Monthly Sales | Max Covered Chargebacks/Month |
118 + |-----------------------|-------------------------------|
119 + | < $500 | 2 |
120 + | $500-$2,000 | 3 |
121 + | $2,000-$10,000 | 5 |
122 + | $10,000+ | 8 |
123 +
124 + A creator exceeding their cap is likely being targeted. At that point the right response is investigation, not more payouts.
125 +
126 + ### Experience Rating (After 6 Months of Data)
127 +
128 + | Chargeback History | Rate Adjustment |
129 + |---------------------------|-----------------|
130 + | Zero chargebacks | 0.5x (pay 0.375%) |
131 + | At or below pool average | 1.0x (pay 0.75%) |
132 + | Above pool average | 1.5x (pay 1.125%) |
133 + | 2x+ pool average | Removed from pool |
134 +
135 + ### MNW Maintenance Fee
136 +
137 + 5% of pool contributions (not of GMV). Covers:
138 +
139 + - Webhook processing and dispute tracking infrastructure
140 + - Pool accounting and reporting
141 + - Dashboard development and maintenance
142 +
143 + ---
144 +
145 + ## Legal Structure
146 +
147 + **Structure as a platform service, not insurance.** A mutual insurance pool would require licensing in Colorado (captive insurance minimum capital: $100K + actuarial study + ongoing compliance). Instead:
148 +
149 + - Call it "Creator Protection Reserve" — a platform service per the ToS
150 + - Contributions are withheld from payouts, not billed separately
151 + - Payouts from the reserve are at the platform's discretion (not a guaranteed indemnity)
152 + - The fund is MNW's money (not a separate legal entity)
153 +
154 + This is how Stripe, Shopify, Etsy, and PayPal structure their seller protection programs. No special licensing required.
155 +
156 + Key legal distinction: MNW is managing its own funds and choosing to absorb certain costs as a business decision, not promising to indemnify third parties against risk.
157 +
158 + ---
159 +
160 + ## Implementation Sketch
161 +
162 + ### New Infrastructure
163 +
164 + 1. **Webhook handler** for `charge.dispute.created` and `charge.dispute.closed` on connected accounts (none exists today)
165 + 2. **Pool ledger table** — contributions, claims, running balance per creator
166 + 3. **Payout mechanism** — credit against future subscription fees or direct transfer
167 + 4. **Opt-in flag** on creator accounts
168 + 5. **Dashboard** — pool health, personal contribution/claims history, experience rating
169 +
170 + ### Data Model (Rough)
171 +
172 + - `chargeback_pool_members` — creator_id, opted_in_at, experience_multiplier
173 + - `chargeback_pool_contributions` — creator_id, transaction_id, amount_cents, created_at
174 + - `chargeback_pool_claims` — creator_id, dispute_id, amount_cents, status, created_at
175 + - `chargeback_pool_balance` — running total, reserve amount
176 +
177 + ### Webhook Flow
178 +
179 + 1. Receive `charge.dispute.created` from connected account
180 + 2. Match to creator, check pool membership
181 + 3. If member and under cap: create claim, mark pending
182 + 4. On `charge.dispute.closed` (lost): pay out from pool
183 + 5. On `charge.dispute.closed` (won): cancel claim, no payout needed
184 +
185 + ---
186 +
187 + ## Open Questions
188 +
189 + - Should the contribution be withheld from payouts or billed as a separate line item?
190 + - Should the pool cover only fan-to-creator transactions, or also Fan+ subscription chargebacks?
191 + - What happens to a creator's contributions if they leave the pool? Refundable? Forfeited?
192 + - Should there be a minimum sales volume to join (to avoid adverse selection from very low-volume creators)?
193 + - How transparent should pool financials be? Full transparency to all members, or just individual dashboards?
@@ -79,48 +79,7 @@ Each implementation calls `render_template(self)`, which renders to HTML and ret
79 79
80 80 ## HTMX Dual-Response Pattern
81 81
82 - Routes detect HTMX requests and return different formats. Page routes return HTML fragments; API clients get JSON.
83 -
84 - ```rust
85 - if is_htmx_request(&headers) {
86 - // Return HX-Redirect header
87 - let mut response = Response::new(Body::empty());
88 - response.headers_mut().insert(
89 - "HX-Redirect",
90 - format!("/dashboard/item/{}", item.id).parse().expect("valid path"),
91 - );
92 - return Ok(response);
93 - }
94 -
95 - Ok(Json(ItemResponse { /* ... */ }).into_response())
96 - ```
97 -
98 - ### Response patterns by action
99 -
100 - | Action | HTMX response | Non-HTMX response |
101 - |--------|---------------|-------------------|
102 - | Create + redirect | `HX-Redirect` header | `Json(ItemResponse)` |
103 - | Delete with feedback | `htmx_toast_response("Deleted", "success")` | `StatusCode::NO_CONTENT` |
104 - | Save with status | `Html(SaveStatusTemplate { success, message })` | `Json(UpdateTextResponse)` |
105 - | Inline edit | HTML fragment (re-rendered partial) | `Json(...)` |
106 -
107 - ### Helper functions
108 -
109 - ```rust
110 - // src/helpers.rs
111 - pub fn is_htmx_request(headers: &HeaderMap) -> bool {
112 - headers.get("HX-Request").is_some()
113 - }
114 -
115 - pub fn htmx_toast_response(message: &str, toast_type: &str) -> impl IntoResponse {
116 - // Returns HX-Trigger header with showToast JSON event
117 - // Frontend JS listens for this and renders a toast notification
118 - }
119 -
120 - pub fn hx_toast(message: &str, toast_type: &str) -> HeaderValue {
121 - // Serializes: { "showToast": { "message": "...", "type": "..." } }
122 - }
123 - ```
82 + See `CONTRIBUTING.md` § HTMX Responses for the full pattern (dual-format handlers, response table, helper functions).
124 83
125 84 ## `ListResponse<T>` Envelope
126 85
@@ -143,55 +102,7 @@ Ok(Json(ListResponse { data: chapters.into_iter().map(ChapterResponse::from).col
143 102
144 103 ## Error Handling
145 104
146 - **Location:** `src/error.rs`
147 -
148 - ### AppError enum
149 -
150 - ```rust
151 - pub enum AppError {
152 - NotFound,
153 - Unauthorized,
154 - Forbidden,
155 - BadRequest(String),
156 - Validation(String),
157 - Database(sqlx::Error),
158 - Internal(anyhow::Error),
159 - Storage(String),
160 - InvalidFileType(String),
161 - FileTooLarge(String),
162 - MalwareDetected(String),
163 - ServiceUnavailable(String),
164 - }
165 - ```
166 -
167 - ### HTTP status mapping
168 -
169 - | Variant | Status code |
170 - |---------|------------|
171 - | `NotFound` | 404 |
172 - | `Unauthorized` | 401 |
173 - | `Forbidden` | 403 |
174 - | `BadRequest` | 400 |
175 - | `Validation` | 422 |
176 - | `Database`, `Internal`, `Storage` | 500 |
177 - | `InvalidFileType` | 400 |
178 - | `FileTooLarge` | 413 |
179 - | `MalwareDetected` | 422 |
180 - | `ServiceUnavailable` | 503 |
181 -
182 - ### User-safe messages
183 -
184 - Internal errors (`Database`, `Internal`, `Storage`) return generic "Something went wrong" to the client. `Validation` and `BadRequest` messages pass through since they describe user input problems.
185 -
186 - ### JSON error middleware
187 -
188 - API routes use a middleware layer (`json_error_layer` in `routes/api/mod.rs`) that converts `AppError` HTML responses to JSON `{"error": "..."}` responses. Page routes render an error template.
189 -
190 - ### Result alias
191 -
192 - ```rust
193 - pub type Result<T> = std::result::Result<T, AppError>;
194 - ```
105 + See `CONTRIBUTING.md` § Error Handling for the `AppError` enum, HTTP status mapping, user-safe message rules, and JSON error middleware. Source: `src/error.rs`.
195 106
196 107 ## Rate Limiting
197 108
@@ -3,7 +3,7 @@
3 3 ## Status
4 4 Done: All pre-beta phases, UX audit remediation, creator trust audit remediation. Active: Creator setup (Stripe), manual testing. Next: Soft launch.
5 5
6 - v0.4.10 deployed 2026-05-04. Audit grade A (Run 20, 2026-05-04). ~83K LOC, 1,214+ test annotations, 0 cargo warnings, 2 cold spots. CI on astra operational. Mutation kill rate 99.4%. Property-based testing active (proptest). `cargo test --features fast-tests` for fast runs.
6 + v0.4.10 deployed 2026-05-04. Audit grade A (Run 20, 2026-05-04). ~83K LOC, 1,214+ test annotations, 0 cargo warnings, 2 cold spots. CI on astra operational. Mutation kill rate 99.4%. Property-based testing active (proptest). `cargo test --features fast-tests` for fast runs. Doc fuzz (2026-05-06): deleted stale database_schema.md, fixed MT README, updated SyncKit version in docs.
7 7
8 8 Human tasks (manual testing, outreach, legal, infrastructure) moved to `human_todo.md`.
9 9 Completed items moved to `todo_done.md`.
@@ -20,6 +20,8 @@ Split checkout.rs (792 -> 434 + 308 helpers). yara-x bumped 1.14->1.15 (intaglio
20 20 ### Deferred Code Quality
21 21 - [ ] Remove `async-trait` in favor of Rust 2024 native async traits (chronic)
22 22 - [ ] Add README.md to server/
23 + - [x] Delete stale `database_schema.md` (50 migrations) — superseded by `schema.md` (57 migrations)
24 + - [x] Fix MT README prerequisites: removed Redis/Valkey (sessions are PostgreSQL-backed)
23 25 - [ ] Split oversized route files: exports.rs (737), license_keys.rs (741), health.rs (844), tabs/user.rs (707)
24 26 - [ ] Monitor scheduler.rs (1249), git/mod.rs (624) for growth
25 27 - [x] [rust-fuzz] Replace `.unwrap()` with `.expect("tier passed is_none guard")` in db/creator_tiers.rs:607
@@ -27,7 +29,7 @@ Split checkout.rs (792 -> 434 + 308 helpers). yara-x bumped 1.14->1.15 (intaglio
27 29
28 30 ### Dashboard Usability — Remaining (2026-05-05)
29 31
30 - Dashboard restructure complete (Phases 1-6 in todo_done.md). Tab layout: Projects, Payments, Analytics, Profile, Account, Plan + overflow. Fan/creator progressive disclosure implemented. Labels removed. Jargon renamed.
32 + Dashboard restructure complete (Phases 1-6 in todo_done.md). Tab layout: Projects, Payments, Analytics, Profile, Account, Plan + overflow. Fan/creator progressive disclosure implemented. Labels removed. Jargon renamed. Discoverability items complete.
31 33
32 34 #### Performance
33 35 - [x] Add performance philosophy doc (`docs/performance_philosophy.md`) — Tufte/McMaster-Carr principles applied to MNW
@@ -45,18 +47,23 @@ Dashboard restructure complete (Phases 1-6 in todo_done.md). Tab layout: Project
45 47
46 48 Unify audio and video playback into one shared component. Video gets custom controls, insertions, chapters, speed, volume, progress persistence — matching the audio experience. Plan at `~/.claude/plans/eager-ancient-salmon.md`.
47 49
48 - - [ ] **Phase 1:** Extract media-player.js + media-player.css from audio_player.html (no behavior change, just extraction)
49 - - [ ] **Phase 2:** Make state machine media-type aware (audio: dual-element gapless; video: single-element with segment advancement)
50 - - [ ] **Phase 3:** Create unified media_player.html template + /watch/{slug} route for video items
51 - - [ ] **Phase 4:** Update item.html video section — poster + "Watch" button linking to /watch/ (matches /listen/ pattern)
52 - - [ ] **Phase 5:** Add keyboard shortcuts (Space, arrows, M, F, S) to unified player
50 + - [x] **Phase 1:** Extract media-player.js + media-player.css from audio_player.html — CSS renamed to media-* classes, JS reads config from `<script type="application/json">` bridge, keyboard shortcuts (Space/arrows/M/F/S) included. Template: 1035 → 206 lines
51 + - [x] **Phase 2:** Make state machine media-type aware — video uses single element (no dual gapless), all mediaB references guarded with null checks
52 + - [x] **Phase 3:** VideoPlayerTemplate + video_player.html template + video item handler in item.rs. Video items get dedicated player page (like audio). build_segments_json handles Video content type
53 + - [x] **Phase 4:** Video items now route to dedicated player page (early return in item handler, matching audio pattern) — item.html video section is dead code
54 + - [x] **Phase 5:** Keyboard shortcuts included in Phase 1 (Space, arrows, M, F, S)
55 + - [x] **Code fuzz fixes:** null standbyEl crashes in chapter nav + position restore, play-before-seek race in single-element advance, integer cast overflow guards in build_segments_json
53 56
54 - #### Discoverability
55 - - [ ] Add Media Library access from content editors — "Insert Image" button in blog/item editors
56 - - [ ] Add content search/filter to project Content tab — search by title, filter by status/type
57 - - [ ] Add "Embed & Share" quick action on Item Overview
58 - - [ ] Add bulk operations hint on Content tab
59 - - [ ] Add contextual next-step suggestions after key actions
57 + #### Remaining (from code fuzz 2026-05-05)
58 + - [ ] **Video URL fallback on S3 presign failure** — audio handler falls back to `audio_url` (CDN URL stored in DB), but video handler falls back to `None` because `ContentData::Video` has no `video_url` field. Add `video_url` column to items table (migration), populate from CDN base + S3 key on upload, fall back to it in video handler when presign fails. Matches audio pattern.
59 + - [ ] **Arrow key seek should use virtual timeline** — arrow keys currently set `el.currentTime` directly, bypassing segment boundaries. In segment mode this can seek into content from adjacent segments or past boundaries. Fix: route arrow seek through the progress bar seek logic (find target segment from virtual time, load correct segment, seek to offset). Affects both audio and video.
60 +
61 + #### Discoverability — DONE
62 + - [x] Add Media Library access from content editors — "Insert Image" button in blog/item editors, section editors. Shared `media-picker.js` modal fetches `/api/media`, inserts markdown ref at cursor
63 + - [x] Add content search/filter to project Content tab — client-side search by title, filter by status + type dropdowns
64 + - [x] Add "Embed & Share" quick action on Item Overview — button navigates to Embed tab
65 + - [x] Add bulk operations hint on Content tab — form-hint explaining checkbox selection
66 + - [x] Add contextual next-step suggestions after key actions — empty state flow hint, draft/published guidance on item overview
60 67
61 68 #### Feature Completeness
62 69 - [ ] Add download count analytics per item
@@ -179,6 +186,12 @@ Creators can opt into email alerts when platform status changes. Migration 091 a
179 186 - [ ] Full spec in git history. MVP: 22A + 22B + 22E + 22C + 22H
180 187 - [ ] Trigger: first Everything tier creator subscribes
181 188
189 + ### Chargeback Protection Fund
190 + - [ ] Mutual pool: creators contribute ~0.75% of sales, pool reimburses chargeback losses (all types, not just fraud)
191 + - [ ] Requires: dispute webhooks on connected accounts, pool ledger, per-creator caps, experience rating
192 + - [ ] Full plan: `docs/internal/business/chargeback_protection_fund.md`
193 + - [ ] Prerequisite: 50+ participating creators, 3-month reserve buildup before payouts activate
194 +
182 195 ### Phase 24: Payment Independence
183 196 - [ ] Stablecoin checkout, reduce creator-side fees, Stripe dependency mitigation, international expansion
184 197
@@ -4,6 +4,30 @@ Items moved from todo.md. See git history for implementation details.
4 4
5 5 ---
6 6
7 + ## Unified Media Player — Phase 1 (2026-05-05)
8 +
9 + - [x] Extract `static/media-player.js` (~450 lines) — full state machine: simple mode, segment mode (dual-element gapless), insertions, chapters, seek, speed, volume, progress persistence. Keyboard shortcuts: Space (play/pause), arrows (seek/volume), M (mute), F (fullscreen/video), S (skip insertion)
10 + - [x] Extract `static/media-player.css` (~370 lines) — all player styles with `media-*` class names (renamed from `audio-*`)
11 + - [x] Slim `audio_player.html` from 1,035 to 206 lines — inline CSS/JS removed, uses `<link>` + `<script src>` + JSON config bridge (`<script type="application/json">`)
12 + - [x] Data bridge: template passes segments, mediaType, itemId via JSON script tag; JS reads on init
13 +
14 + Phase 2-4 (2026-05-05):
15 + - [x] **Phase 2:** media-player.js handles video — single `<video>` element (no dual-element gapless), all `mediaB` references null-guarded
16 + - [x] **Phase 3:** `VideoPlayerTemplate` + `video_player.html` — custom controls, insertions, chapters, speed, volume, progress persistence for video. Video item handler in `item.rs` with presigned S3 URL, segment building. `build_segments_json` updated for `ContentData::Video`
17 + - [x] **Phase 4:** Video items now route to dedicated player page via early return (matching audio pattern). `item.html` inline video player is dead code
18 +
19 + ---
20 +
21 + ## Discoverability Improvements (2026-05-05)
22 +
23 + - [x] Add Media Library access from content editors — shared `media-picker.js` modal with search/folder filter, inserts markdown ref at cursor. Added to blog editor, item text editor, section editors (new + edit)
24 + - [x] Add content search/filter to project Content tab — client-side search by title + status/type dropdown filters above items table
25 + - [x] Add "Embed & Share" quick action on Item Overview — button navigates to existing Embed tab
26 + - [x] Add bulk operations hint on Content tab — form-hint text explaining checkbox bulk actions
27 + - [x] Add contextual next-step suggestions — empty state create/pricing/publish flow hint, draft vs published guidance on item overview
28 +
29 + ---
30 +
7 31 ## Dashboard Restructure + Usability (2026-05-05)
8 32
9 33 Dashboard:
@@ -53,14 +53,15 @@ If you leave, your fan contact list is yours (export it anytime). See Buyer Acce
53 53
54 54 ## Buyer Access
55 55
56 - **Guarantee:** If a creator deletes their account, fans who purchased content retain download access for 90 days.
56 + **Guarantee:** If a creator deletes their account, fans retain access to content for 90 days.
57 57
58 - - Purchased items remain accessible for 90 days after the creator leaves.
58 + - **One-time purchases:** Items remain downloadable for 90 days after the creator leaves.
59 + - **Subscription content:** Fans with active subscriptions retain access during the same 90-day grace period. Subscriptions are not billed during this period.
59 60 - After 90 days, content is removed from our servers.
60 61 - Buyers are notified by email when the grace period begins so they can download their files.
61 62 - Transaction records are preserved indefinitely (receipts remain valid).
62 63
63 - This gives buyers a reasonable window to retrieve content they paid for, while still allowing creators to fully remove their data from the platform.
64 + This gives fans a reasonable window to retrieve content they paid for, while still allowing creators to fully remove their data from the platform.
64 65
65 66 ---
66 67
@@ -71,7 +71,7 @@ Your tier fee funds the platform. We have no reason to take a cut of your revenu
71 71 | **Basic** | $10 | Text, blogs, newsletters | 10MB | 50GB |
72 72 | **Small Files** | $20 | Audio, software, plugins, sample packs | 500MB | 250GB |
73 73 | **Big Files** | $30 | Video, games, large software | 20GB | 500GB |
74 - | **Everything** | $60 | All features, current and future (live streaming coming soon) | 20GB | 500GB |
74 + | **Everything** | $60 | All features, current and future | 20GB | 500GB |
75 75
76 76 All files (content, covers, downloads, supplementary materials) count toward total storage. Big Files and Everything creators can request a per-file size increase beyond 20GB from their dashboard.
77 77
@@ -108,9 +108,7 @@ The prices reflect what it actually costs to store and deliver each content type
108 108
109 109 ### Earn-Back Credit Program
110 110
111 - *Launching no later than January 1, 2027.*
112 -
113 - If you earn less on the platform than you paid in tier fees during a 12-month period, the difference would be credited as free months for the following year (capped at 12 months). Credits would be calculated annually on your account anniversary.
111 + An earn-back credit program is on the [roadmap](./roadmap.md#earn-back-credit-program). If your revenue doesn't cover your tier fees over 12 months, the difference is credited as free months the following year.
114 112
115 113 ### Add-Ons
116 114
@@ -152,11 +150,11 @@ Content that has been on the platform for 12 months or more would stay hosted ev
152 150
153 151 ---
154 152
155 - ## No Algorithm
153 + ## Discovery: Intentional
156 154
157 - There is no recommendation engine. No trending page. No feed designed to maximize engagement. Fans find your work through search, tags, and links — the same way people find anything worth finding on the internet.
155 + We built real discovery tools — a [Discover page](/discover) with search, tag browsing, content type filters, and sorting — but none of them track behavior or optimize for engagement. Fans find your work through choices they made: a search they typed, a tag they browsed, a creator they followed. We call this [Discovery Through Exploration](../guide/discovery.md#discovery-through-exploration). The answer to "why am I seeing this?" is always one sentence.
158 156
159 - This means you need to bring an audience or build one elsewhere. We host and sell your work. We don't pretend to be a marketing department.
157 + There is no recommendation engine, no behavioral profiling, and no paid placement. Visibility comes from good metadata, accurate tags, and consistent publishing — all things within your control. We expect most early creators will bring an existing audience, but search and tag browsing mean new fans can find you too. See [Discovery](../guide/discovery.md) for the full picture.
160 158
161 159 ---
162 160
@@ -19,7 +19,7 @@ Everything listed here is live and working.
19 19 - **Scheduled publishing**: Set a future publish date, items auto-publish on schedule
20 20 - **Item duplication**: Clone any item's metadata to a new draft
21 21 - **Bulk operations**: Publish, unpublish, or delete multiple items at once
22 - - **Content insertions**: Reusable audio clip library with pre-roll, mid-roll, and post-roll placement per item
22 + - **Dynamic clips**: Reusable audio clip library with pre-roll, mid-roll, and post-roll placement per item
23 23 - **Video**: Upload video files, stream in-browser with native player, versioned downloads
24 24 - **Embeddable players and widgets**: Buy button, product card, audio player, tip button, and project card embeds for external sites. Copy-paste HTML from dashboard
25 25
@@ -127,6 +127,10 @@ The platform launches in three stages:
127 127 2. **Beta**: Open to everyone. The goal is a stable, polished platform with minimal to no bugs. All core features working reliably.
128 128 3. **Full launch**: Bugs and UX are under control. The platform is ready for creators who depend on it for income.
129 129
130 + ### Earn-back credit program
131 +
132 + If you earn less on the platform than you paid in tier fees during a 12-month period, the difference is credited as free months for the following year (capped at 12 months). Credits are calculated annually on your account anniversary. The 12-month clock starts when the alpha period ends. Launching no later than January 1, 2027.
133 +
130 134 ### Near-term
131 135
132 136 - **Admin CLI expansion**: Broadcast sending to all users for platform announcements
@@ -75,7 +75,7 @@ We're actively building better discovery features — ways to help fans find cre
75 75
76 76 ### Your Existing Audience Still Matters
77 77
78 - Most creators here started with an existing audience elsewhere. Search and tags mean new fans can find you too, but don't rely on discovery as your primary growth channel. Bring your people with you.
78 + We expect most early creators will bring an existing audience. Search and tags mean new fans can find you too, but don't rely on discovery as your primary growth channel. Bring your people with you.
79 79
80 80 ### Follows
81 81
@@ -1,61 +0,0 @@
1 - # Content Insertions
2 -
3 - Content insertions let you place reusable audio clips at specific points within your items — intros, outros, mid-roll announcements, sponsor reads, or any recurring audio you want to attach without editing the source files.
4 -
5 - ## How It Works
6 -
7 - 1. Upload a clip to your insertions library (separate from the media library)
8 - 2. Place that clip on one or more items at a specific position
9 - 3. When a fan streams the item, the clips play at the designated points
10 -
11 - The original item file is never modified.
12 -
13 - ## Managing Insertions
14 -
15 - ### Uploading Clips
16 -
17 - From the dashboard, go to your media area and upload an insertion clip. Each clip has:
18 -
19 - - **Title** — A name for your reference (1-200 characters)
20 - - **Audio file** — The clip to insert
21 - - **Duration** — Detected automatically from the file
22 -
23 - Clips count toward your tier's total storage quota — see [Pricing Tiers](./tiers.md) for limits.
24 -
25 - ### Placing Clips on Items
26 -
27 - Once uploaded, you can place a clip on any item:
28 -
29 - 1. Open the item from your dashboard
30 - 2. Go to the insertions section
31 - 3. Click "Add Insertion"
32 - 4. Select the clip and choose a position:
33 -
34 - | Position | When it plays | Offset required? |
35 - |----------|--------------|-----------------|
36 - | **Pre-roll** | Before the item starts | No |
37 - | **Mid-roll** | At a specific timestamp | Yes (milliseconds) |
38 - | **Post-roll** | After the item ends | No |
39 -
40 - You can place the same clip on multiple items, and place multiple clips on a single item. Use the sort order to control the sequence when multiple clips share a position.
41 -
42 - ### Removing Placements
43 -
44 - Removing a placement detaches the clip from the item. The clip itself stays in your library for reuse.
45 -
46 - ### Deleting Clips
47 -
48 - Deleting a clip from your library also removes all its placements across all items. The S3 file is deleted and storage quota is freed.
49 -
50 - ## Use Cases
51 -
52 - - **Podcast intros/outros** — Upload once, attach to every episode
53 - - **Sponsor reads** — Place mid-roll ads at natural break points
54 - - **Brand jingles** — Consistent audio branding across releases
55 - - **Announcements** — Temporary pre-roll for tour dates, sales, or new releases — remove the placement when it's no longer relevant
56 -
57 - ## See Also
58 -
59 - - [Content & Items](./02-content.md) — Creating audio items
60 - - [Media Library](./media-library.md) — Storing images and video for embedding
61 - - [Items](./items.md) — Item management details
@@ -0,0 +1,66 @@
1 + # Dynamic Clips
2 +
3 + Dynamic clips let you attach reusable audio segments to your items without editing the source files. Upload a clip once and place it on any number of items as a pre-roll, mid-roll, or post-roll.
4 +
5 + When a fan streams the item, the clips play at the designated points. The original file is never modified, so you can add, swap, or remove clips at any time.
6 +
7 + ## How It Works
8 +
9 + 1. Upload a clip to your clip library (separate from the media library)
10 + 2. Add that clip to one or more items at a specific position
11 + 3. Fans hear the clips seamlessly during playback — they can skip if they choose
12 +
13 + ## Managing Clips
14 +
15 + ### Uploading Clips
16 +
17 + From the dashboard, go to your media area and upload a clip. Each clip has:
18 +
19 + - **Title** — A name for your reference (1-200 characters)
20 + - **Audio file** — The clip to attach
21 + - **Duration** — Detected automatically from the file
22 +
23 + Clips count toward your tier's total storage quota — see [Pricing Tiers](./tiers.md) for limits.
24 +
25 + ### Adding Clips to Items
26 +
27 + Once uploaded, you can add a clip to any item:
28 +
29 + 1. Open the item from your dashboard
30 + 2. Go to the Dynamic Clips section
31 + 3. Click "Add"
32 + 4. Select the clip and choose a position:
33 +
34 + | Position | When it plays | Offset required? |
35 + |----------|--------------|-----------------|
36 + | **Pre-roll** | Before the item starts | No |
37 + | **Mid-roll** | At a specific timestamp | Yes (seconds) |
38 + | **Post-roll** | After the item ends | No |
39 +
40 + You can add the same clip to multiple items, and add multiple clips to a single item. Use the sort order to control the sequence when multiple clips share a position.
41 +
42 + ### Removing Clips from Items
43 +
44 + Removing a clip from an item detaches it. The clip itself stays in your library for reuse.
45 +
46 + ### Deleting Clips
47 +
48 + Deleting a clip from your library also removes it from all items. The file is deleted and storage quota is freed.
49 +
50 + ## Ideas and Use Cases
51 +
52 + Dynamic clips are flexible — use them for anything that benefits from repeatable, swappable audio segments:
53 +
54 + - **Announcements** — Temporary pre-roll for tour dates, sales, crowdfunding campaigns, or new releases. Remove when no longer relevant.
55 + - **Intros and outros** — Upload once, attach to every episode or track. Update your intro across your entire catalog by swapping one clip.
56 + - **Sponsorship reads** — Place mid-roll ads at natural break points. Swap sponsors without re-uploading episodes.
57 + - **Brand audio** — Consistent jingles, stingers, or sonic branding across releases.
58 + - **Seasonal greetings** — Holiday messages, anniversary notes, or event tie-ins that rotate in and out.
59 + - **Calls to action** — Post-roll clips encouraging fans to subscribe, follow, or check out related work.
60 + - **Creative experiments** — Layer spoken-word intros over instrumental releases, add commentary tracks, or attach behind-the-scenes notes.
61 +
62 + ## See Also
63 +
64 + - [Content & Items](./02-content.md) — Creating audio items
65 + - [Media Library](./media-library.md) — Storing images and video for embedding
66 + - [Items](./items.md) — Item management details
@@ -10,7 +10,7 @@ You can export all of your data from Makenot.work at any time. No lock-in, no fr
10 10 | Sales | CSV | Date, item, amount, status, and buyer email for every sale |
11 11 | Purchases | CSV | Date, item, amount, and status for everything you have bought |
12 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 |
13 + | Content Files | ZIP | All uploaded files (audio, video, covers, versions, dynamic clips) with a manifest |
14 14
15 15 ## How to Export
16 16
@@ -115,7 +115,7 @@ Before you go further, a few realities worth understanding up front.
115 115
116 116 **This is a one-person operation.** The source code is public, all data is exportable at any time, and fan payments are held in your Stripe account (not ours). But support responses happen during business hours, not 24/7. See [What We Guarantee](../about/guarantees.md) for the full continuity plan.
117 117
118 - **The subscription costs money whether you sell or not.** If your revenue doesn't cover your subscription, an [earn-back credit program](../support/faq.md#what-if-i-dont-earn-back-my-subscription-cost) is coming (no later than January 1, 2027). In the meantime, use the [pricing calculator](/pricing) to compare what you'd keep here versus percentage-cut platforms at your revenue level.
118 + **The subscription costs money whether you sell or not.** An [earn-back credit program](../about/roadmap.md#earn-back-credit-program) is on the roadmap to address this. In the meantime, use the [pricing calculator](/pricing) to compare what you'd keep here versus percentage-cut platforms at your revenue level.
119 119
120 120 **Moderation appeals are reviewed by the founder.** Independent review is planned once the team grows. See [Appeals](../legal/appeals.md) for the full process.
121 121
@@ -109,15 +109,15 @@ Timestamp markers for audio items. Fans can jump to specific sections within a t
109 109
110 110 Chapters display sorted by sort order first, then by start time.
111 111
112 - ## Content Insertions
112 + ## Dynamic Clips
113 113
114 - Audio items support insertions — reusable clips placed before, during, or after the main audio:
114 + Audio items support dynamic clips — reusable audio segments placed before, during, or after the main audio:
115 115
116 116 - **Pre-roll**: Plays before the main content
117 - - **Mid-roll**: Inserted at a specific timestamp
117 + - **Mid-roll**: Plays at a specific timestamp
118 118 - **Post-roll**: Plays after the main content
119 119
120 - Upload clips once and place them across multiple items. See [Content Insertions](./content-insertions.md) for the full guide.
120 + Upload clips once and add them across multiple items. See [Dynamic Clips](./dynamic-clips.md) for the full guide.
121 121
122 122 ## Cover Images
123 123
@@ -54,4 +54,4 @@ Delete files from the Media tab. Deleting a file removes it from the CDN and fre
54 54 - [Content & Items](./02-content.md) — Writing item descriptions
55 55 - [Blog](./blog.md) — Blog posts with embedded media
56 56 - [Pricing Tiers](./tiers.md) — Storage limits per tier
57 - - [Content Insertions](./content-insertions.md) — Audio clips for pre/mid/post-roll
57 + - [Dynamic Clips](./dynamic-clips.md) — Reusable audio clips for pre/mid/post-roll
@@ -57,11 +57,9 @@ Don't overthink this. If you have a website, link it. If you're active on social
57 57
58 58 ## Customizing Your Storefront
59 59
60 - Your profile isn't a template — it's a canvas. You can write custom CSS and rearrange elements on your page to make it look and feel like your own site, within a structure that keeps the experience consistent for fans browsing the platform.
60 + Storefront customization — custom colors, fonts, CSS overrides, and template access — is on the [roadmap](../about/roadmap.md#creator-storefront-customization). The current profile uses the platform's default theme, which is designed to look clean and get out of the way of your work.
61 61
62 - Think of it like the old MySpace or early YouTube: the platform provides the frame, you control the aesthetic. Change colors, fonts, spacing, and layout to match your visual identity. The consistent navigation and purchase flow stay intact so fans always know how to find, buy, and access your work — but everything in between is yours to shape.
63 -
64 - You don't need to know CSS to have a good-looking page. The defaults work. But if you want your storefront to feel like *yours* rather than a profile on someone else's site, the tools are there.
62 + When customization ships, it will be available on every tier.
65 63
66 64 ## What Fans See
67 65