Skip to main content

max / makenotwork

v0.3.11: Project features system + item type filtering Replace static project_type with flexible features array. Projects now declare capabilities (audio, downloads, text, blog, subscriptions, license_keys, source_code) and item creation is filtered to match. - Migration 046: features TEXT[] column with backfill from project_type - ProjectFeature enum with allowed_item_types() mapping - Item wizard filters type cards by project features - Server-side validation in both wizard and API item creation - Features grid in project settings with live-save - Project wizard basics step uses feature checkboxes - Updated docs: roadmap, tiers, migration guide, legal policies - 8 new unit tests (526 total) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-03-28 16:39 UTC
Commit: d1fc7c8a5ad0b706e6def1445f42c0e6f822d385
Parent: 4db53ab
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 &amp; Downloads</a></li>
29 + <li><a href="./tags.html">Tags</a></li>
30 + <li><a href="./03-selling.html">Selling &amp; Monetization</a></li>
31 + <li><a href="./pricing.html">Pricing Tiers</a></li>
32 + <li><a href="./tiers.html">Tier Comparison &amp; Break-Even</a></li>
33 + <li><a href="./migration.html">Platform Migration Guides</a></li>
34 + <li><a href="./profile.html">Profile &amp; 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 &amp; 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