max / makenotwork
32 files changed,
+1920 insertions,
-174 deletions
| @@ -87,6 +87,12 @@ Everything listed here is live and working. | |||
| 87 | 87 | - **Transactional email**: Password reset, email verification, purchase receipts, subscription updates, sale and follower notifications | |
| 88 | 88 | - **Git source browser**: Browse server-hosted repositories with syntax highlighting | |
| 89 | 89 | - **SSH git access**: Clone and push to hosted repositories with SSH key authentication | |
| 90 | + | - **Email-first issue tracker**: File issues and reply via email, close via commit message | |
| 91 | + | - **Git patch inbound**: Email patches to a project address, threaded in community forums | |
| 92 | + | - **Custom domains**: Point your own domain to a project page with automatic TLS | |
| 93 | + | - **Mailing lists**: Per-project mailing lists with auto-subscribe on follow or purchase | |
| 94 | + | - **Content newsletters**: Blog post and release announcements delivered to mailing list subscribers | |
| 95 | + | - **Creator tier enforcement**: Storage tracking and per-tier limits with grace period | |
| 90 | 96 | - **Health monitoring**: Uptime tracking, service connectivity checks | |
| 91 | 97 | - **Malware scanning**: Every uploaded file is scanned for malware before it's made available | |
| 92 | 98 | - **1000+ automated tests**: Comprehensive test suite covering all platform features | |
| @@ -135,11 +141,7 @@ Creators upload pre-converted files (FLAC, MP3 320k, etc.). Fans choose their pr | |||
| 135 | 141 | ||
| 136 | 142 | ### Open source creator tools | |
| 137 | 143 | ||
| 138 | - | Built-in git hosting with source browser, plus GitHub integration for software creators. Sponsor tiers, license display, release hosting, build status badges. | |
| 139 | - | ||
| 140 | - | ### Custom domains | |
| 141 | - | ||
| 142 | - | Point your own domain to a project page. DNS verification, automatic TLS via Caddy. | |
| 144 | + | Sponsor tiers, license display, release hosting, build status badges. Git-backed wikis, compare view, code search. | |
| 143 | 145 | ||
| 144 | 146 | ### Payment independence | |
| 145 | 147 |
| @@ -12,9 +12,16 @@ Items are individual pieces of content — a song, an article, a software releas | |||
| 12 | 12 | ||
| 13 | 13 | | Type | Content | Player/Viewer | Chapters | Versions | | |
| 14 | 14 | |------|---------|--------------|----------|----------| | |
| 15 | - | | **Audio** | MP3, WAV, FLAC, OGG | In-browser streaming player | Yes | Yes | | |
| 15 | + | | **Audio** | MP3, WAV, FLAC, OGG, AAC, AIFF | In-browser streaming player | Yes | Yes | | |
| 16 | 16 | | **Text** | Markdown | Clean reading view | No | No | | |
| 17 | - | | **Digital** | Any file (ZIP, DMG, EXE, etc.) | Download link | No | Yes | | |
| 17 | + | | **Digital** | Any file (ZIP, DMG, EXE, PDF, etc.) | Download link | No | Yes | | |
| 18 | + | | **Video** | Video files | Coming soon | No | Yes | | |
| 19 | + | | **Image** | Image files | Gallery view | No | No | | |
| 20 | + | | **Plugin** | Audio plugins (VST, AU, CLAP) | Download link | No | Yes | | |
| 21 | + | | **Preset** | Presets and patches | Download link | No | Yes | | |
| 22 | + | | **Sample** | Sample packs | Download link | No | Yes | | |
| 23 | + | | **Course** | Educational content | Course viewer | No | Yes | | |
| 24 | + | | **Template** | Templates and themes | Download link | No | Yes | | |
| 18 | 25 | ||
| 19 | 26 | Choose the type when creating the item. It cannot be changed afterward. | |
| 20 | 27 | ||
| @@ -75,7 +82,7 @@ Upload audio files in MP3, WAV, FLAC, or OGG format. Each audio item gets: | |||
| 75 | 82 | - **Chapters**: Timestamp markers for navigating within the track. See [Chapters](#chapters) below. | |
| 76 | 83 | - **Downloads**: Fans can download the original file after purchase. | |
| 77 | 84 | ||
| 78 | - | Upload your audio file from the item's content tab. Large files are supported — there are no per-file size limits. | |
| 85 | + | Upload your audio file from the item's content tab. Per-file size limits depend on your [pricing tier](./pricing.md) (10MB for Basic, 500MB for Small Files, 20GB for Big Files and Streaming). | |
| 79 | 86 | ||
| 80 | 87 | ### Text | |
| 81 | 88 |
| @@ -0,0 +1,399 @@ | |||
| 1 | + | # Platform Migration Guides | |
| 2 | + | ||
| 3 | + | How to move your creator business to makenot.work from other platforms. Each section covers how to export your data, the step-by-step migration process, and how to minimize subscriber loss during the switch. | |
| 4 | + | ||
| 5 | + | No platform offers automated subscription transfer — every subscriber must manually re-enter payment info on the new platform. Email is the only truly portable audience asset. Expect significant subscriber loss without active retention tactics. | |
| 6 | + | ||
| 7 | + | --- | |
| 8 | + | ||
| 9 | + | ## Quick Reference: Export Capabilities | |
| 10 | + | ||
| 11 | + | | Platform | Email export? | Content export? | Subscription transfer? | | |
| 12 | + | |----------|--------------|----------------|----------------------| | |
| 13 | + | | Patreon | Partial (opt-in only) | No bulk content export | No | | |
| 14 | + | | Substack | Yes (full CSV) | Yes (zip with posts) | No | | |
| 15 | + | | Gumroad | Yes (sales CSV) | No (re-upload products) | No | | |
| 16 | + | | YouTube | No | N/A | No | | |
| 17 | + | | DriveThruRPG | No | No | No | | |
| 18 | + | | Bandcamp | No | Artist can download own files | No | | |
| 19 | + | | Ghost | Yes | Yes (full JSON export) | Stripe subscriptions can transfer | | |
| 20 | + | | Udemy | No | Re-upload courses | No | | |
| 21 | + | ||
| 22 | + | --- | |
| 23 | + | ||
| 24 | + | ## From Patreon | |
| 25 | + | ||
| 26 | + | ### How to export your data | |
| 27 | + | ||
| 28 | + | **Subscriber emails (partial):** | |
| 29 | + | 1. Go to your Relationship Manager (patreon.com/members) | |
| 30 | + | 2. Click the CSV export button | |
| 31 | + | 3. This exports patron names, emails, tier, and pledge amount | |
| 32 | + | 4. **Critical limitation:** You only get emails for patrons who opted in to sharing their email. Patreon does not give you emails for patrons who didn't check that box. There is no workaround. | |
| 33 | + | ||
| 34 | + | **Posts and content:** | |
| 35 | + | Patreon has no bulk content export. You must manually save each post. For text posts, copy-paste. For files, download each attachment individually. If you have hundreds of posts, this is tedious — consider exporting only key content and letting older posts remain on Patreon during the transition. | |
| 36 | + | ||
| 37 | + | **Financial history:** | |
| 38 | + | 1. Go to your Creator dashboard | |
| 39 | + | 2. Navigate to Income > Payouts | |
| 40 | + | 3. Export transaction history as CSV | |
| 41 | + | ||
| 42 | + | ### Migration playbook | |
| 43 | + | ||
| 44 | + | **Phase 1: Build your email list (2-4 weeks before launch)** | |
| 45 | + | - Post on Patreon asking patrons to share their email (the opt-in checkbox) | |
| 46 | + | - Pin a post explaining you're adding a new way to support your work | |
| 47 | + | - Start collecting emails through a separate form (Google Forms, Buttondown, etc.) linked from your Patreon About section | |
| 48 | + | - Goal: capture as many patron emails as possible before the switch | |
| 49 | + | ||
| 50 | + | **Phase 2: Parallel run (60-90 days)** | |
| 51 | + | - Set up your MNW creator page with your existing content | |
| 52 | + | - Post new content on MNW first, then cross-post to Patreon | |
| 53 | + | - Update your Patreon About section with your MNW link | |
| 54 | + | - In every Patreon post, include a line: "This content is also available at [your MNW link]" | |
| 55 | + | - Do NOT delete or lock Patreon content yet — let existing patrons continue as normal | |
| 56 | + | ||
| 57 | + | **Phase 3: Redirect (after 60-90 days)** | |
| 58 | + | - Make your Patreon tiers invisible to new patrons (Settings > Page > hide tiers from public view) | |
| 59 | + | - Update all external links (website, social bios, link-in-bio) to point to MNW | |
| 60 | + | - Pin a Patreon post explaining the move and linking to MNW | |
| 61 | + | - Stop cross-posting to Patreon — new content is MNW-exclusive | |
| 62 | + | ||
| 63 | + | **Phase 4: Sunset (after 3-6 months)** | |
| 64 | + | - Email remaining Patreon patrons (the ones whose emails you have) with a final migration reminder | |
| 65 | + | - Consider grandfathering early MNW supporters at a lower price as a thank-you | |
| 66 | + | - Keep your Patreon page live with a pinned redirect post — don't delete it, as inbound links may still send people there | |
| 67 | + | ||
| 68 | + | ### Audience retention tactics | |
| 69 | + | ||
| 70 | + | - **Announce early and often.** Don't surprise people. Give 30+ days notice before any change in where content appears. | |
| 71 | + | - **Grandfather pricing.** Offer migrating patrons the same or better rate they were paying on Patreon. | |
| 72 | + | - **Use the parallel period.** The biggest retention killer is asking people to re-subscribe on a new platform with no content yet. 60-90 days of parallel content means your MNW page already has a library when you ask people to switch. | |
| 73 | + | ||
| 74 | + | --- | |
| 75 | + | ||
| 76 | + | ## From Substack | |
| 77 | + | ||
| 78 | + | ### How to export your data | |
| 79 | + | ||
| 80 | + | Substack has the best export in the industry. | |
| 81 | + | ||
| 82 | + | **Full export:** | |
| 83 | + | 1. Go to Settings > Exports | |
| 84 | + | 2. Click "Create new export" | |
| 85 | + | 3. Download the zip file | |
| 86 | + | ||
| 87 | + | **What you get:** | |
| 88 | + | - `posts.csv` — all posts with title, subtitle, date, URL, and full body (HTML) | |
| 89 | + | - `subscribers.csv` — full subscriber list with email, subscription type (free/paid), and date | |
| 90 | + | - Post images are included as URLs (hosted by Substack — download separately if needed) | |
| 91 | + | ||
| 92 | + | **What you don't get:** | |
| 93 | + | - Comment history | |
| 94 | + | - Substack Notes content | |
| 95 | + | - Analytics/view data | |
| 96 | + | ||
| 97 | + | The subscriber CSV gives you your full email list, including free subscribers. This is your most valuable asset — guard it. | |
| 98 | + | ||
| 99 | + | ### Migration playbook | |
| 100 | + | ||
| 101 | + | **Phase 1: Export everything (day 1)** | |
| 102 | + | - Download your full Substack export immediately. Don't wait. | |
| 103 | + | - Back up the subscriber CSV in multiple places. | |
| 104 | + | - Download any images hosted on Substack's CDN that you want to keep. | |
| 105 | + | ||
| 106 | + | **Phase 2: Set up MNW (1-2 weeks)** | |
| 107 | + | - Create your MNW creator page. | |
| 108 | + | - Re-publish your best/most popular posts as text content on MNW. You don't need to migrate everything — curate. | |
| 109 | + | - Set up your paywall for new paid content. | |
| 110 | + | ||
| 111 | + | **Phase 3: Announce the move (email your list)** | |
| 112 | + | - Use your exported subscriber list to email everyone directly (through a service like Buttondown, Postmark, or even a BCC'd email for small lists). | |
| 113 | + | - Explain why you're moving. Be specific — your readers chose Substack for the writing, not the platform. | |
| 114 | + | - Link to your MNW page. Include instructions for how to subscribe. | |
| 115 | + | ||
| 116 | + | **Phase 4: Redirect** | |
| 117 | + | - Publish a final Substack post linking to your new home. | |
| 118 | + | - Update your Substack About page with a redirect notice. | |
| 119 | + | - If you have a custom domain on Substack, point it to your MNW page (when custom domains ship) or your own site. | |
| 120 | + | - Keep the Substack archive live — inbound links from Google, social media, and other newsletters still point there. | |
| 121 | + | ||
| 122 | + | ### Audience retention tactics | |
| 123 | + | ||
| 124 | + | - **Email is your superpower.** Unlike every other platform on this list, Substack gives you your full subscriber list. Use it. A direct email from you lands harder than a platform notification. | |
| 125 | + | - **Migrate your back catalog.** Readers will want to see that your new home has content before they subscribe. Bring over your 10-20 best posts. | |
| 126 | + | - **Offer a free month.** If you had paid subscribers on Substack, offer them a free month on MNW as a thank-you for making the switch. | |
| 127 | + | - **Time it with a post.** Announce the move alongside a piece of writing that reminds people why they subscribed in the first place. Don't make the migration the story — make the writing the story. | |
| 128 | + | ||
| 129 | + | --- | |
| 130 | + | ||
| 131 | + | ## From Gumroad | |
| 132 | + | ||
| 133 | + | ### How to export your data | |
| 134 | + | ||
| 135 | + | **Customer list:** | |
| 136 | + | 1. Go to your Gumroad dashboard > Audience | |
| 137 | + | 2. Export customers as CSV | |
| 138 | + | 3. You get: email, name, product purchased, date, amount paid | |
| 139 | + | ||
| 140 | + | **Sales data:** | |
| 141 | + | 1. Dashboard > Analytics | |
| 142 | + | 2. Export sales history as CSV | |
| 143 | + | ||
| 144 | + | **Product files:** | |
| 145 | + | Gumroad does not offer bulk file export. You must already have your original product files (PDFs, zip archives, audio files, etc.). If you uploaded to Gumroad and didn't keep local copies, download each product's files from your product editor page. | |
| 146 | + | ||
| 147 | + | ### Migration playbook | |
| 148 | + | ||
| 149 | + | **Phase 1: Prepare (1-2 weeks)** | |
| 150 | + | - Export your full customer list from Gumroad. | |
| 151 | + | - Verify you have local copies of all product files. | |
| 152 | + | - Set up your MNW creator page and upload your products. | |
| 153 | + | - Match your pricing. Don't change prices during a migration. | |
| 154 | + | ||
| 155 | + | **Phase 2: Email your customers (day 1 of migration)** | |
| 156 | + | - Email your entire customer list directly. Gumroad gives you emails — use them. | |
| 157 | + | - Explain the move. Keep it short: "Same products, same prices, new home." | |
| 158 | + | - Include a direct link to each product on MNW. | |
| 159 | + | - Consider a migration discount (10-15% off next purchase) to incentivize bookmarking your new store. | |
| 160 | + | ||
| 161 | + | **Phase 3: Update all links (same week)** | |
| 162 | + | - Update every external link that points to Gumroad: website, social media bios, pinned tweets, YouTube descriptions, email signatures, course materials. | |
| 163 | + | - Update embedded buy buttons on your website to point to MNW (when embeddable widgets ship) or use direct links. | |
| 164 | + | - If you have a custom domain on Gumroad, redirect it. | |
| 165 | + | ||
| 166 | + | **Phase 4: Sunset Gumroad (after 30-60 days)** | |
| 167 | + | - Stop listing new products on Gumroad. | |
| 168 | + | - Update Gumroad product descriptions to say "This product has moved to [MNW link]." | |
| 169 | + | - Keep existing products live with the redirect notice — buyers may still find them via old links or Google. | |
| 170 | + | ||
| 171 | + | ### Audience retention tactics | |
| 172 | + | ||
| 173 | + | - **Email everyone on day one.** Gumroad gives you customer emails. Your existing customers already paid you once — they're the easiest to convert. | |
| 174 | + | - **Migration discount.** Offer 10-15% off the next purchase on MNW. | |
| 175 | + | - **Bundle your back catalog.** If you have multiple products, create a discounted bundle on MNW that gives returning customers a reason to rebuy or upgrade. | |
| 176 | + | - **Update your "receipt" email.** If you send post-purchase emails or course materials that link to Gumroad, update those links immediately. | |
| 177 | + | ||
| 178 | + | --- | |
| 179 | + | ||
| 180 | + | ## From YouTube (Memberships) | |
| 181 | + | ||
| 182 | + | ### How to export your data | |
| 183 | + | ||
| 184 | + | **The hard truth: you can't.** | |
| 185 | + | ||
| 186 | + | YouTube does not export member email addresses. There is no CSV, no API, no workaround. Your members are YouTube's members, and YouTube keeps that wall intact. | |
| 187 | + | ||
| 188 | + | **What you can get:** | |
| 189 | + | - Member count and analytics (YouTube Studio > Analytics > Membership) | |
| 190 | + | - Your own uploaded video files (Google Takeout > YouTube) | |
| 191 | + | - Comments, playlists, and other channel data (Google Takeout) | |
| 192 | + | ||
| 193 | + | **What you cannot get:** | |
| 194 | + | - Member email addresses | |
| 195 | + | - Any way to directly contact members off-platform | |
| 196 | + | ||
| 197 | + | ### Migration playbook | |
| 198 | + | ||
| 199 | + | Because you cannot export your member list, the entire strategy shifts to building an independent email list before you migrate. | |
| 200 | + | ||
| 201 | + | **Phase 1: Build an email list (start immediately — this takes months)** | |
| 202 | + | - Pin a community post asking members to join your mailing list. Link to a signup form (Buttondown, Mailchimp free tier, Google Forms — anything that captures emails). | |
| 203 | + | - Mention the mailing list at the end of every video. "If you want to make sure you never miss my stuff regardless of what happens with YouTube, join my email list." | |
| 204 | + | - Add the mailing list link to your channel banner, About section, and video descriptions. | |
| 205 | + | - Goal: convert as many members as possible to email subscribers before announcing any move. | |
| 206 | + | ||
| 207 | + | **Phase 2: Announce on YouTube (after building your list)** | |
| 208 | + | - Make a video explaining the change. | |
| 209 | + | - Don't frame it as "leaving YouTube" — frame it as "adding a better way to support me." | |
| 210 | + | - Keep making YouTube videos. YouTube is your discovery engine. MNW is where the support goes. | |
| 211 | + | ||
| 212 | + | **Phase 3: Run parallel indefinitely** | |
| 213 | + | - Unlike other platforms, you probably shouldn't fully leave YouTube memberships. YouTube is where your audience discovers you. | |
| 214 | + | - New supporters sign up on MNW. Existing YouTube members can stay or switch. | |
| 215 | + | - Exclusive content goes to MNW first. YouTube gets a delayed or truncated version. | |
| 216 | + | - Over time, the MNW audience grows and the YouTube membership becomes secondary. | |
| 217 | + | ||
| 218 | + | **Phase 4: (Optional) Sunset memberships** | |
| 219 | + | - Only do this once your MNW supporter base exceeds your YouTube membership base. | |
| 220 | + | - Announce well in advance. Pin a post. Mention it in videos. | |
| 221 | + | - Turn off YouTube memberships but keep your channel active for discovery. | |
| 222 | + | ||
| 223 | + | ### Audience retention tactics | |
| 224 | + | ||
| 225 | + | - **This is a long game.** YouTube migration takes months, not weeks. Accept that. | |
| 226 | + | - **Email list is everything.** Until you have emails, you have nothing portable. Every video should mention the mailing list. | |
| 227 | + | - **Don't burn the bridge.** YouTube is still the best free discovery engine for video creators. The goal is to redirect the support, not leave the platform. | |
| 228 | + | - **Create MNW-exclusive value.** Give email subscribers and MNW supporters something they can't get on YouTube — early access, bonus content, behind-the-scenes. | |
| 229 | + | ||
| 230 | + | --- | |
| 231 | + | ||
| 232 | + | ## From DriveThruRPG | |
| 233 | + | ||
| 234 | + | ### How to export your data | |
| 235 | + | ||
| 236 | + | **Customer emails:** Not available. DriveThruRPG does not share customer contact information with publishers. This is the biggest lock-in mechanism. | |
| 237 | + | ||
| 238 | + | **Sales data:** Download sales reports from your Publisher Admin dashboard. You get dates, product names, quantities, and revenue — but not customer emails. | |
| 239 | + | ||
| 240 | + | **Product files:** You should already have your original files (PDFs, maps, art assets). DriveThruRPG doesn't provide a bulk download of your uploaded files, so ensure your local copies are current. | |
| 241 | + | ||
| 242 | + | ### Migration playbook | |
| 243 | + | ||
| 244 | + | **Phase 1: Go multi-platform (immediately)** | |
| 245 | + | - If you're currently exclusive on DriveThruRPG, switch to non-exclusive. You lose the exclusive royalty rate, but you gain the ability to sell everywhere else. | |
| 246 | + | - Set up your MNW store and upload your catalog. | |
| 247 | + | - Start building an email list independently. Add a signup link to your product descriptions, your website, and your social media. | |
| 248 | + | ||
| 249 | + | **Phase 2: Build direct audience (ongoing)** | |
| 250 | + | - Every product description on DriveThruRPG should include a line: "Get my latest releases at [your MNW link]." | |
| 251 | + | - Post in TTRPG communities (r/rpg, r/osr, r/pbta, Discord servers, itch.io forums) with links to your MNW store. | |
| 252 | + | - Offer MNW-exclusive products or early access to new releases on MNW. | |
| 253 | + | - Use social media (particularly Twitter/X, Mastodon, and Bluesky TTRPG communities) to drive traffic to MNW. | |
| 254 | + | ||
| 255 | + | **Phase 3: New releases on MNW first (ongoing)** | |
| 256 | + | - Publish new products on MNW before DriveThruRPG (or exclusively on MNW). | |
| 257 | + | - Keep older products on DriveThruRPG for discoverability. | |
| 258 | + | - Think of DriveThruRPG as a billboard and MNW as your store. | |
| 259 | + | ||
| 260 | + | **Phase 4: Don't fully leave DriveThruRPG (probably)** | |
| 261 | + | - DriveThruRPG is the largest TTRPG marketplace. It has organic traffic you can't replicate. | |
| 262 | + | - Keep a presence there for discovery, but direct your existing audience to MNW. | |
| 263 | + | ||
| 264 | + | ### Audience retention tactics | |
| 265 | + | ||
| 266 | + | - **You don't have an audience to retain.** DriveThruRPG never gave you one. This is a build-from-scratch situation, which is why the parallel approach matters. | |
| 267 | + | - **Community is your audience.** TTRPG creators with active Discord servers, subreddit presences, or newsletter followings have the easiest path. If you don't have these yet, start building them now. | |
| 268 | + | - **itch.io as a bridge.** Many TTRPG publishers are already on itch.io alongside DriveThruRPG. If you're on itch.io, you're already used to multi-platform selling. Add MNW as another storefront, then consolidate. | |
| 269 | + | ||
| 270 | + | --- | |
| 271 | + | ||
| 272 | + | ## From Bandcamp | |
| 273 | + | ||
| 274 | + | ### How to export your data | |
| 275 | + | ||
| 276 | + | **Fan emails:** Not available. Bandcamp does not export fan/buyer email addresses. You can see who purchased your music, but you cannot download a list or contact them outside Bandcamp messaging. | |
| 277 | + | ||
| 278 | + | **Your music files:** You should have your original masters and release files locally. Bandcamp does let you download your own uploaded files from the album editor, but there's no bulk export. | |
| 279 | + | ||
| 280 | + | **Sales data:** Go to your artist dashboard > Stats > Sales. You can view sales history but export options are limited. | |
| 281 | + | ||
| 282 | + | ### Migration playbook | |
| 283 | + | ||
| 284 | + | **Phase 1: Build an email list (start now)** | |
| 285 | + | - Add a mailing list signup link to your Bandcamp bio, every album description, and every release announcement. | |
| 286 | + | - Use a service like Buttondown, Mailchimp, or Sendy to collect emails independently. | |
| 287 | + | - On Bandcamp Fridays, include a note in your release announcements: "Want to hear about new releases first? Join my mailing list." | |
| 288 | + | ||
| 289 | + | **Phase 2: Set up MNW as your second storefront** | |
| 290 | + | - Upload your catalog to MNW. Focus on your best-selling or most recent releases. | |
| 291 | + | - MNW's audio player with chapters is a differentiator for longer works (EPs, albums, podcasts, audiobooks). | |
| 292 | + | - Price identically to Bandcamp. | |
| 293 | + | ||
| 294 | + | **Phase 3: Drive traffic to MNW** | |
| 295 | + | - When announcing new releases on social media, link to MNW instead of Bandcamp (or both). | |
| 296 | + | - Email your mailing list with MNW links for new releases. | |
| 297 | + | - Keep Bandcamp for discovery — fans who find you on Bandcamp can be directed to MNW for future purchases via your mailing list. | |
| 298 | + | ||
| 299 | + | **Phase 4: Don't leave Bandcamp entirely** | |
| 300 | + | - Bandcamp has real discovery value for musicians. The editorial features, genre browsing, and fan collections drive organic sales. | |
| 301 | + | - Think of Bandcamp as a discovery funnel and MNW as your primary storefront. | |
| 302 | + | - Over time, as your mailing list grows, MNW sales should overtake Bandcamp sales. | |
| 303 | + | ||
| 304 | + | ### Audience retention tactics | |
| 305 | + | ||
| 306 | + | - **You can't retain what you don't have.** Like DriveThruRPG, Bandcamp doesn't give you fan emails. This is a funnel-building exercise, not a migration. | |
| 307 | + | - **Use Bandcamp to funnel.** Every Bandcamp album description, bio section, and thank-you email should mention your mailing list and MNW store. | |
| 308 | + | - **Physical merch stays on Bandcamp.** MNW doesn't do physical products. Keep merch on Bandcamp (or Big Cartel, Shopify, etc.) and digital on MNW. | |
| 309 | + | ||
| 310 | + | --- | |
| 311 | + | ||
| 312 | + | ## From Ghost / Beehiiv | |
| 313 | + | ||
| 314 | + | ### How to export your data | |
| 315 | + | ||
| 316 | + | **Ghost:** | |
| 317 | + | 1. Settings > Labs > Export | |
| 318 | + | 2. Downloads a JSON file with all posts, pages, tags, and settings | |
| 319 | + | 3. Member list exports as CSV with full emails | |
| 320 | + | 4. Stripe subscriptions can potentially transfer (both platforms use Stripe Connect — coordinate with MNW support) | |
| 321 | + | ||
| 322 | + | **Beehiiv:** | |
| 323 | + | 1. Settings > Account > Export Data | |
| 324 | + | 2. Subscriber list exports as CSV | |
| 325 | + | 3. Post content can be exported | |
| 326 | + | ||
| 327 | + | Ghost has the best data portability of any newsletter platform. If you're on Ghost, you've already proven you're willing to migrate. | |
| 328 | + | ||
| 329 | + | ### Migration playbook | |
| 330 | + | ||
| 331 | + | **Phase 1: Evaluate whether it makes sense** | |
| 332 | + | - If you're self-hosting Ghost and it's working, the main reason to move is simplification (not cost). | |
| 333 | + | - If you're on Beehiiv's free plan and happy, there may not be a strong reason to move unless you want source-available infrastructure or are selling digital products alongside your newsletter. | |
| 334 | + | ||
| 335 | + | **Phase 2: Export and set up** | |
| 336 | + | - Export your Ghost/Beehiiv data. | |
| 337 | + | - Set up MNW with your best content. | |
| 338 | + | - If your Stripe subscriptions can transfer, coordinate with MNW support to make this as seamless as possible. Ghost-to-MNW via Stripe Connect could be the smoothest subscription migration available. | |
| 339 | + | ||
| 340 | + | **Phase 3: Redirect** | |
| 341 | + | - Point your custom domain to MNW (when supported). | |
| 342 | + | - Email your subscriber list with the new URL. | |
| 343 | + | - Update all external links. | |
| 344 | + | ||
| 345 | + | **Phase 4: This is straightforward** | |
| 346 | + | - You've already migrated once (to Ghost or Beehiiv). You know the drill. | |
| 347 | + | - The subscriber list is yours. The content is yours. The hard part is already done. | |
| 348 | + | ||
| 349 | + | ### Audience retention tactics | |
| 350 | + | ||
| 351 | + | - **Stripe subscription continuity.** If MNW can accept existing Stripe subscriptions from Ghost, this is the closest thing to a zero-friction migration. Subscribers don't have to re-enter payment info. | |
| 352 | + | - **You already have emails.** Both Ghost and Beehiiv give you full subscriber lists. Use them. | |
| 353 | + | - **Keep it honest.** Ghost is a great platform. If someone is happy on Ghost, don't push. MNW is a better fit for creators who want simplicity, who don't need custom themes, or who are selling digital products alongside text. | |
| 354 | + | ||
| 355 | + | --- | |
| 356 | + | ||
| 357 | + | ## From Udemy | |
| 358 | + | ||
| 359 | + | ### How to export your data | |
| 360 | + | ||
| 361 | + | **Student emails:** Not available. Udemy does not share student email addresses with instructors. You can message students through Udemy's platform, but you cannot export a contact list. | |
| 362 | + | ||
| 363 | + | **Course content:** You should have your original video files, slides, and materials. Udemy does not provide a bulk download of uploaded content. | |
| 364 | + | ||
| 365 | + | **Revenue data:** Download from Instructor Dashboard > Revenue Report. | |
| 366 | + | ||
| 367 | + | ### Migration playbook | |
| 368 | + | ||
| 369 | + | **Phase 1: Keep Udemy for discovery, sell direct for everything else** | |
| 370 | + | - Do NOT leave Udemy immediately. Udemy's organic traffic is valuable for finding new students. | |
| 371 | + | - Set up your MNW store with course materials (PDFs, code samples, supplementary files now; full courses when video ships). | |
| 372 | + | - In every Udemy course, add a lecture (or update your welcome lecture) mentioning your website/MNW store: "Get my latest courses, bonus materials, and best prices at [your MNW link]." | |
| 373 | + | ||
| 374 | + | **Phase 2: Build your email list through your courses** | |
| 375 | + | - Add a "Resources" lecture to each Udemy course with a link to a free resource hosted on MNW, requiring an email signup. | |
| 376 | + | - Use your Udemy course Q&A and announcement features to mention your mailing list. | |
| 377 | + | - Create a free lead magnet (cheat sheet, summary PDF, starter template) hosted on MNW to capture student emails. | |
| 378 | + | ||
| 379 | + | **Phase 3: New courses go to MNW first** | |
| 380 | + | - Publish new course content on MNW. If it sells well, consider a delayed Udemy release (or don't release on Udemy at all). | |
| 381 | + | - Price your MNW courses at your real price. Udemy can discount their copy during sales — you control your own pricing on MNW. | |
| 382 | + | ||
| 383 | + | **Phase 4: Reduce Udemy dependency over time** | |
| 384 | + | - As your email list grows, your direct audience grows with it. | |
| 385 | + | - Consider keeping a few introductory or older courses on Udemy as loss leaders that funnel students to your MNW store for premium content. | |
| 386 | + | ||
| 387 | + | ### Audience retention tactics | |
| 388 | + | ||
| 389 | + | - **You don't have students to retain — you have students to capture.** Udemy doesn't give you emails, so your first priority is converting Udemy students into email subscribers. | |
| 390 | + | - **Free resources as email magnets.** Offer bonus PDFs, templates, or starter kits on MNW in exchange for email signup. Mention these in your Udemy courses. | |
| 391 | + | - **Don't compete with Udemy on discovery.** Udemy has 70+ million students. You cannot replicate that. Use Udemy as a top-of-funnel and MNW as your direct sales platform. | |
| 392 | + | ||
| 393 | + | --- | |
| 394 | + | ||
| 395 | + | ## See Also | |
| 396 | + | ||
| 397 | + | - [Data Export](./files.md) — Export formats and what's included | |
| 398 | + | - [Contact Sharing](./contact-sharing.md) — Building your email list on MNW | |
| 399 | + | - [How We Work](../about/how-we-work.md) — Business model and pricing |
| @@ -0,0 +1,312 @@ | |||
| 1 | + | # Pricing Tiers | |
| 2 | + | ||
| 3 | + | Choose the tier that matches your content. Every tier includes all features from lower tiers. | |
| 4 | + | ||
| 5 | + | | Tier | Monthly | For | Per-file limit | Total storage | Download budget | | |
| 6 | + | |------|---------|-----|---------------|---------------|-----------------| | |
| 7 | + | | **Basic** | $10 | Text, blogs, newsletters | 10MB | 50GB | 2GB (50MB/file) | | |
| 8 | + | | **Small Files** | $20 | Audio, plugins, small software | 500MB | 250GB | 5GB (500MB/file) | | |
| 9 | + | | **Big Files** | $30 | Video, games, large software | 20GB | 500GB | 10GB (2GB/file) | | |
| 10 | + | | **Streaming** | $40 | Live streaming + everything above | 20GB | 500GB + VOD | 10GB (2GB/file) | | |
| 11 | + | ||
| 12 | + | All tiers include: 0% platform fee on fan payments, custom profile, project organization, data export, subscriptions, RSS, analytics, 2FA/passkeys. | |
| 13 | + | ||
| 14 | + | **Download budget**: Every tier includes a separate allowance for general-purpose compressed downloads (zips, bundles, supplementary materials). This is independent of your primary content storage. | |
| 15 | + | ||
| 16 | + | **Need a larger file?** Big Files and Streaming creators can request a per-file size increase beyond 20GB. Submit a brief description of the file and its size from your dashboard. Requests are typically approved same-day for legitimate content. | |
| 17 | + | ||
| 18 | + | --- | |
| 19 | + | ||
| 20 | + | ## Basic — $10/month | |
| 21 | + | ||
| 22 | + | For writers, bloggers, journalists, newsletter creators, and anyone whose primary content is written. | |
| 23 | + | ||
| 24 | + | ### What You Get | |
| 25 | + | ||
| 26 | + | - Unlimited posts and articles | |
| 27 | + | - Rich text editor with markdown support | |
| 28 | + | - Code syntax highlighting | |
| 29 | + | - Free, paid, and subscriber-only posts | |
| 30 | + | - Pay-what-you-want pricing | |
| 31 | + | - Subscription tiers | |
| 32 | + | - RSS feed generation | |
| 33 | + | - Contact sharing (opt-in at purchase) | |
| 34 | + | - Analytics (revenue charts, period comparisons, per-project breakdowns) | |
| 35 | + | - Export projects and items as JSON, transactions as CSV | |
| 36 | + | ||
| 37 | + | ### Storage | |
| 38 | + | ||
| 39 | + | - **50GB total** for images (covers and embedded images in posts) | |
| 40 | + | - **10MB per file** (covers, embedded images) | |
| 41 | + | - **2GB download budget** for general downloads (PDFs, ebook zips, resource bundles) at 50MB per file | |
| 42 | + | ||
| 43 | + | ### Who This Is For | |
| 44 | + | ||
| 45 | + | - **Newsletter writers** moving away from Substack's 10% cut | |
| 46 | + | - **Bloggers** who want to monetize without ads | |
| 47 | + | - **Journalists** building direct reader relationships | |
| 48 | + | - **Fiction writers** serializing stories | |
| 49 | + | - **Technical writers** with paid tutorials or guides | |
| 50 | + | - **Ebook authors** distributing PDF or EPUB collections | |
| 51 | + | ||
| 52 | + | ### Compared to Substack | |
| 53 | + | ||
| 54 | + | | | Makenot.work | Substack | | |
| 55 | + | |---|---|---| | |
| 56 | + | | Monthly cost | $10 flat | Free | | |
| 57 | + | | Platform cut | 0% | 10% | | |
| 58 | + | | At $500/mo revenue | $10 cost | $50 cost | | |
| 59 | + | | At $2,000/mo revenue | $10 cost | $200 cost | | |
| 60 | + | | Subscriber export | Full, always | Full | | |
| 61 | + | | Data export | Full (JSON + CSV) | Posts + subscriber list | | |
| 62 | + | | One-time purchases | Yes | No (subscriptions only) | | |
| 63 | + | | PWYW pricing | Yes | No | | |
| 64 | + | | Open source | Yes | No | | |
| 65 | + | | Discovery network | No | Yes (32M in-app subscribers) | | |
| 66 | + | | Newsletter delivery | Broadcast only | Full post-as-email | | |
| 67 | + | | Mobile app | No | Yes | | |
| 68 | + | | Comments | No | Yes (threaded) | | |
| 69 | + | ||
| 70 | + | Break-even: If you earn more than $100/month from fans, the Basic tier is cheaper than Substack. | |
| 71 | + | ||
| 72 | + | **The trade-off:** Substack has a massive discovery network and delivers every post as an email. MNW has broadcast emails but not post-as-email delivery. If you already have an audience and a mailing list, MNW saves you money. If you need to build an audience from scratch, Substack's network is hard to beat. | |
| 73 | + | ||
| 74 | + | --- | |
| 75 | + | ||
| 76 | + | ## Small Files — $20/month | |
| 77 | + | ||
| 78 | + | For musicians, podcasters, sound designers, indie developers, and plugin makers. | |
| 79 | + | ||
| 80 | + | ### What You Get (in addition to Basic) | |
| 81 | + | ||
| 82 | + | - Audio hosting (MP3, WAV, FLAC, OGG, AAC, AIFF) | |
| 83 | + | - In-browser streaming via custom audio player | |
| 84 | + | - Chapter markers with timestamps | |
| 85 | + | - Cover art per item | |
| 86 | + | - Podcast RSS feeds | |
| 87 | + | - Binary downloads (.zip, .dmg, .exe, .appimage, .deb, .tar.gz, .clap, .vst3) | |
| 88 | + | - Versioned releases with changelogs | |
| 89 | + | - License keys with activation tracking | |
| 90 | + | - Promo codes and discount codes | |
| 91 | + | ||
| 92 | + | ### Storage | |
| 93 | + | ||
| 94 | + | - **250GB total** for primary content (audio files, binaries, plugins) | |
| 95 | + | - **500MB per file** | |
| 96 | + | - **5GB download budget** for general downloads (liner notes, documentation, bonus materials) at 500MB per file | |
| 97 | + | ||
| 98 | + | ### Who This Is For | |
| 99 | + | ||
| 100 | + | - **Musicians** selling music directly to fans | |
| 101 | + | - **Podcasters** monetizing without ads | |
| 102 | + | - **Sound designers** selling sample packs | |
| 103 | + | - **Plugin makers** selling CLAP/VST3 instruments and effects | |
| 104 | + | - **Indie developers** distributing desktop apps under 500MB | |
| 105 | + | - **Preset designers** selling instrument presets and sound banks | |
| 106 | + | - **Template makers** selling project templates and starter kits | |
| 107 | + | ||
| 108 | + | ### Compared to Bandcamp | |
| 109 | + | ||
| 110 | + | | | Makenot.work | Bandcamp | | |
| 111 | + | |---|---|---| | |
| 112 | + | | Monthly cost | $20 flat | Free ($10 for Pro) | | |
| 113 | + | | Revenue share | 0% | 15% (10% after $5k) | | |
| 114 | + | | At $500/mo sales | $20 cost | $75 cost | | |
| 115 | + | | At $2,000/mo sales | $20 cost | $300 cost | | |
| 116 | + | | Per-file limit | 500MB | 291MB free, 2GB Pro | | |
| 117 | + | | Total storage | 250GB | No documented cap | | |
| 118 | + | | Fan contact export | Full | CSV export | | |
| 119 | + | | License keys | Built-in | No | | |
| 120 | + | | Versioned releases | Built-in | No | | |
| 121 | + | | Format transcoding | No (serve as uploaded) | Yes (WAV to 8 formats) | | |
| 122 | + | | Embeddable player | No | Yes | | |
| 123 | + | | Merch / physical goods | No | Yes (vinyl, CDs, apparel) | | |
| 124 | + | | Discovery | No | Yes (editorial, tags, Bandcamp Daily) | | |
| 125 | + | | Mobile app | No | Yes (fan + artist) | | |
| 126 | + | | Pre-orders | No | Yes | | |
| 127 | + | ||
| 128 | + | Break-even: If you earn more than ~$133/month, we're cheaper than Bandcamp. | |
| 129 | + | ||
| 130 | + | **The trade-off:** Bandcamp has format transcoding (upload WAV, fans download in 8 formats), an embeddable player widget, merch support, and a strong discovery ecosystem with editorial curation. MNW has license keys and versioned releases that Bandcamp lacks, and takes 0% of your revenue. | |
| 131 | + | ||
| 132 | + | ### Compared to Patreon | |
| 133 | + | ||
| 134 | + | | | Makenot.work | Patreon | | |
| 135 | + | |---|---|---| | |
| 136 | + | | Monthly cost | $20 flat | Free | | |
| 137 | + | | Platform cut | 0% | 10% | | |
| 138 | + | | At $1,000/mo | $20 cost | $100+ cost | | |
| 139 | + | | Audio hosting | Native player | Native (5GB/file) | | |
| 140 | + | | Audio player | Built-in | Built-in | | |
| 141 | + | | Podcast RSS | Yes | Yes (private feeds, Spotify) | | |
| 142 | + | | Community features | No | Yes (chat, polls, DMs) | | |
| 143 | + | | Mobile app | No | Yes | | |
| 144 | + | ||
| 145 | + | --- | |
| 146 | + | ||
| 147 | + | ## Big Files — $30/month | |
| 148 | + | ||
| 149 | + | For video creators, game developers, filmmakers, educators, and anyone producing large content. | |
| 150 | + | ||
| 151 | + | *Video hosting and transcoding are not yet implemented. Digital file downloads of any size (up to 20GB per file) are available now.* | |
| 152 | + | ||
| 153 | + | ### What You Get (in addition to Small Files) | |
| 154 | + | ||
| 155 | + | - Video uploads up to 4K resolution (planned) | |
| 156 | + | - Automatic transcoding: 360p, 720p, 1080p, 4K (planned) | |
| 157 | + | - Adaptive streaming (planned) | |
| 158 | + | - Thumbnail generation and custom thumbnails (planned) | |
| 159 | + | - Subscriber-only and pay-per-view videos | |
| 160 | + | - Video series and playlists | |
| 161 | + | - Large binary downloads up to 20GB per file (available now) | |
| 162 | + | - Per-file size increase available on request for files over 20GB | |
| 163 | + | ||
| 164 | + | **Video formats (planned):** MP4, MOV, AVI, MKV, WebM | |
| 165 | + | ||
| 166 | + | ### Storage | |
| 167 | + | ||
| 168 | + | - **500GB total** for primary content (video, large binaries, game builds) | |
| 169 | + | - **20GB per file** (increase available on request) | |
| 170 | + | - **10GB download budget** for general downloads (course slides, project files, supplementary materials) at 2GB per file | |
| 171 | + | ||
| 172 | + | ### Who This Is For | |
| 173 | + | ||
| 174 | + | - **Game developers** distributing builds directly to players | |
| 175 | + | - **Sample library developers** shipping large instrument installers | |
| 176 | + | - **YouTubers** wanting to escape ads and algorithm dependency | |
| 177 | + | - **Filmmakers** distributing work directly | |
| 178 | + | - **Educators** with video courses | |
| 179 | + | - **Video essayists** tired of platform demonetization | |
| 180 | + | - **Software developers** with large applications or datasets | |
| 181 | + | ||
| 182 | + | ### Compared to YouTube | |
| 183 | + | ||
| 184 | + | | | Makenot.work | YouTube | | |
| 185 | + | |---|---|---| | |
| 186 | + | | Monthly cost | $30 flat | Free | | |
| 187 | + | | Revenue share | 0% | 45% | | |
| 188 | + | | Ads on your content | Never | Mandatory | | |
| 189 | + | | Subscriber ownership | Yes | No | | |
| 190 | + | ||
| 191 | + | **The trade-off:** YouTube has discovery. We don't. Use YouTube to find your audience, then bring them here. | |
| 192 | + | ||
| 193 | + | ### Compared to itch.io | |
| 194 | + | ||
| 195 | + | | | Makenot.work | itch.io | | |
| 196 | + | |---|---|---| | |
| 197 | + | | Monthly cost | $30 flat | Free | | |
| 198 | + | | Revenue share | 0% | 0-100% (you choose, default 10%) | | |
| 199 | + | | Per-file limit | 20GB | 1GB default (raisable on request) | | |
| 200 | + | | Total storage | 500GB | No documented cap | | |
| 201 | + | | License keys | Built-in with activation API | Download keys only | | |
| 202 | + | | Versioned releases | Built-in with changelogs | Yes (butler CLI, delta patches) | | |
| 203 | + | | Promo codes | Built-in | Yes (coupon URLs) | | |
| 204 | + | | Web playable (HTML5) | No | Yes | | |
| 205 | + | | Game jam hosting | No | Yes | | |
| 206 | + | | Devlogs | No | Yes | | |
| 207 | + | | Community forums | No | Yes (per-project boards) | | |
| 208 | + | | Desktop app / auto-updates | No | Yes (itch app + butler delta patches) | | |
| 209 | + | | Embeddable widget | No | Yes | | |
| 210 | + | ||
| 211 | + | **The trade-off:** itch.io has a beloved ecosystem for games -- jams, devlogs, community boards, a desktop app with delta patching, and browser-playable HTML5 games. MNW has stronger commerce tools (license keys with activation tracking, subscriptions, analytics) and takes 0% vs itch.io's default 10%. If you're a game developer who also sells non-game software, MNW handles both; itch.io is games-first. | |
| 212 | + | ||
| 213 | + | ### Compared to Gumroad | |
| 214 | + | ||
| 215 | + | | | Makenot.work | Gumroad | | |
| 216 | + | |---|---|---| | |
| 217 | + | | Monthly cost | $30 flat | Free | | |
| 218 | + | | Revenue share | 0% | 10% + $0.50/txn | | |
| 219 | + | | Per-file limit | 20GB | 16GB | | |
| 220 | + | | Total storage | 500GB | Unlimited (stated) | | |
| 221 | + | | License keys | Built-in | Built-in (with verify API) | | |
| 222 | + | | Versioned releases | Built-in | File replace + notify | | |
| 223 | + | | Embeddable widget | No | Yes (overlay + inline) | | |
| 224 | + | | Affiliate program | No | Yes (per-product) | | |
| 225 | + | | Email workflows | No | Yes (drip sequences) | | |
| 226 | + | | Tax MoR | No | Yes (global since 2025) | | |
| 227 | + | | Marketplace discovery | No | Yes (Gumroad Discover) | | |
| 228 | + | ||
| 229 | + | --- | |
| 230 | + | ||
| 231 | + | ## Streaming — $40/month | |
| 232 | + | ||
| 233 | + | *Live streaming is not yet implemented. This describes the planned feature set.* | |
| 234 | + | ||
| 235 | + | For live streamers, broadcasters, and creators who need real-time video delivery. | |
| 236 | + | ||
| 237 | + | ### What You Get (in addition to Big Files) | |
| 238 | + | ||
| 239 | + | - RTMP ingest (works with OBS, Streamlabs, etc.) | |
| 240 | + | - Up to 1080p60 | |
| 241 | + | - Low-latency mode (~3-5 seconds) | |
| 242 | + | - Real-time chat | |
| 243 | + | - Subscriber-only streams | |
| 244 | + | - Stream scheduling and go-live notifications | |
| 245 | + | - Clip creation and VOD archives | |
| 246 | + | - 30-day automatic archive retention | |
| 247 | + | ||
| 248 | + | **Bitrate limit:** 8,000 kbps | |
| 249 | + | ||
| 250 | + | ### Storage | |
| 251 | + | ||
| 252 | + | - **500GB total** for primary content (same as Big Files) | |
| 253 | + | - **20GB per file** (same as Big Files, increase available on request) | |
| 254 | + | - **10GB download budget** (same as Big Files) | |
| 255 | + | - **VOD archives** retained for 30 days automatically, then deleted unless saved to primary storage | |
| 256 | + | ||
| 257 | + | ### Who This Is For | |
| 258 | + | ||
| 259 | + | - **Twitch streamers** tired of 50% revenue cuts | |
| 260 | + | - **YouTube streamers** wanting more control | |
| 261 | + | - **Musicians** doing live performances | |
| 262 | + | - **Educators** teaching live classes | |
| 263 | + | - **Game developers** streaming devlogs | |
| 264 | + | ||
| 265 | + | ### Compared to Twitch | |
| 266 | + | ||
| 267 | + | | | Makenot.work | Twitch | | |
| 268 | + | |---|---|---| | |
| 269 | + | | Monthly cost | $40 flat | Free | | |
| 270 | + | | Subscription split | 0% | 50% | | |
| 271 | + | | At $2,000/mo subs | $40 cost | $1,000 cost | | |
| 272 | + | | Ads | Never | Required | | |
| 273 | + | | Exclusivity | None | Partner restrictions | | |
| 274 | + | ||
| 275 | + | **The trade-off:** Twitch has millions of viewers browsing. We don't. Build your audience elsewhere, monetize it here. | |
| 276 | + | ||
| 277 | + | --- | |
| 278 | + | ||
| 279 | + | ## Software on Makenot.work | |
| 280 | + | ||
| 281 | + | Software creators (app developers, plugin makers, game studios) fit naturally into the tier system based on their file sizes: | |
| 282 | + | ||
| 283 | + | | What you make | Typical size | Recommended tier | | |
| 284 | + | |---------------|-------------|-----------------| | |
| 285 | + | | WordPress themes, small tools | Under 50MB | Basic ($10) via download budget | | |
| 286 | + | | VST3/CLAP plugins, presets | 50-500MB | Small Files ($20) | | |
| 287 | + | | Sample libraries with installers | 500MB-2GB | Big Files ($30) | | |
| 288 | + | | Games, large applications | 2-20GB | Big Files ($30) | | |
| 289 | + | | Very large games (20GB+) | 20GB+ | Big Files ($30) + size increase request | | |
| 290 | + | ||
| 291 | + | All tiers from Small Files up include versioned releases, changelogs, license keys with activation tracking, and promo codes. Pick the tier that fits your largest deliverable. | |
| 292 | + | ||
| 293 | + | --- | |
| 294 | + | ||
| 295 | + | ## Upgrading and Downgrading | |
| 296 | + | ||
| 297 | + | - **Upgrade** to a higher tier anytime. All existing content, subscribers, and settings carry over. You pay the new rate starting immediately (prorated for the current billing period). | |
| 298 | + | - **Downgrade** to a lower tier anytime. Existing files stay. New uploads are subject to the lower tier's limits. If you're over the new tier's storage cap, you can't upload new files until you're under the limit. | |
| 299 | + | - **Cancellation**: 30-day grace period. Uploads are disabled, but existing items remain accessible to past buyers. After 30 days, items become hidden. Resubscribe to restore everything. | |
| 300 | + | ||
| 301 | + | --- | |
| 302 | + | ||
| 303 | + | ## Getting Started | |
| 304 | + | ||
| 305 | + | 1. [Create your account](./01-getting-started.md) | |
| 306 | + | 2. [Set up your profile](./profile.md) | |
| 307 | + | 3. Share with your audience | |
| 308 | + | ||
| 309 | + | ## See Also | |
| 310 | + | ||
| 311 | + | - [Pricing Overview](./03-selling.md) | |
| 312 | + | - [Getting Started](./01-getting-started.md) |
| @@ -14,36 +14,57 @@ | |||
| 14 | 14 | <p class="doc-subtitle">Public documentation for Makenot.work.</p> | |
| 15 | 15 | <h2>About</h2> | |
| 16 | 16 | <ul> | |
| 17 | - | <li><a href="./guarantees.html">Service Level Agreement</a></li> | |
| 17 | + | <li><a href="./story.html">About Makenot.work</a></li> | |
| 18 | 18 | <li><a href="./how-we-work.html">How We Work</a></li> | |
| 19 | + | <li><a href="./guarantees.html">Service Level Agreement</a></li> | |
| 19 | 20 | <li><a href="./roadmap.html">Roadmap</a></li> | |
| 20 | - | <li><a href="./story.html">About Makenot.work</a></li> | |
| 21 | 21 | </ul> | |
| 22 | - | <h2>Guide</h2> | |
| 22 | + | <h2>Creator Guide</h2> | |
| 23 | + | <ul> | |
| 24 | + | <li><a href="./01-getting-started.html">Getting Started</a></li> | |
| 25 | + | <li><a href="./projects.html">Projects</a></li> | |
| 26 | + | <li><a href="./items.html">Items</a></li> | |
| 27 | + | <li><a href="./02-content.html">Content Types</a></li> | |
| 28 | + | <li><a href="./files.html">Uploading & Downloads</a></li> | |
| 29 | + | <li><a href="./tags.html">Tags</a></li> | |
| 30 | + | <li><a href="./03-selling.html">Selling & Monetization</a></li> | |
| 31 | + | <li><a href="./pricing.html">Pricing Tiers</a></li> | |
| 32 | + | <li><a href="./tiers.html">Tier Comparison & Break-Even</a></li> | |
| 33 | + | <li><a href="./migration.html">Platform Migration Guides</a></li> | |
| 34 | + | <li><a href="./profile.html">Profile & Customization</a></li> | |
| 35 | + | <li><a href="./analytics.html">Analytics</a></li> | |
| 36 | + | <li><a href="./payouts.html">Payouts</a></li> | |
| 37 | + | <li><a href="./rss.html">RSS Feeds</a></li> | |
| 38 | + | <li><a href="./contact-sharing.html">Contact Sharing</a></li> | |
| 39 | + | <li><a href="./metadata.html">Metadata & SEO</a></li> | |
| 40 | + | <li><a href="./best-practices.html">Best Practices</a></li> | |
| 41 | + | </ul> | |
| 42 | + | <h2>For Fans</h2> | |
| 43 | + | <ul> | |
| 44 | + | <li><a href="./fan-guide.html">Fan Guide</a></li> | |
| 45 | + | </ul> | |
| 46 | + | <h2>Developer</h2> | |
| 23 | 47 | <ul> | |
| 24 | - | <li><a href="./01-getting-started.html">Getting Started </a></li> | |
| 25 | - | <li><a href="./02-projects.html">Projects </a></li> | |
| 26 | - | <li><a href="./03-items.html">Items </a></li> | |
| 27 | - | <li><a href="./04-content.html">Content Types </a></li> | |
| 28 | - | <li><a href="./05-versions.html">Versions </a></li> | |
| 29 | - | <li><a href="./06-chapters.html">Chapters </a></li> | |
| 30 | - | <li><a href="./07-pricing.html">Pricing & Monetization </a></li> | |
| 31 | - | <li><a href="./08-audience.html">Audience & Communication </a></li> | |
| 32 | - | <li><a href="./09-analytics.html">Analytics & Data </a></li> | |
| 33 | - | <li><a href="./10-security.html">Security </a></li> | |
| 34 | - | <li><a href="./11-profile.html">Profile & Customization </a></li> | |
| 35 | - | <li><a href="./12-for-fans.html">For Fans </a></li> | |
| 48 | + | <li><a href="./api-overview.html">API Overview</a></li> | |
| 49 | + | <li><a href="./synckit.html">SyncKit Cloud Sync</a></li> | |
| 50 | + | <li><a href="./ota.html">OTA Updates</a></li> | |
| 51 | + | <li><a href="./oauth.html">OAuth 2.0</a></li> | |
| 52 | + | <li><a href="./license-keys.html">License Key API</a></li> | |
| 36 | 53 | </ul> | |
| 37 | 54 | <h2>Legal</h2> | |
| 38 | 55 | <ul> | |
| 39 | - | <li><a href="./acceptable-use.html">Acceptable Use Policy</a></li> | |
| 40 | - | <li><a href="./privacy-policy.html">Privacy Policy</a></li> | |
| 41 | 56 | <li><a href="./terms-of-service.html">Terms of Service</a></li> | |
| 57 | + | <li><a href="./privacy-policy.html">Privacy Policy</a></li> | |
| 58 | + | <li><a href="./acceptable-use.html">Acceptable Use Policy</a></li> | |
| 59 | + | <li><a href="./moderation.html">Content Moderation</a></li> | |
| 60 | + | <li><a href="./appeals.html">Appeal Process</a></li> | |
| 42 | 61 | </ul> | |
| 43 | 62 | <h2>Support</h2> | |
| 44 | 63 | <ul> | |
| 45 | - | <li><a href="./contact.html">Contact</a></li> | |
| 46 | 64 | <li><a href="./faq.html">FAQ</a></li> | |
| 65 | + | <li><a href="./contact.html">Contact</a></li> | |
| 66 | + | <li><a href="./forums.html">Community Forums</a></li> | |
| 67 | + | <li><a href="./code-of-conduct.html">Code of Conduct</a></li> | |
| 47 | 68 | </ul> | |
| 48 | 69 | </article> | |
| 49 | 70 | </body> |
| @@ -94,9 +94,9 @@ Violations result in action proportional to severity: | |||
| 94 | 94 | - Repeated or moderate issues: Content removal, temporary restrictions | |
| 95 | 95 | - Serious or repeated violations: Account suspension or termination | |
| 96 | 96 | ||
| 97 | - | See [Content Moderation & Enforcement](../../unpublished/legal/moderation.md) for details. | |
| 97 | + | See [Content Moderation & Enforcement](./moderation.md) for details. | |
| 98 | 98 | ||
| 99 | - | You can appeal any action. See [Appeal Process](../../unpublished/legal/appeals.md). | |
| 99 | + | You can appeal any action. See [Appeal Process](./appeals.md). | |
| 100 | 100 | ||
| 101 | 101 | --- | |
| 102 | 102 | ||
| @@ -120,6 +120,6 @@ We review all reports. We don't disclose reporter identities to the reported use | |||
| 120 | 120 | - [Terms of Service](./terms-of-service.md) — Full legal terms | |
| 121 | 121 | - [Privacy Policy](./privacy-policy.md) — Data collection and handling | |
| 122 | 122 | - [FAQ](../support/faq.md) — Content policy questions | |
| 123 | - | - [Content Moderation](../../unpublished/legal/moderation.md) — How we approach moderation | |
| 124 | - | - [Content Moderation & Enforcement](../../unpublished/legal/moderation.md) — What happens when rules are broken | |
| 125 | - | - [Appeal Process](../../unpublished/legal/appeals.md) — Disputing decisions | |
| 123 | + | - [Content Moderation](./moderation.md) — How we approach moderation | |
| 124 | + | - [Content Moderation & Enforcement](./moderation.md) — What happens when rules are broken | |
| 125 | + | - [Appeal Process](./appeals.md) — Disputing decisions |
| @@ -0,0 +1,111 @@ | |||
| 1 | + | # Appeal Process | |
| 2 | + | ||
| 3 | + | How to dispute moderation decisions. | |
| 4 | + | ||
| 5 | + | --- | |
| 6 | + | ||
| 7 | + | ## What Can Be Appealed | |
| 8 | + | ||
| 9 | + | You can appeal: | |
| 10 | + | ||
| 11 | + | - Content removal | |
| 12 | + | - Account warnings | |
| 13 | + | - Account suspension | |
| 14 | + | - Account termination | |
| 15 | + | - Any other moderation action | |
| 16 | + | ||
| 17 | + | --- | |
| 18 | + | ||
| 19 | + | ## How to Appeal | |
| 20 | + | ||
| 21 | + | Send an email to: | |
| 22 | + | ||
| 23 | + | **appeals@makenot.work** | |
| 24 | + | ||
| 25 | + | Include: | |
| 26 | + | ||
| 27 | + | 1. **Your username** or account email | |
| 28 | + | 2. **What action you're appealing** - What was removed or what restriction was applied | |
| 29 | + | 3. **Why you believe the decision was wrong** - Be specific | |
| 30 | + | 4. **Any relevant context** - Information we might have missed | |
| 31 | + | ||
| 32 | + | --- | |
| 33 | + | ||
| 34 | + | ## What Happens Next | |
| 35 | + | ||
| 36 | + | 1. **Acknowledgment** - We confirm receipt (usually within 24 hours) | |
| 37 | + | ||
| 38 | + | 2. **Review** - Someone other than the original decision-maker reviews your appeal | |
| 39 | + | ||
| 40 | + | 3. **Decision** - We notify you of the outcome with explanation | |
| 41 | + | ||
| 42 | + | 4. **If overturned** - Content is restored or restrictions are lifted | |
| 43 | + | ||
| 44 | + | 5. **If upheld** - We explain why and what options remain | |
| 45 | + | ||
| 46 | + | --- | |
| 47 | + | ||
| 48 | + | ## Timeline | |
| 49 | + | ||
| 50 | + | We aim to resolve appeals within: | |
| 51 | + | ||
| 52 | + | - **Content removal:** 3-5 business days | |
| 53 | + | - **Account warnings:** 3-5 business days | |
| 54 | + | - **Account suspension:** 5-10 business days | |
| 55 | + | - **Account termination:** 10-14 business days | |
| 56 | + | ||
| 57 | + | Complex cases may take longer. We'll keep you informed of delays. | |
| 58 | + | ||
| 59 | + | --- | |
| 60 | + | ||
| 61 | + | ## What We Consider | |
| 62 | + | ||
| 63 | + | When reviewing appeals, we look at: | |
| 64 | + | ||
| 65 | + | - **Original context** - Was important context missed? | |
| 66 | + | - **Policy interpretation** - Was the policy applied correctly? | |
| 67 | + | - **Consistency** - Is this consistent with how we've handled similar cases? | |
| 68 | + | - **New information** - Did you provide information that changes the picture? | |
| 69 | + | ||
| 70 | + | --- | |
| 71 | + | ||
| 72 | + | ## Data Access During Appeals | |
| 73 | + | ||
| 74 | + | Even if your account is suspended or terminated: | |
| 75 | + | ||
| 76 | + | - You can request a data export | |
| 77 | + | - We preserve your data during the appeal window | |
| 78 | + | - Access to export is maintained | |
| 79 | + | ||
| 80 | + | We don't hold your data hostage. | |
| 81 | + | ||
| 82 | + | --- | |
| 83 | + | ||
| 84 | + | ## Limits | |
| 85 | + | ||
| 86 | + | Some decisions cannot be appealed: | |
| 87 | + | ||
| 88 | + | - **Legal requirements** - If we're legally required to remove content or terminate an account | |
| 89 | + | - **Imminent harm** - If we believe content poses immediate danger to someone | |
| 90 | + | - **CSAM** - Content involving minors is not subject to appeal | |
| 91 | + | ||
| 92 | + | For these cases, we'll explain why the decision is final. | |
| 93 | + | ||
| 94 | + | --- | |
| 95 | + | ||
| 96 | + | ## If You Disagree with the Appeal Outcome | |
| 97 | + | ||
| 98 | + | If your appeal is denied and you believe we made an error: | |
| 99 | + | ||
| 100 | + | - You can request a second review (once per decision) | |
| 101 | + | - Provide new information or arguments we didn't consider | |
| 102 | + | - We'll have a different reviewer examine the case | |
| 103 | + | ||
| 104 | + | Beyond that, our decision is final. We're a small team and can't endlessly re-litigate decisions. | |
| 105 | + | ||
| 106 | + | --- | |
| 107 | + | ||
| 108 | + | ## See Also | |
| 109 | + | ||
| 110 | + | - [Content Moderation](./moderation.md) — How we make decisions | |
| 111 | + | - [Acceptable Use Policy](./acceptable-use.md) — What we enforce |
| @@ -0,0 +1,194 @@ | |||
| 1 | + | # Content Moderation & Enforcement | |
| 2 | + | ||
| 3 | + | How we balance creative freedom with maintaining a platform free from harassment, and what happens when accounts violate our policies. | |
| 4 | + | ||
| 5 | + | --- | |
| 6 | + | ||
| 7 | + | ## Our Approach | |
| 8 | + | ||
| 9 | + | We have a dual mandate: supporting creative expression while ensuring Makenot.work remains a space free from harassment and harm. | |
| 10 | + | ||
| 11 | + | We enforce policies against harassment, illegal content, and fraud to keep the platform safe for creators and fans. We reserve the right to refuse service to creators whose conduct harms this environment. | |
| 12 | + | ||
| 13 | + | **Context and intent matter.** We evaluate work as a whole, not isolated elements. Satire, critique, historical documentation, and artistic exploration are considered in full context. | |
| 14 | + | ||
| 15 | + | --- | |
| 16 | + | ||
| 17 | + | ## What's Not Allowed | |
| 18 | + | ||
| 19 | + | ### Harmful Content | |
| 20 | + | ||
| 21 | + | - **Dehumanization, harassment, and incitement to violence** — Content that attacks, degrades, or encourages violence toward individuals or groups | |
| 22 | + | - **Stricter review for content targeting marginalized groups** — Content that would be tolerable if directed at dominant groups may be removed when it targets communities facing systemic discrimination | |
| 23 | + | - **Doxxing** — Sharing private information (addresses, phone numbers, workplaces, etc.) without consent | |
| 24 | + | ||
| 25 | + | ### Platform Integrity | |
| 26 | + | ||
| 27 | + | - **Spam and fraud** — Deceptive practices, scams, or manipulative content | |
| 28 | + | - **Impersonation** — Misrepresenting identity to deceive others | |
| 29 | + | - **Illegal content** — Content that violates applicable law | |
| 30 | + | ||
| 31 | + | ### Content Type Restrictions | |
| 32 | + | ||
| 33 | + | - **Adult/NSFW content** — Not permitted on Makenot.work. We recognize our infrastructure suits adult content and plan to create a separate platform for adult creators with identical commitments—but the SFW platform comes first | |
| 34 | + | - **Unqualified promotion** — Creators promoting health products, financial services, or similar without appropriate qualification may be subject to scrutiny or moderation | |
| 35 | + | ||
| 36 | + | --- | |
| 37 | + | ||
| 38 | + | ## On Marginalized Groups | |
| 39 | + | ||
| 40 | + | We apply stricter review when content targets groups that face systemic discrimination or historical oppression. Content that would be tolerable if directed at dominant groups may be removed when targeting marginalized communities. | |
| 41 | + | ||
| 42 | + | **Why this exists:** Harmful content targeting marginalized groups contributes to real-world discrimination and violence. Identical rules applied without context produce unequal effects. Protecting marginalized groups from targeted harassment protects the platform's ability to welcome diverse creators. | |
| 43 | + | ||
| 44 | + | --- | |
| 45 | + | ||
| 46 | + | ## Enforcement Actions | |
| 47 | + | ||
| 48 | + | We enforce proportionally. Minor issues get warnings. Serious or repeated issues get serious responses. | |
| 49 | + | ||
| 50 | + | ### Warning | |
| 51 | + | ||
| 52 | + | For minor or first-time issues: | |
| 53 | + | ||
| 54 | + | - We explain what policy was violated | |
| 55 | + | - We explain what needs to change | |
| 56 | + | - No immediate account restrictions | |
| 57 | + | - Warning is noted on your account history | |
| 58 | + | ||
| 59 | + | Most issues end here. People make mistakes. | |
| 60 | + | ||
| 61 | + | ### Content Removal | |
| 62 | + | ||
| 63 | + | For content that violates policy: | |
| 64 | + | ||
| 65 | + | - Specific content is removed or hidden | |
| 66 | + | - You're notified with explanation | |
| 67 | + | - Account remains active | |
| 68 | + | - You can appeal the decision | |
| 69 | + | ||
| 70 | + | ### Temporary Suspension | |
| 71 | + | ||
| 72 | + | For moderate violations or patterns of minor violations: | |
| 73 | + | ||
| 74 | + | - Account access is restricted | |
| 75 | + | - Duration depends on severity (typically 1-30 days) | |
| 76 | + | - You can still export your data | |
| 77 | + | - You can still appeal | |
| 78 | + | ||
| 79 | + | During suspension: | |
| 80 | + | - Your content remains but is hidden from fans | |
| 81 | + | - Subscriptions are paused (fans aren't charged) | |
| 82 | + | - You can't upload or modify content | |
| 83 | + | ||
| 84 | + | ### Permanent Termination | |
| 85 | + | ||
| 86 | + | For serious violations or repeated moderate violations: | |
| 87 | + | ||
| 88 | + | - Account is permanently closed | |
| 89 | + | - Content is removed from public access | |
| 90 | + | - You have 30 days to export your data | |
| 91 | + | - Future accounts will also be terminated | |
| 92 | + | ||
| 93 | + | --- | |
| 94 | + | ||
| 95 | + | ## What Leads to Each Level | |
| 96 | + | ||
| 97 | + | ### Usually a Warning | |
| 98 | + | ||
| 99 | + | - Borderline content that could be interpreted as policy-violating | |
| 100 | + | - Technical policy violations (wrong format, metadata issues) | |
| 101 | + | - Minor behavioral issues (heated argument, single incident) | |
| 102 | + | ||
| 103 | + | ### Usually Content Removal | |
| 104 | + | ||
| 105 | + | - Clear policy violations in specific content | |
| 106 | + | - Content that received valid complaints and can't be defended | |
| 107 | + | ||
| 108 | + | ### Usually Suspension | |
| 109 | + | ||
| 110 | + | - Multiple warnings without behavior change | |
| 111 | + | - Moderate harassment or abuse | |
| 112 | + | - Repeated content violations | |
| 113 | + | - Deceptive practices | |
| 114 | + | ||
| 115 | + | ### Usually Termination | |
| 116 | + | ||
| 117 | + | - Severe harassment, threats, or doxxing | |
| 118 | + | - CSAM or content sexualizing minors | |
| 119 | + | - Large-scale fraud or scams | |
| 120 | + | - Ban evasion | |
| 121 | + | - Illegal activity | |
| 122 | + | - Three strikes (repeated moderate violations) | |
| 123 | + | ||
| 124 | + | --- | |
| 125 | + | ||
| 126 | + | ## Immediate Termination | |
| 127 | + | ||
| 128 | + | Some violations skip the escalation ladder: | |
| 129 | + | ||
| 130 | + | - **CSAM** - Immediate termination, law enforcement referral | |
| 131 | + | - **Credible threats of violence** - Immediate termination | |
| 132 | + | - **Illegal content** - Immediate termination | |
| 133 | + | - **Large-scale fraud** - Immediate termination | |
| 134 | + | ||
| 135 | + | For these, there's no warning period. The severity justifies immediate action. | |
| 136 | + | ||
| 137 | + | --- | |
| 138 | + | ||
| 139 | + | ## Proactive Measures | |
| 140 | + | ||
| 141 | + | Automated scanning for clearly illegal content (CSAM, terrorism recruitment). Otherwise, we operate reactively based on reports. | |
| 142 | + | ||
| 143 | + | We investigate reported content with attention to context. Not every complaint results in action — false reports are common and sometimes coordinated. | |
| 144 | + | ||
| 145 | + | --- | |
| 146 | + | ||
| 147 | + | ## Data Handling for Terminated Accounts | |
| 148 | + | ||
| 149 | + | When your account is terminated: | |
| 150 | + | ||
| 151 | + | - **30-day export window** - You can download your content and data | |
| 152 | + | - **Content removed from public access** - Fans can't access it | |
| 153 | + | - **Account data retained** - We keep records to prevent ban evasion | |
| 154 | + | - **Financial records retained** - As required by law and payment processors | |
| 155 | + | ||
| 156 | + | After 90 days, content is deleted. Account records are retained for 2 years. | |
| 157 | + | ||
| 158 | + | --- | |
| 159 | + | ||
| 160 | + | ## Ban Evasion | |
| 161 | + | ||
| 162 | + | Creating new accounts after termination: | |
| 163 | + | ||
| 164 | + | - New accounts will be terminated when detected | |
| 165 | + | - Evasion may extend data retention periods | |
| 166 | + | - Repeated evasion may result in legal action | |
| 167 | + | ||
| 168 | + | --- | |
| 169 | + | ||
| 170 | + | ## Appeals | |
| 171 | + | ||
| 172 | + | Per our [Creator Guarantees](../about/guarantees.md): | |
| 173 | + | ||
| 174 | + | - Clear explanation of what policy was violated | |
| 175 | + | - Opportunity to appeal | |
| 176 | + | - Access to export your data even if suspended | |
| 177 | + | ||
| 178 | + | You can appeal any enforcement action. See [Appeal Process](./appeals.md). Even terminated accounts can file appeals. We've overturned terminations when we got it wrong. | |
| 179 | + | ||
| 180 | + | --- | |
| 181 | + | ||
| 182 | + | ## Transparency | |
| 183 | + | ||
| 184 | + | - **Public moderation code** — Our moderation tools are in the public source code (excluding anti-spam measures that would be defeated by disclosure) | |
| 185 | + | - **Versioned policies** — This document is version-controlled; view history in our repository | |
| 186 | + | - **Major decisions documented** — Significant moderation decisions and policy changes are documented on The Changelog | |
| 187 | + | ||
| 188 | + | --- | |
| 189 | + | ||
| 190 | + | ## See Also | |
| 191 | + | ||
| 192 | + | - [Creator Guarantees](../about/guarantees.md) | |
| 193 | + | - [Appeal Process](./appeals.md) | |
| 194 | + | - [Acceptable Use Policy](./acceptable-use.md) |
| @@ -42,7 +42,7 @@ Don't upload: | |||
| 42 | 42 | - Malware or harmful code | |
| 43 | 43 | ||
| 44 | 44 | ### Content Moderation | |
| 45 | - | We remove content that violates these terms. We try to give notice before removal when possible. See [Content Moderation](../../unpublished/legal/moderation.md) for details. | |
| 45 | + | We remove content that violates these terms. We try to give notice before removal when possible. See [Content Moderation](./moderation.md) for details. | |
| 46 | 46 | ||
| 47 | 47 | ## Fan Terms | |
| 48 | 48 |
| @@ -25,9 +25,8 @@ DMCA takedown notices should be sent to: | |||
| 25 | 25 | **Mail:** | |
| 26 | 26 | Make Creative, LLC | |
| 27 | 27 | ATTN: DMCA Agent | |
| 28 | - | [Address to be registered with Copyright Office] | |
| 29 | - | ||
| 30 | - | We will register our designated agent with the U.S. Copyright Office. Until registration is complete, email is the fastest way to reach us. | |
| 28 | + | 2055 Main St, Apt 502 | |
| 29 | + | Irvine, CA 92614 | |
| 31 | 30 | ||
| 32 | 31 | --- | |
| 33 | 32 |
| @@ -53,19 +53,27 @@ If someone violates our policies, report it. But business disputes, creative dis | |||
| 53 | 53 | ||
| 54 | 54 | ## Dispute Resolution | |
| 55 | 55 | ||
| 56 | - | *[This section requires legal review to determine: arbitration vs. litigation, class action waiver, governing law, jurisdiction]* | |
| 56 | + | *Note: This section is pending formal legal review.* | |
| 57 | 57 | ||
| 58 | 58 | ### Governing Law | |
| 59 | 59 | ||
| 60 | - | This agreement is governed by the laws of Colorado, without regard to conflict of law principles. | |
| 60 | + | This agreement is governed by the laws of the State of Colorado, without regard to conflict of law principles. | |
| 61 | 61 | ||
| 62 | 62 | ### Informal Resolution | |
| 63 | 63 | ||
| 64 | - | Before filing any claim, contact us at legal@makenot.work. Many disputes can be resolved through conversation. | |
| 64 | + | Before filing any formal claim, you agree to contact us at legal@makenot.work and attempt to resolve the dispute informally for at least thirty (30) days. | |
| 65 | 65 | ||
| 66 | - | ### Formal Disputes | |
| 66 | + | ### Binding Arbitration | |
| 67 | 67 | ||
| 68 | - | *[Pending legal review: arbitration clause, small claims exception, class action waiver]* | |
| 68 | + | Any dispute arising from or relating to this agreement or your use of the platform that cannot be resolved informally shall be resolved by binding arbitration administered by the American Arbitration Association (AAA) under its Commercial Arbitration Rules. The arbitration shall be conducted in the State of Colorado. The arbitrator's decision shall be final and binding and may be entered as a judgment in any court of competent jurisdiction. | |
| 69 | + | ||
| 70 | + | ### Class Action Waiver | |
| 71 | + | ||
| 72 | + | You agree that any disputes will be resolved on an individual basis. You waive any right to participate in a class action, class arbitration, or representative proceeding. | |
| 73 | + | ||
| 74 | + | ### Small Claims Exception | |
| 75 | + | ||
| 76 | + | Either party may bring an individual action in small claims court in the State of Colorado if the claim qualifies. | |
| 69 | 77 | ||
| 70 | 78 | --- | |
| 71 | 79 |
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | [package] | |
| 2 | 2 | name = "makenotwork" | |
| 3 | - | version = "0.3.10" | |
| 3 | + | version = "0.3.11" | |
| 4 | 4 | edition = "2024" | |
| 5 | 5 | license-file = "../../LICENSE" | |
| 6 | 6 |
| @@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | 3 | ## How to Test | |
| 4 | 4 | ||
| 5 | - | - Automated tests cover units and integration (97+ passing) but can't catch visual bugs, broken flows, or UX issues | |
| 5 | + | - Automated tests cover units and integration (1,060+ passing) but can't catch visual bugs, broken flows, or UX issues | |
| 6 | 6 | - Work through each section sequentially, checking boxes as you go | |
| 7 | 7 | - If something fails, note the issue inline and keep going — don't block the whole run | |
| 8 | 8 | - Prioritized: P0 first (launch-blocking), then P1 (core features), then P2 (edge cases) | |
| @@ -89,7 +89,7 @@ | |||
| 89 | 89 | - [ ] Set item visibility to private — disappears from `/discover` | |
| 90 | 90 | - [ ] Create paid item (set price > 0) | |
| 91 | 91 | ||
| 92 | - | ### Purchase Flow | |
| 92 | + | ### Purchase Flow (Fixed Price) | |
| 93 | 93 | ||
| 94 | 94 | - [ ] As buyer (different account), browse `/discover` — find the paid item | |
| 95 | 95 | - [ ] `GET /purchase/{item_id}` — purchase page shows price and fee breakdown | |
| @@ -101,6 +101,43 @@ | |||
| 101 | 101 | - [ ] Buyer can access item content (stream audio, read text, download file) | |
| 102 | 102 | - [ ] Cancel checkout — `/stripe/cancel` renders, no transaction created | |
| 103 | 103 | ||
| 104 | + | ### Pay-What-You-Want (PWYW) Purchase | |
| 105 | + | ||
| 106 | + | - [ ] Create PWYW item with $0 minimum — save succeeds | |
| 107 | + | - [ ] Purchase page shows PWYW input with suggested prices | |
| 108 | + | - [ ] Complete purchase at $0 — item added to library, no Stripe checkout | |
| 109 | + | - [ ] Complete purchase at custom amount (e.g. $5) — Stripe Checkout, item in library | |
| 110 | + | - [ ] Create PWYW item with non-zero minimum (e.g. $5) | |
| 111 | + | - [ ] Attempt purchase below minimum — rejected | |
| 112 | + | ||
| 113 | + | ### Subscription Flow | |
| 114 | + | ||
| 115 | + | - [ ] Create subscription tier on a project (e.g. $3/mo) | |
| 116 | + | - [ ] As buyer, subscription page renders with tier details | |
| 117 | + | - [ ] `POST /stripe/subscribe/{project_id}` — redirects to Stripe Checkout (subscription mode) | |
| 118 | + | - [ ] Complete subscription with test card | |
| 119 | + | - [ ] Webhook fires (`customer.subscription.created`) — subscription recorded | |
| 120 | + | - [ ] Subscriber can access subscriber-only items | |
| 121 | + | - [ ] Non-subscriber cannot access subscriber-only content | |
| 122 | + | - [ ] Cancel subscription — access continues until end of billing period | |
| 123 | + | ||
| 124 | + | ### Discount Codes | |
| 125 | + | ||
| 126 | + | - [ ] Create discount code (e.g. LAUNCH50, 50% off, limited uses) | |
| 127 | + | - [ ] Apply code at checkout — price reduced correctly | |
| 128 | + | - [ ] Discount shows in fee breakdown | |
| 129 | + | - [ ] Exhausted code rejected (after max uses reached) | |
| 130 | + | - [ ] Expired code rejected | |
| 131 | + | ||
| 132 | + | ### License Keys | |
| 133 | + | ||
| 134 | + | - [ ] Create item with license keys enabled | |
| 135 | + | - [ ] After purchase, license key displayed to buyer | |
| 136 | + | - [ ] `POST /api/licenses/{key}/activate` — activation succeeds | |
| 137 | + | - [ ] Activation count increments | |
| 138 | + | - [ ] `GET /api/licenses/{key}/verify` — returns valid status | |
| 139 | + | - [ ] Exceed activation limit — activation rejected | |
| 140 | + | ||
| 104 | 141 | ### Free Item Claim | |
| 105 | 142 | ||
| 106 | 143 | - [ ] As buyer, find a free item on `/discover` | |
| @@ -191,6 +228,38 @@ | |||
| 191 | 228 | - [ ] `/p/{slug}/rss` — valid RSS 2.0, includes project's public items | |
| 192 | 229 | - [ ] Feed updates when new item published | |
| 193 | 230 | ||
| 231 | + | ### Blog Posts | |
| 232 | + | ||
| 233 | + | - [ ] Create blog post on a project — title, slug, body (markdown) | |
| 234 | + | - [ ] Blog post renders at `/p/{slug}/blog/{post_slug}` | |
| 235 | + | - [ ] Blog post appears in project RSS feed | |
| 236 | + | - [ ] Edit blog post — changes saved and visible | |
| 237 | + | - [ ] Delete blog post — removed from project page and RSS | |
| 238 | + | ||
| 239 | + | ### Two-Factor Authentication | |
| 240 | + | ||
| 241 | + | - [ ] Enable TOTP 2FA — QR code and secret displayed | |
| 242 | + | - [ ] Login with 2FA enabled — prompted for TOTP code after password | |
| 243 | + | - [ ] Correct TOTP code — login succeeds | |
| 244 | + | - [ ] Wrong TOTP code — login rejected | |
| 245 | + | - [ ] Backup codes — one works, same code cannot be reused | |
| 246 | + | - [ ] Disable 2FA — login no longer prompts for code | |
| 247 | + | ||
| 248 | + | ### Passkeys (WebAuthn) | |
| 249 | + | ||
| 250 | + | - [ ] Register passkey from dashboard security section | |
| 251 | + | - [ ] Login with passkey — bypasses password | |
| 252 | + | - [ ] Remove passkey — can no longer use it to login | |
| 253 | + | ||
| 254 | + | ### Git Browser | |
| 255 | + | ||
| 256 | + | - [ ] `/git/{username}/{repo}` — file tree renders | |
| 257 | + | - [ ] Click file — blob view with syntax highlighting | |
| 258 | + | - [ ] `/git/{username}/{repo}/commits` — commit log renders | |
| 259 | + | - [ ] Click commit — diff view renders | |
| 260 | + | - [ ] `/git/{username}/{repo}/blame/{path}` — blame view renders | |
| 261 | + | - [ ] Clone URL displayed and correct (`ssh.makenot.work`) | |
| 262 | + | ||
| 194 | 263 | ### Data Export | |
| 195 | 264 | ||
| 196 | 265 | - [ ] Projects export (`POST /api/export/projects`) — downloads JSON | |
| @@ -257,7 +326,7 @@ | |||
| 257 | 326 | ### DNS + HTTPS | |
| 258 | 327 | ||
| 259 | 328 | - [ ] A record points to server IP (`dig makenot.work`) | |
| 260 | - | - [ ] HTTPS certificate valid and auto-renewing (Caddy + Let's Encrypt) | |
| 329 | + | - [ ] HTTPS certificate valid (Cloudflare Origin CA, 15yr wildcard) | |
| 261 | 330 | - [ ] `Strict-Transport-Security` header present | |
| 262 | 331 | - [ ] `http://makenot.work` redirects to `https://makenot.work` | |
| 263 | 332 | - [ ] `www.makenot.work` redirects to `makenot.work` (if configured) | |
| @@ -281,7 +350,7 @@ | |||
| 281 | 350 | ||
| 282 | 351 | ### Database | |
| 283 | 352 | ||
| 284 | - | - [ ] Migrations applied: all 21 migrations (`cargo sqlx migrate info` or check schema) | |
| 353 | + | - [ ] Migrations applied: all 45 migrations (`cargo sqlx migrate info` or check schema) | |
| 285 | 354 | - [ ] Connection healthy: `GET /health` shows database green | |
| 286 | 355 | - [ ] Demo seed data removed (migrations 011-014, 016-017 are seed data — verify no test users/items in production) | |
| 287 | 356 | - [ ] pg_trgm extension installed (required for search) | |
| @@ -322,8 +391,10 @@ | |||
| 322 | 391 | ||
| 323 | 392 | ### Firewall | |
| 324 | 393 | ||
| 325 | - | - [ ] Only ports 80, 443, 22 open: `ufw status` | |
| 394 | + | - [ ] Ports 80, 443 open to all (required for custom domains + on-demand TLS): `ufw status` | |
| 395 | + | - [ ] Port 22 open (SSH) | |
| 326 | 396 | - [ ] All other ports blocked | |
| 397 | + | - [ ] makenot.work protected by Cloudflare mTLS even with open ports | |
| 327 | 398 | ||
| 328 | 399 | --- | |
| 329 | 400 |
| @@ -0,0 +1,15 @@ | |||
| 1 | + | -- Add features column: array of platform capability strings | |
| 2 | + | ALTER TABLE projects ADD COLUMN features TEXT[] NOT NULL DEFAULT '{}'; | |
| 3 | + | ||
| 4 | + | -- Backfill from project_type (no production data, but covers test/dev DBs) | |
| 5 | + | UPDATE projects SET features = CASE project_type | |
| 6 | + | WHEN 'music' THEN ARRAY['audio', 'blog'] | |
| 7 | + | WHEN 'podcast' THEN ARRAY['audio', 'blog'] | |
| 8 | + | WHEN 'blog' THEN ARRAY['text', 'blog'] | |
| 9 | + | WHEN 'book' THEN ARRAY['text'] | |
| 10 | + | WHEN 'course' THEN ARRAY['downloads', 'text'] | |
| 11 | + | WHEN 'software' THEN ARRAY['downloads', 'blog'] | |
| 12 | + | WHEN 'art' THEN ARRAY['downloads'] | |
| 13 | + | WHEN 'writing' THEN ARRAY['text', 'blog'] | |
| 14 | + | ELSE ARRAY['downloads'] | |
| 15 | + | END; |
| @@ -287,7 +287,7 @@ impl_str_enum!(DiscoverSort { | |||
| 287 | 287 | ||
| 288 | 288 | // ── Items ── | |
| 289 | 289 | ||
| 290 | - | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] | |
| 290 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] | |
| 291 | 291 | #[serde(rename_all = "lowercase")] | |
| 292 | 292 | pub enum ItemType { | |
| 293 | 293 | Audio, | |
| @@ -459,6 +459,147 @@ impl CreatorTier { | |||
| 459 | 459 | } | |
| 460 | 460 | } | |
| 461 | 461 | ||
| 462 | + | // ── Project Features ── | |
| 463 | + | ||
| 464 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] | |
| 465 | + | #[serde(rename_all = "snake_case")] | |
| 466 | + | pub enum ProjectFeature { | |
| 467 | + | Audio, | |
| 468 | + | Downloads, | |
| 469 | + | Text, | |
| 470 | + | Blog, | |
| 471 | + | Subscriptions, | |
| 472 | + | LicenseKeys, | |
| 473 | + | SourceCode, | |
| 474 | + | } | |
| 475 | + | ||
| 476 | + | impl_str_enum!(ProjectFeature { | |
| 477 | + | Audio => "audio", | |
| 478 | + | Downloads => "downloads", | |
| 479 | + | Text => "text", | |
| 480 | + | Blog => "blog", | |
| 481 | + | Subscriptions => "subscriptions", | |
| 482 | + | LicenseKeys => "license_keys", | |
| 483 | + | SourceCode => "source_code", | |
| 484 | + | }); | |
| 485 | + | ||
| 486 | + | impl ProjectFeature { | |
| 487 | + | /// Human-readable label for display. | |
| 488 | + | pub fn label(&self) -> &'static str { | |
| 489 | + | match self { | |
| 490 | + | Self::Audio => "Audio", | |
| 491 | + | Self::Downloads => "Downloads", | |
| 492 | + | Self::Text => "Text", | |
| 493 | + | Self::Blog => "Blog", | |
| 494 | + | Self::Subscriptions => "Subscriptions", | |
| 495 | + | Self::LicenseKeys => "License Keys", | |
| 496 | + | Self::SourceCode => "Source Code", | |
| 497 | + | } | |
| 498 | + | } | |
| 499 | + | ||
| 500 | + | /// One-line description of what this feature enables. | |
| 501 | + | pub fn description(&self) -> &'static str { | |
| 502 | + | match self { | |
| 503 | + | Self::Audio => "Upload and stream audio files. Player with chapters.", | |
| 504 | + | Self::Downloads => "Host file downloads with versioned releases.", | |
| 505 | + | Self::Text => "Write and publish text content with markdown.", | |
| 506 | + | Self::Blog => "Project blog with RSS feed.", | |
| 507 | + | Self::Subscriptions => "Monthly subscriber tiers.", | |
| 508 | + | Self::LicenseKeys => "Software license management with activation API.", | |
| 509 | + | Self::SourceCode => "Git repository with source browser.", | |
| 510 | + | } | |
| 511 | + | } | |
| 512 | + | ||
| 513 | + | /// All features as (value, label, description) tuples for form rendering. | |
| 514 | + | pub fn all() -> &'static [(&'static str, &'static str, &'static str)] { | |
| 515 | + | &[ | |
| 516 | + | ("audio", "Audio", "Upload and stream audio files. Player with chapters."), | |
| 517 | + | ("downloads", "Downloads", "Host file downloads with versioned releases."), | |
| 518 | + | ("text", "Text", "Write and publish text content with markdown."), | |
| 519 | + | ("blog", "Blog", "Project blog with RSS feed."), | |
| 520 | + | ("subscriptions", "Subscriptions", "Monthly subscriber tiers."), | |
| 521 | + | ("license_keys", "License Keys", "Software license management with activation API."), | |
| 522 | + | ("source_code", "Source Code", "Git repository with source browser."), | |
| 523 | + | ] | |
| 524 | + | } | |
| 525 | + | ||
| 526 | + | /// Derive the best-fit project type from a set of features. | |
| 527 | + | pub fn derive_project_type(features: &[String]) -> ProjectType { | |
| 528 | + | if features.iter().any(|f| f == "audio") { | |
| 529 | + | return ProjectType::Music; | |
| 530 | + | } | |
| 531 | + | if features.iter().any(|f| f == "text") && !features.iter().any(|f| f == "downloads") { | |
| 532 | + | return ProjectType::Blog; | |
| 533 | + | } | |
| 534 | + | if features.iter().any(|f| f == "downloads") { | |
| 535 | + | return ProjectType::Software; | |
| 536 | + | } | |
| 537 | + | ProjectType::General | |
| 538 | + | } | |
| 539 | + | ||
| 540 | + | /// Which item types a feature unlocks. | |
| 541 | + | pub fn allowed_item_types(&self) -> &'static [ItemType] { | |
| 542 | + | match self { | |
| 543 | + | Self::Audio => &[ItemType::Audio, ItemType::Sample, ItemType::Preset], | |
| 544 | + | Self::Downloads => &[ | |
| 545 | + | ItemType::Digital, | |
| 546 | + | ItemType::Plugin, | |
| 547 | + | ItemType::Template, | |
| 548 | + | ItemType::Course, | |
| 549 | + | ItemType::Image, | |
| 550 | + | ItemType::Video, | |
| 551 | + | ], | |
| 552 | + | Self::Text => &[ItemType::Text], | |
| 553 | + | // Non-content features don't gate item types | |
| 554 | + | Self::Blog | Self::Subscriptions | Self::LicenseKeys | Self::SourceCode => &[], | |
| 555 | + | } | |
| 556 | + | } | |
| 557 | + | ||
| 558 | + | /// Compute the set of item types allowed by a project's feature list. | |
| 559 | + | /// If no content features are enabled, all types are allowed (permissive default). | |
| 560 | + | pub fn allowed_item_type_cards( | |
| 561 | + | features: &[String], | |
| 562 | + | ) -> Vec<(&'static str, &'static str, &'static str)> { | |
| 563 | + | let allowed: std::collections::HashSet<ItemType> = features | |
| 564 | + | .iter() | |
| 565 | + | .filter_map(|f| f.parse::<ProjectFeature>().ok()) | |
| 566 | + | .flat_map(|f| f.allowed_item_types().iter().copied()) | |
| 567 | + | .collect(); | |
| 568 | + | ||
| 569 | + | // If no content features enabled, show all types (backwards compat) | |
| 570 | + | if allowed.is_empty() { | |
| 571 | + | return Self::all_item_type_cards().to_vec(); | |
| 572 | + | } | |
| 573 | + | ||
| 574 | + | Self::all_item_type_cards() | |
| 575 | + | .iter() | |
| 576 | + | .filter(|(value, _, _)| { | |
| 577 | + | value | |
| 578 | + | .parse::<ItemType>() | |
| 579 | + | .map(|t| allowed.contains(&t)) | |
| 580 | + | .unwrap_or(false) | |
| 581 | + | }) | |
| 582 | + | .copied() | |
| 583 | + | .collect() | |
| 584 | + | } | |
| 585 | + | ||
| 586 | + | /// All item type cards: (value, label, description) tuples for form rendering. | |
| 587 | + | pub fn all_item_type_cards() -> &'static [(&'static str, &'static str, &'static str)] { | |
| 588 | + | &[ | |
| 589 | + | ("audio", "Audio", "Podcast, music, sound effects"), | |
| 590 | + | ("text", "Text", "Articles, posts, essays, guides"), | |
| 591 | + | ("digital", "Digital Download", "Files, archives, documents"), | |
| 592 | + | ("video", "Video", "Tutorials, films, recordings"), | |
| 593 | + | ("course", "Course", "Multi-part lessons, curricula"), | |
| 594 | + | ("plugin", "Plugin", "Software extensions, add-ons"), | |
| 595 | + | ("sample", "Sample Pack", "Audio samples, loops, one-shots"), | |
| 596 | + | ("preset", "Preset Pack", "Synth presets, effect chains"), | |
| 597 | + | ("template", "Template", "Design templates, starter kits"), | |
| 598 | + | ("image", "Image", "Photos, artwork, graphics"), | |
| 599 | + | ] | |
| 600 | + | } | |
| 601 | + | } | |
| 602 | + | ||
| 462 | 603 | // ── Projects ── | |
| 463 | 604 | ||
| 464 | 605 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] | |
| @@ -739,6 +880,116 @@ mod tests { | |||
| 739 | 880 | } | |
| 740 | 881 | ||
| 741 | 882 | #[test] | |
| 883 | + | fn project_feature_round_trip() { | |
| 884 | + | assert_eq!(ProjectFeature::Audio.to_string(), "audio"); | |
| 885 | + | assert_eq!("downloads".parse::<ProjectFeature>().unwrap(), ProjectFeature::Downloads); | |
| 886 | + | assert_eq!("license_keys".parse::<ProjectFeature>().unwrap(), ProjectFeature::LicenseKeys); | |
| 887 | + | assert_eq!("source_code".parse::<ProjectFeature>().unwrap(), ProjectFeature::SourceCode); | |
| 888 | + | assert!("bogus".parse::<ProjectFeature>().is_err()); | |
| 889 | + | } | |
| 890 | + | ||
| 891 | + | #[test] | |
| 892 | + | fn project_feature_label_and_description() { | |
| 893 | + | assert_eq!(ProjectFeature::Audio.label(), "Audio"); | |
| 894 | + | assert_eq!(ProjectFeature::LicenseKeys.label(), "License Keys"); | |
| 895 | + | assert!(!ProjectFeature::Audio.description().is_empty()); | |
| 896 | + | } | |
| 897 | + | ||
| 898 | + | #[test] | |
| 899 | + | fn project_feature_all() { | |
| 900 | + | let all = ProjectFeature::all(); | |
| 901 | + | assert_eq!(all.len(), 7); | |
| 902 | + | assert_eq!(all[0].0, "audio"); | |
| 903 | + | assert_eq!(all[6].0, "source_code"); | |
| 904 | + | } | |
| 905 | + | ||
| 906 | + | #[test] | |
| 907 | + | fn project_feature_allowed_item_types_audio() { | |
| 908 | + | let types = ProjectFeature::Audio.allowed_item_types(); | |
| 909 | + | assert!(types.contains(&ItemType::Audio)); | |
| 910 | + | assert!(types.contains(&ItemType::Sample)); | |
| 911 | + | assert!(types.contains(&ItemType::Preset)); | |
| 912 | + | assert!(!types.contains(&ItemType::Text)); | |
| 913 | + | } | |
| 914 | + | ||
| 915 | + | #[test] | |
| 916 | + | fn project_feature_allowed_item_types_downloads() { | |
| 917 | + | let types = ProjectFeature::Downloads.allowed_item_types(); | |
| 918 | + | assert!(types.contains(&ItemType::Digital)); | |
| 919 | + | assert!(types.contains(&ItemType::Plugin)); | |
| 920 | + | assert!(types.contains(&ItemType::Video)); | |
| 921 | + | assert!(!types.contains(&ItemType::Audio)); | |
| 922 | + | } | |
| 923 | + | ||
| 924 | + | #[test] | |
| 925 | + | fn project_feature_allowed_item_types_text() { | |
| 926 | + | let types = ProjectFeature::Text.allowed_item_types(); | |
| 927 | + | assert!(types.contains(&ItemType::Text)); | |
| 928 | + | assert_eq!(types.len(), 1); | |
| 929 | + | } | |
| 930 | + | ||
| 931 | + | #[test] | |
| 932 | + | fn project_feature_allowed_item_types_non_content() { | |
| 933 | + | assert!(ProjectFeature::Blog.allowed_item_types().is_empty()); | |
| 934 | + | assert!(ProjectFeature::Subscriptions.allowed_item_types().is_empty()); | |
| 935 | + | assert!(ProjectFeature::LicenseKeys.allowed_item_types().is_empty()); | |
| 936 | + | assert!(ProjectFeature::SourceCode.allowed_item_types().is_empty()); | |
| 937 | + | } | |
| 938 | + | ||
| 939 | + | #[test] | |
| 940 | + | fn project_feature_allowed_cards_filtered() { | |
| 941 | + | let cards = ProjectFeature::allowed_item_type_cards(&["audio".into()]); | |
| 942 | + | let values: Vec<&str> = cards.iter().map(|(v, _, _)| *v).collect(); | |
| 943 | + | assert!(values.contains(&"audio")); | |
| 944 | + | assert!(values.contains(&"sample")); | |
| 945 | + | assert!(values.contains(&"preset")); | |
| 946 | + | assert!(!values.contains(&"text")); | |
| 947 | + | assert!(!values.contains(&"digital")); | |
| 948 | + | } | |
| 949 | + | ||
| 950 | + | #[test] | |
| 951 | + | fn project_feature_allowed_cards_combined() { | |
| 952 | + | let cards = ProjectFeature::allowed_item_type_cards(&["audio".into(), "text".into()]); | |
| 953 | + | let values: Vec<&str> = cards.iter().map(|(v, _, _)| *v).collect(); | |
| 954 | + | assert!(values.contains(&"audio")); | |
| 955 | + | assert!(values.contains(&"text")); | |
| 956 | + | assert!(!values.contains(&"digital")); | |
| 957 | + | } | |
| 958 | + | ||
| 959 | + | #[test] | |
| 960 | + | fn project_feature_allowed_cards_empty_features_shows_all() { | |
| 961 | + | let cards = ProjectFeature::allowed_item_type_cards(&[]); | |
| 962 | + | assert_eq!(cards.len(), ProjectFeature::all_item_type_cards().len()); | |
| 963 | + | } | |
| 964 | + | ||
| 965 | + | #[test] | |
| 966 | + | fn project_feature_allowed_cards_non_content_features_shows_all() { | |
| 967 | + | let cards = ProjectFeature::allowed_item_type_cards(&["blog".into(), "subscriptions".into()]); | |
| 968 | + | // Blog and subscriptions don't gate item types, so all should be shown | |
| 969 | + | assert_eq!(cards.len(), ProjectFeature::all_item_type_cards().len()); | |
| 970 | + | } | |
| 971 | + | ||
| 972 | + | #[test] | |
| 973 | + | fn project_feature_derive_type() { | |
| 974 | + | assert_eq!( | |
| 975 | + | ProjectFeature::derive_project_type(&["audio".into(), "blog".into()]), | |
| 976 | + | ProjectType::Music, | |
| 977 | + | ); | |
| 978 | + | assert_eq!( | |
| 979 | + | ProjectFeature::derive_project_type(&["text".into()]), | |
| 980 | + | ProjectType::Blog, | |
| 981 | + | ); | |
| 982 | + | assert_eq!( | |
| 983 | + | ProjectFeature::derive_project_type(&["downloads".into(), "text".into()]), | |
| 984 | + | ProjectType::Software, | |
| 985 | + | ); | |
| 986 | + | assert_eq!( | |
| 987 | + | ProjectFeature::derive_project_type(&["subscriptions".into()]), | |
| 988 | + | ProjectType::General, | |
| 989 | + | ); | |
| 990 | + | } | |
| 991 | + | ||
| 992 | + | #[test] | |
| 742 | 993 | fn project_type_round_trip() { | |
| 743 | 994 | assert_eq!(ProjectType::Blog.to_string(), "blog"); | |
| 744 | 995 | assert_eq!("software".parse::<ProjectType>().unwrap(), ProjectType::Software); |
| @@ -202,6 +202,8 @@ pub struct DbProject { | |||
| 202 | 202 | pub cache_generation: i64, | |
| 203 | 203 | /// Linked MT community ID (None if not yet provisioned or MT unavailable). | |
| 204 | 204 | pub mt_community_id: Option<uuid::Uuid>, | |
| 205 | + | /// Platform features enabled for this project (e.g. audio, blog, downloads). | |
| 206 | + | pub features: Vec<String>, | |
| 205 | 207 | } | |
| 206 | 208 | ||
| 207 | 209 | /// A git repository tracked on disk, optionally linked to a project. |
| @@ -8,18 +8,21 @@ use super::{ProjectId, UserId}; | |||
| 8 | 8 | use crate::error::Result; | |
| 9 | 9 | ||
| 10 | 10 | /// Insert a new project and return the created row. | |
| 11 | + | /// | |
| 12 | + | /// `project_type` is auto-derived from `features` using [`ProjectFeature::derive_project_type`]. | |
| 11 | 13 | pub async fn create_project( | |
| 12 | 14 | pool: &PgPool, | |
| 13 | 15 | user_id: UserId, | |
| 14 | 16 | slug: &Slug, | |
| 15 | 17 | title: &str, | |
| 16 | 18 | description: Option<&str>, | |
| 17 | - | project_type: super::ProjectType, | |
| 19 | + | features: &[String], | |
| 18 | 20 | ) -> Result<DbProject> { | |
| 21 | + | let project_type = super::ProjectFeature::derive_project_type(features); | |
| 19 | 22 | let project = sqlx::query_as::<_, DbProject>( | |
| 20 | 23 | r#" | |
| 21 | - | INSERT INTO projects (user_id, slug, title, description, project_type) | |
| 22 | - | VALUES ($1, $2, $3, $4, $5) | |
| 24 | + | INSERT INTO projects (user_id, slug, title, description, project_type, features) | |
| 25 | + | VALUES ($1, $2, $3, $4, $5, $6) | |
| 23 | 26 | RETURNING * | |
| 24 | 27 | "#, | |
| 25 | 28 | ) | |
| @@ -28,6 +31,7 @@ pub async fn create_project( | |||
| 28 | 31 | .bind(title) | |
| 29 | 32 | .bind(description) | |
| 30 | 33 | .bind(project_type) | |
| 34 | + | .bind(features) | |
| 31 | 35 | .fetch_one(pool) | |
| 32 | 36 | .await?; | |
| 33 | 37 | ||
| @@ -93,21 +97,25 @@ pub async fn get_projects_by_user(pool: &PgPool, user_id: UserId) -> Result<Vec< | |||
| 93 | 97 | } | |
| 94 | 98 | ||
| 95 | 99 | /// Partially update a project's fields (COALESCE keeps existing values when `None`). | |
| 100 | + | /// | |
| 101 | + | /// When `features` is `Some`, the project_type is auto-derived from the new features. | |
| 96 | 102 | pub async fn update_project( | |
| 97 | 103 | pool: &PgPool, | |
| 98 | 104 | id: ProjectId, | |
| 99 | 105 | title: Option<&str>, | |
| 100 | 106 | description: Option<&str>, | |
| 101 | - | project_type: Option<super::ProjectType>, | |
| 107 | + | features: Option<&[String]>, | |
| 102 | 108 | is_public: Option<bool>, | |
| 103 | 109 | ) -> Result<DbProject> { | |
| 110 | + | let project_type = features.map(super::ProjectFeature::derive_project_type); | |
| 104 | 111 | let project = sqlx::query_as::<_, DbProject>( | |
| 105 | 112 | r#" | |
| 106 | 113 | UPDATE projects | |
| 107 | 114 | SET title = COALESCE($2, title), | |
| 108 | 115 | description = COALESCE($3, description), | |
| 109 | 116 | project_type = COALESCE($4, project_type), | |
| 110 | - | is_public = COALESCE($5, is_public) | |
| 117 | + | is_public = COALESCE($5, is_public), | |
| 118 | + | features = COALESCE($6, features) | |
| 111 | 119 | WHERE id = $1 | |
| 112 | 120 | RETURNING * | |
| 113 | 121 | "#, | |
| @@ -117,6 +125,7 @@ pub async fn update_project( | |||
| 117 | 125 | .bind(description) | |
| 118 | 126 | .bind(project_type) | |
| 119 | 127 | .bind(is_public) | |
| 128 | + | .bind(features) | |
| 120 | 129 | .fetch_one(pool) | |
| 121 | 130 | .await?; | |
| 122 | 131 |
| @@ -70,13 +70,26 @@ pub(super) async fn create_item( | |||
| 70 | 70 | ||
| 71 | 71 | verify_project_ownership(&state, project_id, user.id).await?; | |
| 72 | 72 | ||
| 73 | + | // Validate item type against project features | |
| 74 | + | let project = db::projects::get_project_by_id(&state.db, project_id) | |
| 75 | + | .await? | |
| 76 | + | .ok_or(AppError::NotFound)?; | |
| 77 | + | let item_type = req.item_type.unwrap_or(ItemType::Digital); | |
| 78 | + | let allowed = db::ProjectFeature::allowed_item_type_cards(&project.features); | |
| 79 | + | if !allowed.iter().any(|(v, _, _)| *v == item_type.to_string().as_str()) { | |
| 80 | + | return Err(AppError::Validation(format!( | |
| 81 | + | "Item type '{}' is not available for this project's features", | |
| 82 | + | item_type | |
| 83 | + | ))); | |
| 84 | + | } | |
| 85 | + | ||
| 73 | 86 | let item = db::items::create_item( | |
| 74 | 87 | &state.db, | |
| 75 | 88 | project_id, | |
| 76 | 89 | &req.title, | |
| 77 | 90 | req.description.as_deref(), | |
| 78 | 91 | req.price_cents.unwrap_or(0), | |
| 79 | - | req.item_type.unwrap_or(ItemType::Digital), | |
| 92 | + | item_type, | |
| 80 | 93 | ) | |
| 81 | 94 | .await?; | |
| 82 | 95 |
| @@ -30,7 +30,8 @@ pub struct CreateProjectRequest { | |||
| 30 | 30 | pub slug: Slug, | |
| 31 | 31 | pub title: String, | |
| 32 | 32 | pub description: Option<String>, | |
| 33 | - | pub project_type: Option<String>, | |
| 33 | + | #[serde(default)] | |
| 34 | + | pub features: Vec<String>, | |
| 34 | 35 | pub category: Option<String>, | |
| 35 | 36 | } | |
| 36 | 37 | ||
| @@ -42,6 +43,7 @@ pub struct ProjectResponse { | |||
| 42 | 43 | pub title: String, | |
| 43 | 44 | pub description: Option<String>, | |
| 44 | 45 | pub project_type: ProjectType, | |
| 46 | + | pub features: Vec<String>, | |
| 45 | 47 | pub is_public: bool, | |
| 46 | 48 | } | |
| 47 | 49 | ||
| @@ -79,12 +81,11 @@ pub(super) async fn create_project( | |||
| 79 | 81 | None | |
| 80 | 82 | }; | |
| 81 | 83 | ||
| 82 | - | let project_type: ProjectType = req | |
| 83 | - | .project_type | |
| 84 | - | .as_deref() | |
| 85 | - | .unwrap_or("general") | |
| 86 | - | .parse() | |
| 87 | - | .map_err(|_| AppError::Validation("Invalid project type".to_string()))?; | |
| 84 | + | // Validate feature values | |
| 85 | + | for f in &req.features { | |
| 86 | + | f.parse::<db::ProjectFeature>() | |
| 87 | + | .map_err(|_| AppError::Validation(format!("Invalid feature: {f}")))?; | |
| 88 | + | } | |
| 88 | 89 | ||
| 89 | 90 | let project = db::projects::create_project( | |
| 90 | 91 | &state.db, | |
| @@ -92,7 +93,7 @@ pub(super) async fn create_project( | |||
| 92 | 93 | &req.slug, | |
| 93 | 94 | &req.title, | |
| 94 | 95 | req.description.as_deref(), | |
| 95 | - | project_type, | |
| 96 | + | &req.features, | |
| 96 | 97 | ) | |
| 97 | 98 | .await?; | |
| 98 | 99 | ||
| @@ -162,6 +163,7 @@ pub(super) async fn create_project( | |||
| 162 | 163 | title: project.title, | |
| 163 | 164 | description: project.description, | |
| 164 | 165 | project_type: project.project_type, | |
| 166 | + | features: project.features, | |
| 165 | 167 | is_public: project.is_public, | |
| 166 | 168 | }).into_response()) | |
| 167 | 169 | } | |
| @@ -182,6 +184,7 @@ pub(super) async fn list_projects( | |||
| 182 | 184 | title: p.title, | |
| 183 | 185 | description: p.description, | |
| 184 | 186 | project_type: p.project_type, | |
| 187 | + | features: p.features, | |
| 185 | 188 | is_public: p.is_public, | |
| 186 | 189 | }) | |
| 187 | 190 | .collect(); | |
| @@ -194,7 +197,7 @@ pub(super) async fn list_projects( | |||
| 194 | 197 | pub struct UpdateProjectRequest { | |
| 195 | 198 | pub title: Option<String>, | |
| 196 | 199 | pub description: Option<String>, | |
| 197 | - | pub project_type: Option<String>, | |
| 200 | + | pub features: Option<Vec<String>>, | |
| 198 | 201 | pub is_public: Option<bool>, | |
| 199 | 202 | pub category: Option<String>, | |
| 200 | 203 | } | |
| @@ -229,19 +232,20 @@ pub(super) async fn update_project( | |||
| 229 | 232 | } | |
| 230 | 233 | } | |
| 231 | 234 | ||
| 232 | - | let project_type: Option<ProjectType> = req | |
| 233 | - | .project_type | |
| 234 | - | .as_deref() | |
| 235 | - | .map(|s| s.parse()) | |
| 236 | - | .transpose() | |
| 237 | - | .map_err(|_| AppError::Validation("Invalid project type".to_string()))?; | |
| 235 | + | // Validate feature values if provided | |
| 236 | + | if let Some(ref features) = req.features { | |
| 237 | + | for f in features { | |
| 238 | + | f.parse::<db::ProjectFeature>() | |
| 239 | + | .map_err(|_| AppError::Validation(format!("Invalid feature: {f}")))?; | |
| 240 | + | } | |
| 241 | + | } | |
| 238 | 242 | ||
| 239 | 243 | let updated = db::projects::update_project( | |
| 240 | 244 | &state.db, | |
| 241 | 245 | id, | |
| 242 | 246 | req.title.as_deref(), | |
| 243 | 247 | req.description.as_deref(), | |
| 244 | - | project_type, | |
| 248 | + | req.features.as_deref(), | |
| 245 | 249 | req.is_public, | |
| 246 | 250 | ) | |
| 247 | 251 | .await?; | |
| @@ -254,6 +258,7 @@ pub(super) async fn update_project( | |||
| 254 | 258 | title: updated.title, | |
| 255 | 259 | description: updated.description, | |
| 256 | 260 | project_type: updated.project_type, | |
| 261 | + | features: updated.features, | |
| 257 | 262 | is_public: updated.is_public, | |
| 258 | 263 | })) | |
| 259 | 264 | } |
| @@ -219,6 +219,8 @@ pub(super) async fn dashboard_project( | |||
| 219 | 219 | .await? | |
| 220 | 220 | .ok_or(AppError::NotFound)?; | |
| 221 | 221 | ||
| 222 | + | let has_blog = db_project.features.iter().any(|f| f == "blog"); | |
| 223 | + | ||
| 222 | 224 | Ok(DashboardProjectTemplate { | |
| 223 | 225 | csrf_token, | |
| 224 | 226 | session_user: Some(session_user.clone()), | |
| @@ -227,6 +229,7 @@ pub(super) async fn dashboard_project( | |||
| 227 | 229 | stats, | |
| 228 | 230 | items, | |
| 229 | 231 | stripe_connected: db_user.stripe_account_id.is_some(), | |
| 232 | + | has_blog, | |
| 230 | 233 | }) | |
| 231 | 234 | } | |
| 232 | 235 |