max / makenotwork
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 |