max / makenotwork
69 files changed,
+2141 insertions,
-1736 deletions
| @@ -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 |