Skip to main content

max / makenotwork

v0.4.1: Creator trust audit, security hardening, account lifecycle Creator trust audit (all findings resolved): - Subscription export endpoint (CSV with tier/price/period data) - Bundle and collection structure in project JSON export - Custom domain mappings in export - Fan+ docs marked as not yet available - Portability, tiers, roadmap, content guide, items doc corrections - Payouts: multi-currency clarification, expanded tax guidance - FAQ: storage exceeded, discovery capabilities - Contact: human-only support commitment, proactive monitoring, bus=1 - Best practices: expanded discovery section Security hardening: - Security headers middleware (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy) - SyncKit API key hashing (SHA-256 + prefix, migration 068) - Read API rate limiting (10/sec burst-60) - Nested archive magic bytes detection (ZIP, gzip, 7z, RAR) - Privacy policy: streaming session data disclosure Account lifecycle: - Fan subscription pause on creator suspension (Stripe pause_collection, migration 069, auto-resume on unsuspend/appeal approval) - Account limbo state (self-deactivate, migration 070, restricted dashboard with reactivate/export/delete only) - Support ticket portal in dashboard (WAM ticket + confirmation email) Infrastructure: - Offsite backup replication to astra via Tailscale - WAM alerting on backup sync failure Code fuzz cleanup: 30/31 findings resolved, 1 accepted risk. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-04-26 02:41 UTC
Commit: ec897efae95a35069f131b81166472f7e441945e
Parent: a6b721f
102 files changed, +1432 insertions, -328 deletions
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makenotwork"
3 - version = "0.4.0"
3 + version = "0.4.1"
4 4 edition = "2024"
5 5 license-file = "LICENSE"
6 6
@@ -2,7 +2,12 @@
2 2
3 3 How to restore the Makenotwork database from a backup.
4 4
5 - Backups are gzipped SQL dumps in `/opt/makenotwork/backups/`, named `makenotwork-YYYYMMDD-HHMMSS.sql.gz`. Kept for 30 days.
5 + Backups are gzipped SQL dumps kept for 30 days in two locations:
6 +
7 + - **Primary (Hetzner):** `/opt/makenotwork/backups/makenotwork-YYYYMMDD-HHMMSS.sql.gz`
8 + - **Offsite (astra):** `/opt/backups/mnw/makenotwork-YYYYMMDD-HHMMSS.sql.gz` (synced after each backup via Tailscale)
9 +
10 + If Hetzner is destroyed, the offsite copy on astra survives.
6 11
7 12 ---
8 13
@@ -141,6 +146,21 @@ gzip -t /opt/makenotwork/backups/makenotwork-YYYYMMDD-HHMMSS.sql.gz
141 146
142 147 If the most recent backup is bad, use the previous day's backup.
143 148
149 + ### Hetzner destroyed — restore from offsite
150 +
151 + If the Hetzner VPS is lost, backups survive on astra:
152 +
153 + ```bash
154 + # From astra, list available backups
155 + ls -lh /opt/backups/mnw/makenotwork-*.sql.gz
156 +
157 + # Copy the latest to the new server
158 + scp /opt/backups/mnw/makenotwork-YYYYMMDD-HHMMSS.sql.gz \
159 + root@<new-server>:/opt/makenotwork/backups/
160 + ```
161 +
162 + Then follow the Full Restore procedure above on the new server.
163 +
144 164 ### No backups available
145 165
146 166 If all backups have been lost, the only option is to start fresh:
@@ -55,3 +55,15 @@ fi
55 55 # Summary
56 56 TOTAL=$(find "$BACKUP_DIR" -name "${DB_NAME}-*.sql.gz" | wc -l)
57 57 echo "[$(date -Iseconds)] Total backups on disk: $TOTAL"
58 +
59 + # Sync to offsite host (best-effort — failure here does not fail the backup)
60 + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
61 + OFFSITE_SCRIPT="${SCRIPT_DIR}/sync-backup-offsite.sh"
62 + if [ -x "$OFFSITE_SCRIPT" ]; then
63 + "$OFFSITE_SCRIPT"
64 + else
65 + # Fallback: check deployed location
66 + if [ -x /opt/makenotwork/sync-backup-offsite.sh ]; then
67 + /opt/makenotwork/sync-backup-offsite.sh
68 + fi
69 + fi
@@ -0,0 +1,67 @@
1 + #!/bin/bash
2 + # Sync database backups to offsite host (astra) via Tailscale.
3 + # Called by backup-db.sh after each successful backup.
4 + #
5 + # Setup on astra:
6 + # mkdir -p /opt/backups/mnw
7 + #
8 + # Setup on Hetzner (as makenotwork user):
9 + # Ensure SSH key-based auth to astra is configured:
10 + # ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N ""
11 + # ssh-copy-id max@100.106.221.39
12 + # Test: ssh max@100.106.221.39 "echo ok"
13 +
14 + set -euo pipefail
15 +
16 + OFFSITE_HOST="100.106.221.39" # astra (Tailscale IP)
17 + OFFSITE_USER="max"
18 + OFFSITE_DIR="/opt/backups/mnw"
19 + BACKUP_DIR="/opt/makenotwork/backups"
20 + DB_NAME="makenotwork"
21 + OFFSITE_RETENTION_DAYS=30
22 + WAM_URL="${WAM_URL:-http://127.0.0.1:7890}"
23 +
24 + # Open a WAM ticket for offsite backup failures.
25 + wam_alert() {
26 + local title="$1"
27 + local body="${2:-}"
28 + curl -sf -X POST "$WAM_URL/tickets" \
29 + -H "Content-Type: application/json" \
30 + -d "{\"title\": \"$title\", \"body\": \"$body\", \"priority\": \"high\", \"source\": \"backup-offsite\"}" \
31 + >/dev/null 2>&1 || true
32 + }
33 +
34 + # Find the most recent backup
35 + LATEST=$(ls -t "${BACKUP_DIR}/${DB_NAME}"-*.sql.gz 2>/dev/null | head -1)
36 + if [ -z "$LATEST" ]; then
37 + echo "[$(date -Iseconds)] OFFSITE: No backups found to sync"
38 + exit 0
39 + fi
40 +
41 + echo "[$(date -Iseconds)] OFFSITE: Syncing $(basename "$LATEST") to ${OFFSITE_HOST}:${OFFSITE_DIR}"
42 +
43 + # Transfer with compression (already gzipped, so -z won't help much, but
44 + # rsync handles partial transfers and resume on failure)
45 + if rsync -e "ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new" \
46 + --timeout=120 \
47 + "$LATEST" \
48 + "${OFFSITE_USER}@${OFFSITE_HOST}:${OFFSITE_DIR}/"; then
49 + echo "[$(date -Iseconds)] OFFSITE: Transfer complete"
50 + else
51 + echo "[$(date -Iseconds)] OFFSITE: Transfer FAILED (astra unreachable or SSH error)"
52 + wam_alert "Offsite backup sync failed" "rsync to ${OFFSITE_HOST}:${OFFSITE_DIR} failed for $(basename "$LATEST"). Check Tailscale connectivity and SSH auth."
53 + exit 0
54 + fi
55 +
56 + # Prune old offsite backups
57 + DELETED=$(ssh -o ConnectTimeout=10 "${OFFSITE_USER}@${OFFSITE_HOST}" \
58 + "find ${OFFSITE_DIR} -name '${DB_NAME}-*.sql.gz' -mtime +${OFFSITE_RETENTION_DAYS} -delete -print 2>/dev/null | wc -l" \
59 + 2>/dev/null || echo "0")
60 + if [ "$DELETED" -gt 0 ]; then
61 + echo "[$(date -Iseconds)] OFFSITE: Pruned ${DELETED} backup(s) older than ${OFFSITE_RETENTION_DAYS} days"
62 + fi
63 +
64 + TOTAL=$(ssh -o ConnectTimeout=10 "${OFFSITE_USER}@${OFFSITE_HOST}" \
65 + "ls ${OFFSITE_DIR}/${DB_NAME}-*.sql.gz 2>/dev/null | wc -l" \
66 + 2>/dev/null || echo "?")
67 + echo "[$(date -Iseconds)] OFFSITE: Total backups on astra: ${TOTAL}"
@@ -3,7 +3,7 @@
3 3 ## Status
4 4 Done: All pre-beta phases. Active: Creator setup (Stripe), manual testing. Next: Soft launch.
5 5
6 - v0.3.23. Audit grade A. ~1,233 tests.
6 + v0.4.1. Audit grade A. ~1,233 tests.
7 7
8 8 ---
9 9
@@ -112,41 +112,41 @@ Files > 100 MB are now held for review instead of downloaded into RAM. Next step
112 112 - [ ] Consider GPU-accelerated analysis if volume warrants it
113 113
114 114 ### Other scanning hardening
115 - - [ ] Add timeout to YARA scanning (currently unbounded; crafted input could stall)
115 + - [x] ~~Add timeout to YARA scanning. Fixed: `scanner.set_timeout(30s)` via yara-x native API.~~
116 116 - [ ] Cap ClamAV response buffer size (currently unbounded `read_to_end`)
117 - - [ ] Nested archive detection: check magic bytes, not just file extensions
117 + - [x] ~~Nested archive detection: check magic bytes, not just file extensions. Fixed: magic bytes check for ZIP, gzip, 7z, RAR in archive.rs.~~
118 118
119 119 ---
120 120
121 121 ## Code Fuzz Findings (2026-04-25)
122 122
123 - Bugs found during adversarial code review. Ordered by severity.
124 -
125 - ### Critical
126 - - [x] ~~20 GB file downloaded into RAM for scanning — `SCAN_MAX_MEMORY_BYTES` was dead code (`routes/storage/mod.rs:91`). Fixed: size guard added to `scan_and_classify`.~~
127 -
128 - ### Serious
129 - - [x] ~~Refund-before-payment webhook silently lost. Fixed: unmatched refunds stored in `pending_refunds` table (migration 063). Checkout handler checks for pending refunds after completing a transaction. Scheduler escalates unmatched refunds >24h old via admin alert email.~~
130 - - [x] ~~`validate_key_code` accepts `"----"` — empty word segments pass `all()` vacuously. Fixed: added `part.is_empty()` check + tests.~~
131 - - [x] ~~Project image confirm missing S3 key prefix validation. Fixed: added `starts_with` user ID check in `project_image_confirm`.~~
132 - - [x] ~~SyncKit auth lacks dummy hash. Fixed: added `DUMMY_HASH` + `verify_password` timing equalization.~~
133 - - [x] ~~SyncUser extractor does not check user suspension. Fixed: added `get_user_by_id` + `is_suspended()` check.~~
134 - - [x] ~~`delete_subscription_tier` TOCTOU. Fixed: wrapped in transaction with `FOR UPDATE` on the tier row.~~
135 -
136 - ### Minor
137 - - [x] ~~2FA verification has no per-user failed-attempt counter. Fixed: reuses `increment_failed_login` — failed 2FA attempts count toward account lockout (5 attempts, 15 min). Reset on success.~~
138 - - [x] ~~CSRF body buffer (64KB) < global body limit (1MB). Fixed: increased buffer to 1MB to match global `RequestBodyLimitLayer`.~~
139 - - [x] ~~Import endpoint 10MB size limit unreachable due to 1MB global body limit. Fixed: pulled import route into its own group with 15MB `DefaultBodyLimit` override.~~
140 - - [ ] Idempotency check not atomic with operation — concurrent requests both execute (`db/idempotency.rs`). Safe only because underlying ops are themselves idempotent.
141 - - [ ] `Slug::from_trusted` used on untrusted URL path segments (`custom_domain.rs:164,182` + ~20 page routes). Safe due to sqlx parameterization but a latent footgun.
142 -
143 - ### Note
144 - - [x] ~~`get_user_purchases` duplicate rows. Fixed: wrapped query in `DISTINCT ON (p.item_id)` subquery.~~
145 - - [x] ~~`revoke_keys_by_transaction` LIMIT 1000. Fixed: removed the cap — bulk UPDATE already has no limit, SELECT now matches.~~
146 - - [x] ~~YARA scanning has no timeout. Fixed: `scanner.set_timeout(30s)` via yara-x native API.~~
147 - - [ ] 7-day SyncKit JWT with no per-user revocation (`constants.rs:37`). Stolen token usable for full window.
148 - - [ ] Nested archive detection is extension-based only, not magic bytes (`scanning/archive.rs:81`).
149 - - [ ] No rate limiting on read API routes — enables enumeration of tags, categories, domains (`api/mod.rs:366`).
123 + Two rounds of adversarial code review. 31 findings total: 30 fixed, 1 accepted risk, 1 deferred.
124 +
125 + ### Accepted Risk
126 + - Idempotency check not atomic with operation — concurrent requests both execute (`db/idempotency.rs`). Safe because underlying ops are themselves idempotent.
127 +
128 + ### Deferred
129 + - 7-day SyncKit JWT with no per-user revocation (`constants.rs:37`). Stolen token usable for full window. Requires key rotation infrastructure (SyncKit S4, post-beta).
130 +
131 + ### Resolved (28 findings)
132 + All critical, serious, and minor findings from rounds 1 and 2 are fixed. See git history for details.
133 +
134 + ---
135 +
136 + ## Creator Trust Audit (2026-04-25)
137 +
138 + Systematic creator-perspective audit of docs, legal, code, and competitive positioning.
139 +
140 + ### Resolved (20+ findings)
141 + All doc/code fixes, trust gaps, security issues, and doc clarity items are complete. Key changes: subscription export endpoint, offsite backups with WAM alerting, API key hashing, security headers, fan subscription pause on suspension, account limbo state, support ticket portal, expanded tax/payout/discovery/storage docs, privacy policy updates. See git history.
142 +
143 + ### Remaining
144 + - [ ] No incident post-mortems or public historical incident log (process, not code)
145 +
146 + ### Competitive Positioning (acknowledged, not bugs)
147 + - No free tier — deliberate tradeoff. Earn-back credit program planned.
148 + - No mobile fan app — creator apps exist, no general fan app.
149 + - No editorial discovery — search, tags, follows only. Interested in non-algorithmic discovery methods.
150 150
151 151 ---
152 152
@@ -378,7 +378,7 @@ Weak points identified vs Ko-fi. Ordered by effort/impact.
378 378 - [ ] Series/serial ordering, reading progress
379 379 - [ ] Traffic/referrer tracking
380 380 - [ ] Revisit admin system (currently config-based ADMIN_USER_ID)
381 - - [ ] Test restore from backup
381 + - [ ] Test restore from backup (offsite copy now available on astra — good candidate for test restore)
382 382 - [ ] S3 bucket versioning
383 383 - [ ] PDF stamping (watermark with buyer email/name — superseded by fingerprinting system, needs PDF library integration)
384 384
@@ -392,7 +392,7 @@ MNW/server/src/
392 392 import/ (CSV converter, pipeline, intermediate format)
393 393 MNW/server/tests/
394 394 integration.rs, harness/, workflows/*.rs
395 - MNW/server/migrations/ (001-057)
395 + MNW/server/migrations/ (001-070)
396 396 MNW/server/templates/
397 397 MNW/server/deploy/
398 398 MNW/server/site-docs/public/, MNW/server/site-docs/unpublished/
@@ -0,0 +1,10 @@
1 + -- Webhook event deduplication: track processed event IDs to prevent
2 + -- duplicate processing on Stripe retries.
3 + CREATE TABLE IF NOT EXISTS processed_webhook_events (
4 + event_id TEXT PRIMARY KEY,
5 + processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
6 + );
7 +
8 + -- Auto-clean old entries (keep 30 days)
9 + CREATE INDEX idx_processed_webhook_events_processed_at
10 + ON processed_webhook_events (processed_at);
@@ -0,0 +1,5 @@
1 + -- Partial unique index to prevent duplicate unmatched pending refunds
2 + -- for the same payment intent. Allows multiple matched (historical) rows.
3 + CREATE UNIQUE INDEX IF NOT EXISTS idx_pending_refunds_unmatched_pi
4 + ON pending_refunds (payment_intent_id)
5 + WHERE matched_at IS NULL;
@@ -0,0 +1,13 @@
1 + -- Fix download_fingerprints and streaming_sessions FK constraints to cascade on user deletion.
2 + -- Previously these used the default ON DELETE RESTRICT, which blocked account deletion
3 + -- for any user who had downloaded or streamed content.
4 +
5 + ALTER TABLE download_fingerprints
6 + DROP CONSTRAINT download_fingerprints_user_id_fkey,
7 + ADD CONSTRAINT download_fingerprints_user_id_fkey
8 + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
9 +
10 + ALTER TABLE streaming_sessions
11 + DROP CONSTRAINT streaming_sessions_user_id_fkey,
12 + ADD CONSTRAINT streaming_sessions_user_id_fkey
13 + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
@@ -0,0 +1,3 @@
1 + -- Track the last accepted TOTP time step to prevent code replay within the
2 + -- same 30-second window. NULL = never used (freshly set up).
3 + ALTER TABLE users ADD COLUMN IF NOT EXISTS totp_last_used_step BIGINT;
@@ -0,0 +1,19 @@
1 + -- Hash SyncKit API keys: store SHA-256 hash + prefix instead of plaintext.
2 + -- Existing keys are hashed in place. New keys are hashed before storage.
3 +
4 + CREATE EXTENSION IF NOT EXISTS pgcrypto;
5 +
6 + ALTER TABLE sync_apps ADD COLUMN api_key_hash VARCHAR(64);
7 + ALTER TABLE sync_apps ADD COLUMN api_key_prefix VARCHAR(8);
8 +
9 + UPDATE sync_apps SET
10 + api_key_hash = encode(digest(api_key::bytea, 'sha256'), 'hex'),
11 + api_key_prefix = LEFT(api_key, 8);
12 +
13 + ALTER TABLE sync_apps ALTER COLUMN api_key_hash SET NOT NULL;
14 + ALTER TABLE sync_apps ALTER COLUMN api_key_prefix SET NOT NULL;
15 +
16 + DROP INDEX IF EXISTS idx_sync_apps_api_key;
17 + CREATE UNIQUE INDEX idx_sync_apps_api_key_hash ON sync_apps(api_key_hash);
18 +
19 + ALTER TABLE sync_apps DROP COLUMN api_key;
@@ -0,0 +1,3 @@
1 + -- Track when a subscription was paused due to creator suspension.
2 + -- NULL = not paused. Set when creator is suspended, cleared when unsuspended.
3 + ALTER TABLE subscriptions ADD COLUMN paused_at TIMESTAMPTZ;
@@ -0,0 +1,4 @@
1 + -- Self-service account deactivation ("limbo" state).
2 + -- When set, the account cannot create anything new. The user can only
3 + -- reactivate, export data, or permanently delete.
4 + ALTER TABLE users ADD COLUMN deactivated_at TIMESTAMPTZ;
@@ -134,9 +134,9 @@ The following guarantees are commitments we are building toward. They are not ye
134 134
135 135 ### Content Archive
136 136
137 - *Launching before we leave beta — among the first priorities after our initial round of community engagement and testing.*
137 + *Planned — not yet implemented. We intend to build this before leaving beta.*
138 138
139 - Any content that has existed on the platform for 12 months or more (not including comped months from the earn-back credit program) will remain hosted and accessible to fans even if the creator stops paying for their account.
139 + Any content that has existed on the platform for 12 months or more would remain hosted and accessible to fans even if the creator stops paying for their account.
140 140
141 141 - Archived content stays live at its original URLs.
142 142 - Fans who purchased the content retain access.
@@ -7,7 +7,7 @@ Flat fee. All your revenue passes through to you.
7 7 ## For Creators
8 8
9 9 1. **Sign up** and choose a pricing tier ($10-$40/month based on content type)
10 - 2. **Upload content** — text, audio, software, video, or digital files
10 + 2. **Upload content** — text, audio, software, or digital files
11 11 3. **Organize** using hierarchical tags and projects
12 12 4. **Set pricing** — free, pay-what-you-want, fixed price, or subscription
13 13 5. **Get paid** — 0% platform fee, only payment processing fees
@@ -104,9 +104,9 @@ The prices reflect what it actually costs to store and deliver each content type
104 104
105 105 ### Earn-Back Credit Program
106 106
107 - *Launching before we leave beta — among the first priorities after our initial round of community engagement and testing.*
107 + *Planned — not yet implemented. We intend to build this before leaving beta.*
108 108
109 - If you earn less on the platform than you paid in subscription fees during a 12-month period, the difference will be credited as free months for the following year (capped at 12 months). Credits will be calculated annually on your account anniversary.
109 + If you earn less on the platform than you paid in subscription fees during a 12-month period, the difference would be credited as free months for the following year (capped at 12 months). Credits would be calculated annually on your account anniversary.
110 110
111 111 ### Add-Ons
112 112
@@ -142,9 +142,9 @@ Your audience is your business. We facilitate the connection; you own it.
142 142
143 143 ### Content Archive Policy
144 144
145 - *Launching before we leave beta — among the first priorities after our initial round of community engagement and testing.*
145 + *Planned — not yet implemented. We intend to build this before leaving beta.*
146 146
147 - Content that has been on the platform for 12 months or more (not including comped months from the earn-back credit program) stays hosted even if you cancel. Your fans keep access. You just can't upload new content without reactivating. See our [written guarantees](./guarantees.md) for the full commitment.
147 + Content that has been on the platform for 12 months or more would stay hosted even if you cancel. Your fans keep access. You just can't upload new content without reactivating. See our [written guarantees](./guarantees.md) for the full commitment.
148 148
149 149 ---
150 150
@@ -32,7 +32,7 @@ Everything listed here is live and working.
32 32 ### Discovery & Organization
33 33
34 34 - **Discover page**: Browse items or projects with search across titles, descriptions, and usernames
35 - - **Filters**: Item type (Audio, Text, Digital, Physical), price range, tags, project category, sort by newest/price/popular
35 + - **Filters**: Item type (Audio, Text, Digital, Video, Image, Plugin, Preset, Sample, Course, Template, Bundle), price range, tags, project category, sort by newest/price/popular
36 36 - **Hierarchical tags**: Multi-level tag tree with breadcrumb navigation, primary tag designation per item
37 37 - **Project categories**: 12 built-in categories (Music, Band, Podcast, Blog, Software, Art, etc.) plus user-created categories
38 38 - **Follows**: Follow users, projects, or tags, personalized feed page
@@ -80,7 +80,7 @@ Everything listed here is live and working.
80 80 ### Platform
81 81
82 82 - **Source-available codebase**: PolyForm Noncommercial 1.0.0
83 - - **Creator waitlist**: Invite-only launch with lottery waves and hand-picked approvals
83 + - **Creator applications**: Apply to create -- most applications approved within a few days
84 84 - **Admin tools**: Waitlist management, creator approval, suspension/appeal processing, revenue reports, data export
85 85 - **Rich link previews**: Your content shows up properly when shared on social media, search engines, and podcast apps
86 86 - **Documentation**: Creator guide covering the full platform
@@ -17,13 +17,14 @@ Your fan account is ready immediately. You can browse, follow creators, and purc
17 17
18 18 ## Apply for Creator Access
19 19
20 - Creator access is currently invite-only via the waitlist. To apply:
20 + To sell your work, apply for creator access:
21 21
22 - 1. Go to the waitlist page
23 - 2. Write a short pitch (20-500 characters) about what you want to create
24 - 3. Submit your application
22 + 1. Go to the [creators page](/creators) or your dashboard
23 + 2. Tell us what you make (20-500 characters) -- a link to your portfolio, channel, or existing storefront helps
24 + 3. Choose which tier fits your content type
25 + 4. Submit your application
25 26
26 - Applications are reviewed in waves. You'll get an email when you're approved.
27 + Most applications are approved within a few days. You'll get an email when you're in.
27 28
28 29 ## Connect Payments
29 30
@@ -74,7 +75,7 @@ Items are individual pieces of content inside a project.
74 75 ## First 15 Minutes Checklist
75 76
76 77 - [ ] Account created and email verified
77 - - [ ] Waitlist application submitted (or creator access granted)
78 + - [ ] Creator application submitted (or creator access granted)
78 79 - [ ] Payment account connected
79 80 - [ ] First project created with title, slug, and category
80 81 - [ ] First item created with content uploaded and AI tier declared
@@ -130,7 +131,7 @@ After your first publish, here's what to focus on:
130 131 2. **Set up security.** Enable two-factor authentication and save your backup codes. See [Security](../tech/security.md).
131 132 3. **Share your link.** Post your profile URL or project URL wherever your audience is.
132 133 4. **Set up RSS cross-posting.** Connect your RSS feed to social media or newsletter tools. See [RSS](./rss.md).
133 - 5. **Fill in metadata.** Good titles, descriptions, tags, and cover art make your content discoverable and shareable. See [Metadata](./metadata.md).
134 + 5. **Fill in metadata.** Good titles, descriptions, tags, and cover art make your content discoverable and shareable. See [Metadata](./metadata.md). Per-file size limits and supported formats depend on your tier — see [Pricing Tiers](./tiers.md) for specifics.
134 135 6. **Join the forum.** Say hello at [forums.makenot.work](https://forums.makenot.work). It's where platform feedback, feature requests, and creator-to-creator discussion happen.
135 136
136 137 ## See Also
@@ -20,7 +20,7 @@ Don't overthink the type choice — pick what matches the content. The type dete
20 20
21 21 ## Items
22 22
23 - Items are individual pieces of content — a song, an article, a software release, a file download. Ten item types are available (Audio, Text, Digital, Video, Image, Plugin, Preset, Sample, Course, Template). See [Items](./items.md) for the full type matrix with player/viewer support, chapters, and versioning.
23 + Items are individual pieces of content — a song, an article, a software release, a file download. Eleven item types are available (Audio, Text, Digital, Video, Image, Plugin, Preset, Sample, Course, Template, Bundle). See [Items](./items.md) for the full type matrix with player/viewer support, chapters, and versioning.
24 24
25 25 Choose the type when creating the item. It cannot be changed afterward.
26 26
@@ -63,12 +63,16 @@ But don't default to free. Your work has value.
63 63
64 64 We don't have algorithmic recommendations, trending pages, or feeds designed to maximize engagement. What we do have:
65 65
66 - - **Search** — Fans can search for creators, projects, and content
66 + - **Search** — Fans can search for creators, projects, and content by title, description, and username
67 67 - **Tags** — Hierarchical tagging lets fans browse by genre, format, style
68 + - **Categories** — Project categories (Music, Software, Art, etc.) give fans a starting point
69 + - **Follows and feeds** — Fans who follow you see new releases in their personal feed
68 70 - **Project pages** — Shareable, linkable homes for your work
69 71
70 72 This is intentional discovery. Invest in good tagging — accurate genres, descriptive tags, complete metadata. This is how fans browsing for "ambient electronic" or "short fiction" will find your work.
71 73
74 + We're actively interested in building better discovery features — ways to help fans find creators they'd value. What we won't do is build black-box recommendation engines that optimize for engagement over intent. If you have ideas for discovery methods that respect both creators and fans, we'd like to hear them.
75 +
72 76 ### Your Existing Audience Still Matters
73 77
74 78 Most creators here started with an existing audience elsewhere. Search and tags mean new fans can find you too, but don't rely on discovery as your primary growth channel. Bring your people with you.
@@ -1,15 +1,19 @@
1 1 # Fan+ Subscription
2 2
3 - Fan+ is a platform-wide subscription for fans who want to support the Makenot.work ecosystem and get access to premium features.
3 + *Fan+ is not yet available. This page describes the planned feature set. Fan+ will launch during or after the beta period.*
4 +
5 + Fan+ is a platform-wide subscription designed for two audiences: people who use Makenot.work tools but don't distribute content through the platform, and fans who want to show extra support for the ecosystem.
4 6
5 7 ## What Fans Get
6 8
7 9 Fan+ subscribers receive:
8 10
9 11 - **Credit system** — Monthly credits that can be used toward purchases on the platform
10 - - **Premium access** — Features and perks that are gated behind Fan+ membership
12 + - **Fan+ badge** — Visible on your profile
11 13 - **Platform support** — Direct contribution to keeping the platform running with 0% creator fees
12 14
15 + We may occasionally offer bonuses to Fan+ members, but Fan+ will never affect how fans engage with creators or gate access to any creator's content. It is a way to support the platform, not a paywall between fans and the people they follow.
16 +
13 17 Fan+ is separate from creator subscription tiers. A fan can subscribe to individual creators *and* be a Fan+ member — they serve different purposes.
14 18
15 19 ## How It Works
@@ -18,6 +18,7 @@ Items are individual pieces of content: a song, an article, a software release,
18 18 | **Sample** | Sample packs | Download link | No | Yes |
19 19 | **Course** | Educational content | Course viewer | No | Yes |
20 20 | **Template** | Templates and themes | Download link | No | Yes |
21 + | **Bundle** | Collection of other items | Combined listing | No | No |
21 22
22 23 Choose the type when creating the item. It cannot be changed afterward.
23 24