| 1 |
# Schema — MNW Server |
| 2 |
|
| 3 |
PostgreSQL database. Migrations live under `migrations/`, numbered and auto-applied on boot via sqlx; the directory is the source of truth. Extension: `pg_trgm` (trigram fuzzy search). |
| 4 |
|
| 5 |
## Domain Map |
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
| Users & Auth | 6 | Accounts, passkeys, sessions, 2FA, login tokens | |
| 10 |
| Projects & Content | 8 | Creator projects, items, versions, chapters, insertions, sections, bundles | |
| 11 |
| Tags & Taxonomy | 4 | Hierarchical tags, item tagging, platform labels | |
| 12 |
| Commerce | 7 | Transactions, subscriptions, promo codes, license keys | |
| 13 |
| Creator Tiers | 2 | Platform subscription tiers (Basic/Small/Big/Everything) | |
| 14 |
| Email & Mailing | 4 | Mailing lists, subscribers, suppressions, signups | |
| 15 |
| Social | 3 | Follows, blog posts, custom links | |
| 16 |
| Collections | 2 | User-curated item lists | |
| 17 |
| SyncKit | 5 | Cloud sync apps, devices, keys, changelog, blobs | |
| 18 |
| Git | 6 | Repos, SSH keys, issues, comments, labels | |
| 19 |
| OTA Updates | 4 | Releases, artifacts, build configs, build runs | |
| 20 |
| Custom Domains | 1 | Creator vanity domains | |
| 21 |
| OAuth | 1 | PKCE authorization codes | |
| 22 |
| Waitlist & Invites | 3 | Creator waves, waitlist, invite codes | |
| 23 |
| Admin | 1 | Abuse reports | |
| 24 |
| Media | 1 | User media library (images for markdown) | |
| 25 |
| Import | 1 | Bulk import jobs (Patreon, Ko-fi, Gumroad) | |
| 26 |
| Sessions | 1 | HTTP sessions (tower-sessions) | |
| 27 |
|
| 28 |
--- |
| 29 |
|
| 30 |
## Users & Authentication |
| 31 |
|
| 32 |
### users |
| 33 |
Core accounts. Every user has one row; creator features are gated by `can_create_projects`. |
| 34 |
|
| 35 |
|
| 36 |
|
| 37 |
| id | UUID PK | | |
| 38 |
| username | TEXT UNIQUE | URL slug (`/@username`) | |
| 39 |
| email | TEXT UNIQUE | | |
| 40 |
| display_name | TEXT | | |
| 41 |
| password_hash | TEXT | argon2 | |
| 42 |
| email_verified | BOOL | | |
| 43 |
| totp_secret / totp_enabled | TEXT / BOOL | 2FA | |
| 44 |
| failed_login_attempts | INT | Resets on success | |
| 45 |
| locked_until | TIMESTAMPTZ | Lockout after 5 failures | |
| 46 |
| stripe_account_id | TEXT | Stripe Connect account | |
| 47 |
| stripe_onboarding_complete | BOOL | | |
| 48 |
| can_create_projects | BOOL | Creator gate | |
| 49 |
| creator_tier | TEXT | 'basic', 'small_files', 'big_files', 'everything' | |
| 50 |
| storage_used_bytes | BIGINT | Computed from versions + insertions | |
| 51 |
| max_file_override_bytes | BIGINT | Per-user override | |
| 52 |
| grandfathered_until | TIMESTAMPTZ | Grace period for existing creators | |
| 53 |
| suspended_at | TIMESTAMPTZ | Null = active | |
| 54 |
| notify_sale / notify_follower / notify_release / notify_issues | BOOL | Email prefs | |
| 55 |
|
| 56 |
**Indexes:** email, username, email_verified, stripe_account — all B-tree. |
| 57 |
**Trigger:** `update_users_updated_at` — auto-sets `updated_at`. |
| 58 |
|
| 59 |
### user_passkeys |
| 60 |
WebAuthn credentials for passwordless login. |
| 61 |
|
| 62 |
|
| 63 |
|
| 64 |
| id | UUID PK | | |
| 65 |
| user_id | UUID FK → users CASCADE | | |
| 66 |
| credential_json | JSONB | WebAuthn credential blob | |
| 67 |
| credential_id | BYTEA UNIQUE | Lookup key | |
| 68 |
| name | TEXT | User-assigned label | |
| 69 |
|
| 70 |
### login_tokens |
| 71 |
Single-use email login links. |
| 72 |
|
| 73 |
- **FK:** user_id → users CASCADE |
| 74 |
- **Key columns:** token_hash, expires_at, used_at |
| 75 |
- **Indexed on:** user_id, expires_at |
| 76 |
|
| 77 |
### user_sessions |
| 78 |
Active login sessions. Tracks last activity, UA, IP for "active sessions" UI. |
| 79 |
|
| 80 |
- **FK:** user_id → users CASCADE |
| 81 |
- **Indexed on:** user_id |
| 82 |
|
| 83 |
### backup_codes |
| 84 |
2FA recovery codes (hashed). Marked with `used_at` when consumed. |
| 85 |
|
| 86 |
- **FK:** user_id → users CASCADE |
| 87 |
|
| 88 |
### tower_sessions.session |
| 89 |
HTTP session storage (tower-sessions-sqlx-store). Schema `tower_sessions`, PK is TEXT `id`, stores BYTEA `data` with `expiry_date`. |
| 90 |
|
| 91 |
--- |
| 92 |
|
| 93 |
## Projects & Content |
| 94 |
|
| 95 |
### projects |
| 96 |
Creator projects — music releases, software, podcasts, books, etc. |
| 97 |
|
| 98 |
|
| 99 |
|
| 100 |
| id | UUID PK | | |
| 101 |
| user_id | UUID FK → users CASCADE | | |
| 102 |
| slug | TEXT | URL path segment | |
| 103 |
| title | TEXT | | |
| 104 |
| project_type | TEXT | 'music', 'software', 'podcast', etc. | |
| 105 |
| is_public | BOOL | | |
| 106 |
| category_id | UUID FK → project_categories SET NULL | | |
| 107 |
| mt_community_id | UUID | Links to Multithreaded forum | |
| 108 |
| features | TEXT[] | Feature flags per project | |
| 109 |
|
| 110 |
**Constraint:** UNIQUE(user_id, slug). |
| 111 |
**Indexes:** user_id, is_public, category_id, title trigram (GIN), description trigram (GIN). |
| 112 |
**Trigger:** `update_projects_updated_at`. |
| 113 |
|
| 114 |
### items |
| 115 |
Products/content within projects. The central commerce entity — holds pricing, audio, text, licensing. |
| 116 |
|
| 117 |
|
| 118 |
|
| 119 |
| id | UUID PK | | |
| 120 |
| project_id | UUID FK → projects CASCADE | | |
| 121 |
| title / slug | TEXT | UNIQUE(project_id, slug) | |
| 122 |
| item_type | TEXT | 'article', 'audio', 'download', 'video', etc. | |
| 123 |
| price_cents | INT | 0 = free. CHECK >= 0 | |
| 124 |
| pwyw_enabled | BOOL | Pay what you want | |
| 125 |
| pwyw_min_cents | INT | Floor for PWYW | |
| 126 |
| body / word_count / reading_time_minutes | TEXT / INT / INT | Text content | |
| 127 |
| audio_url / audio_s3_key / duration_seconds | TEXT / TEXT / FLOAT | Audio content | |
| 128 |
| video_s3_key / video_duration_seconds | TEXT / FLOAT | Video content | |
| 129 |
| enable_license_keys | BOOL | DRM gate | |
| 130 |
| custom_license_text | TEXT | License shown on download | |
| 131 |
| sales_count / play_count / download_count | INT | Denormalized counters | |
| 132 |
| web_only | BOOL | Publish without emailing mailing-list subscribers | |
| 133 |
|
| 134 |
**Indexes:** project_id, is_public, sales_count, tsvector search (title+description+body), title trigram, desc trigram, (project_id, slug). |
| 135 |
**Trigger:** `update_items_updated_at`. |
| 136 |
|
| 137 |
### versions |
| 138 |
Downloadable file versions per item (software releases, audio stems). |
| 139 |
|
| 140 |
- **FK:** item_id → items CASCADE |
| 141 |
- **Key columns:** version_number, file_url, s3_key, file_size_bytes, is_current, download_count |
| 142 |
- **Constraint:** UNIQUE WHERE is_current = true (only one current version per item) |
| 143 |
|
| 144 |
### chapters |
| 145 |
Audio/podcast chapter markers. Sorted by `start_seconds`. |
| 146 |
|
| 147 |
- **FK:** item_id → items CASCADE |
| 148 |
|
| 149 |
### content_insertions |
| 150 |
Reusable audio clips (ads, intros, outros) uploaded by creators. |
| 151 |
|
| 152 |
- **FK:** user_id → users CASCADE |
| 153 |
- **Key columns:** title, media_type, storage_key, duration_ms, file_size |
| 154 |
|
| 155 |
### content_insertion_placements |
| 156 |
Where insertions attach to items. Position is 'pre_roll', 'mid_roll', or 'post_roll'. |
| 157 |
|
| 158 |
- **FK:** item_id → items CASCADE, insertion_id → content_insertions CASCADE |
| 159 |
- **Constraint:** UNIQUE(item_id, insertion_id, position, offset_ms) |
| 160 |
|
| 161 |
### item_sections |
| 162 |
Tabbed content blocks within items (e.g., "Ingredients", "Instructions", "Changelog"). |
| 163 |
|
| 164 |
- **FK:** item_id → items CASCADE |
| 165 |
- **Constraint:** UNIQUE(item_id, slug) |
| 166 |
|
| 167 |
### bundle_items |
| 168 |
Associates items into bundle-type items. CHECK(bundle_id != item_id) prevents self-reference. |
| 169 |
|
| 170 |
- **PK:** (bundle_id, item_id) |
| 171 |
- **FK:** both → items CASCADE |
| 172 |
|
| 173 |
--- |
| 174 |
|
| 175 |
## Tags & Taxonomy |
| 176 |
|
| 177 |
### tags |
| 178 |
Hierarchical tag system. `path` uses dot-notation for materialized paths (e.g., `music.electronic.ambient`). |
| 179 |
|
| 180 |
|
| 181 |
|
| 182 |
| id | UUID PK | | |
| 183 |
| parent_id | UUID FK → tags CASCADE | Self-referential hierarchy | |
| 184 |
| name | TEXT | Display name | |
| 185 |
| slug | TEXT UNIQUE | URL-safe | |
| 186 |
| path | TEXT | Materialized path (dot-notation) | |
| 187 |
|
| 188 |
**Indexes:** parent_id, slug, name trigram (GIN), path (prefix queries). |
| 189 |
|
| 190 |
### item_tags |
| 191 |
Many-to-many. `is_primary` marks the main tag for an item (UNIQUE WHERE is_primary). |
| 192 |
|
| 193 |
- **PK:** (item_id, tag_id), both CASCADE |
| 194 |
|
| 195 |
### labels |
| 196 |
Platform-curated promises (e.g., "DRM-free", "Lossless audio"). Includes definition, examples, and non-examples. |
| 197 |
|
| 198 |
- **Key columns:** slug UNIQUE, display_name, definition, examples, nonexamples |
| 199 |
|
| 200 |
### project_labels |
| 201 |
Projects adopt platform labels (creator commits to the promise). |
| 202 |
|
| 203 |
- **PK:** (project_id, label_id), both CASCADE |
| 204 |
|
| 205 |
--- |
| 206 |
|
| 207 |
## Commerce & Payments |
| 208 |
|
| 209 |
### transactions |
| 210 |
One-off purchases. Status lifecycle: pending → completed / failed / refunded. |
| 211 |
|
| 212 |
|
| 213 |
|
| 214 |
| id | UUID PK | | |
| 215 |
| buyer_id | UUID FK → users CASCADE | | |
| 216 |
| seller_id | UUID FK → users SET NULL | Preserved if seller deletes account | |
| 217 |
| item_id | UUID FK → items SET NULL | Preserved if item deletes | |
| 218 |
| amount_cents | INT | CHECK >= 0 | |
| 219 |
| platform_fee_cents | INT | Always 0 (0% fee model) | |
| 220 |
| stripe_checkout_session_id | TEXT | Links to Stripe | |
| 221 |
| status | TEXT | 'pending', 'completed', 'failed', 'refunded' | |
| 222 |
| item_title / seller_username | TEXT | Denormalized for receipt display | |
| 223 |
|
| 224 |
**Constraint:** UNIQUE(buyer_id, item_id) WHERE status = 'completed' — prevents double purchase. |
| 225 |
**Indexes:** buyer_id, seller_id, item_id, status, stripe_session. |
| 226 |
|
| 227 |
### subscription_tiers |
| 228 |
Per-project recurring tiers. Each tier has a Stripe product+price. |
| 229 |
|
| 230 |
- **FK:** project_id → projects CASCADE |
| 231 |
- **Key columns:** name, price_cents, stripe_product_id, stripe_price_id, is_active |
| 232 |
|
| 233 |
### subscriptions |
| 234 |
Active subscriber records. UNIQUE(subscriber_id, project_id) WHERE status = 'active'. |
| 235 |
|
| 236 |
- **FK:** subscriber_id → users CASCADE, tier_id → subscription_tiers RESTRICT, project_id → projects CASCADE |
| 237 |
- **Note:** tier_id uses RESTRICT — cannot delete a tier that has active subscribers |
| 238 |
|
| 239 |
### subscription_events |
| 240 |
Webhook events from Stripe. Keyed by `stripe_event_id` (UNIQUE) for idempotency. |
| 241 |
|
| 242 |
- **FK:** subscription_id → subscriptions SET NULL |
| 243 |
|
| 244 |
### promo_codes |
| 245 |
Unified discount/free-access/free-trial codes. Purpose-specific CHECK constraints enforce valid field combinations. |
| 246 |
|
| 247 |
- **FK:** creator_id → users CASCADE; item_id, project_id, tier_id all → SET NULL on target delete |
| 248 |
- **Key columns:** code, code_purpose, discount_type, discount_value, trial_days, max_uses, use_count |
| 249 |
- **Constraint:** UNIQUE(creator_id, code) |
| 250 |
|
| 251 |
### license_keys |
| 252 |
DRM keys for download-limited items. Tracks activation count vs max_activations. |
| 253 |
|
| 254 |
- **FK:** item_id → items CASCADE, owner_id → users CASCADE, transaction_id → transactions SET NULL |
| 255 |
- **Key columns:** key_code UNIQUE, max_activations, activation_count, revoked_at |
| 256 |
|
| 257 |
### license_activations |
| 258 |
Per-machine activations. UNIQUE(license_key_id, machine_id) prevents double-activate on same machine. |
| 259 |
|
| 260 |
- **FK:** license_key_id → license_keys CASCADE |
| 261 |
|
| 262 |
--- |
| 263 |
|
| 264 |
## Creator Tiers |
| 265 |
|
| 266 |
### creator_subscriptions |
| 267 |
Platform subscription for creators (Basic $16, Small Files $24, Big Files $36, Everything $60). One row per creator. |
| 268 |
|
| 269 |
- **FK:** user_id → users CASCADE (UNIQUE) |
| 270 |
- **Key columns:** tier, status, stripe_subscription_id UNIQUE, grace_enforced_at |
| 271 |
|
| 272 |
### fan_plus_subscriptions |
| 273 |
Fan+ consumer subscription (discoverability + collection features). One row per user. |
| 274 |
|
| 275 |
- **FK:** user_id → users CASCADE (UNIQUE) |
| 276 |
- **Key columns:** status, stripe_subscription_id UNIQUE |
| 277 |
|
| 278 |
--- |
| 279 |
|
| 280 |
## Email & Mailing Lists |
| 281 |
|
| 282 |
### mailing_lists |
| 283 |
Per-project lists. Types: 'content' (new releases), 'devlog' (development updates), 'patches' (software patches). |
| 284 |
|
| 285 |
- **FK:** project_id → projects CASCADE |
| 286 |
- **Constraint:** UNIQUE(project_id, list_type) |
| 287 |
|
| 288 |
### mailing_list_subscribers |
| 289 |
Supports both registered users and email-only subscribers. CHECK(user_id NOT NULL OR email NOT NULL). |
| 290 |
|
| 291 |
- **FK:** list_id → mailing_lists CASCADE, user_id → users CASCADE (nullable) |
| 292 |
- **Constraints:** UNIQUE(list_id, user_id), UNIQUE(list_id, email) WHERE email NOT NULL |
| 293 |
|
| 294 |
### email_suppressions |
| 295 |
Hard bounces and spam complaints. Prevents sending to known-bad addresses. |
| 296 |
|
| 297 |
- **Key columns:** email UNIQUE (case-insensitive), reason ('HardBounce', 'SpamComplaint') |
| 298 |
|
| 299 |
### email_signups |
| 300 |
Landing page "notify me" signups (pre-launch or feature waitlist). |
| 301 |
|
| 302 |
- **Key columns:** email UNIQUE, source |
| 303 |
|
| 304 |
--- |
| 305 |
|
| 306 |
## Social & Community |
| 307 |
|
| 308 |
### follows |
| 309 |
Polymorphic follow system — users can follow users, projects, or tags. |
| 310 |
|
| 311 |
- **FK:** follower_id → users CASCADE |
| 312 |
- **Key columns:** target_type ('user', 'project', 'tag'), target_id |
| 313 |
- **Constraint:** UNIQUE(follower_id, target_type, target_id) |
| 314 |
|
| 315 |
### blog_posts |
| 316 |
Project-level blog posts (devlogs, announcements). Markdown source + rendered HTML. |
| 317 |
|
| 318 |
- **FK:** project_id → projects CASCADE, author_id → users (no cascade) |
| 319 |
- **Key columns:** title, slug, body_markdown, body_html, published_at, mt_thread_id |
| 320 |
- **Constraint:** UNIQUE(project_id, slug) |
| 321 |
|
| 322 |
### custom_links |
| 323 |
Creator profile links (social, merch, website). Ordered by sort_order. |
| 324 |
|
| 325 |
- **FK:** user_id → users CASCADE |
| 326 |
- **Trigger:** `update_custom_links_updated_at` |
| 327 |
|
| 328 |
--- |
| 329 |
|
| 330 |
## Collections |
| 331 |
|
| 332 |
### collections |
| 333 |
User-curated lists (playlists, reading lists, favorites). |
| 334 |
|
| 335 |
- **FK:** user_id → users CASCADE |
| 336 |
- **Constraint:** UNIQUE(user_id, slug) |
| 337 |
|
| 338 |
### collection_items |
| 339 |
Items in collections. Ordered by `position`. |
| 340 |
|
| 341 |
- **PK:** (collection_id, item_id), both CASCADE |
| 342 |
|
| 343 |
--- |
| 344 |
|
| 345 |
## SyncKit |
| 346 |
|
| 347 |
### sync_apps |
| 348 |
Registered SyncKit applications (GO, BB, AF, third-party). |
| 349 |
|
| 350 |
- **FK:** creator_id → users CASCADE; project_id → projects SET NULL, item_id → items SET NULL |
| 351 |
- **Key columns:** name, api_key UNIQUE, slug, is_active, redirect_uris TEXT[] |
| 352 |
|
| 353 |
### sync_devices |
| 354 |
User devices per app. Last-seen tracking for device management UI. |
| 355 |
|
| 356 |
- **FK:** app_id → sync_apps CASCADE, user_id → users CASCADE |
| 357 |
- **Constraint:** UNIQUE(app_id, user_id, device_name) |
| 358 |
|
| 359 |
### sync_keys |
| 360 |
E2E encryption keys per app/user pair. Key rotation via `key_version`. |
| 361 |
|
| 362 |
- **PK:** (app_id, user_id), both CASCADE |
| 363 |
- **Key columns:** key_version, encrypted_key |
| 364 |
|
| 365 |
### sync_log |
| 366 |
Changelog of data operations. Sequential `seq` (BIGSERIAL) enables cursor-based pull. |
| 367 |
|
| 368 |
- **FK:** app_id, user_id, device_id — all CASCADE |
| 369 |
- **Key columns:** table_name, operation ('INSERT'/'UPDATE'/'DELETE'), row_id, data (JSONB), client_timestamp |
| 370 |
- **Index:** (app_id, user_id, seq) — the primary pull query path |
| 371 |
|
| 372 |
### sync_blobs |
| 373 |
File blobs for SyncKit (content-hashed dedup). Used when `sync_files=true` on a VFS. |
| 374 |
|
| 375 |
- **FK:** app_id, user_id — both CASCADE |
| 376 |
- **Constraint:** UNIQUE(app_id, user_id, hash) |
| 377 |
- **Key columns:** hash, s3_key, size_bytes |
| 378 |
|
| 379 |
--- |
| 380 |
|
| 381 |
## Git Integration |
| 382 |
|
| 383 |
### git_repos |
| 384 |
Bare git repositories. Displayed via the `/source/` browser (G1, git2-based). |
| 385 |
|
| 386 |
- **FK:** user_id → users CASCADE, project_id → projects SET NULL |
| 387 |
- **Constraint:** UNIQUE(user_id, name) |
| 388 |
|
| 389 |
### ssh_keys |
| 390 |
SSH public keys for git push access. |
| 391 |
|
| 392 |
- **FK:** user_id → users CASCADE |
| 393 |
- **Constraint:** UNIQUE(user_id, fingerprint) |
| 394 |
|
| 395 |
### issues |
| 396 |
Lightweight issue tracker per repo. Sequential `number` per repo. |
| 397 |
|
| 398 |
- **FK:** repo_id → git_repos CASCADE, author_user_id → users CASCADE |
| 399 |
- **Constraint:** UNIQUE(repo_id, number) |
| 400 |
- **Indexed on:** (repo_id, status), author |
| 401 |
|
| 402 |
### issue_comments |
| 403 |
- **FK:** issue_id → issues CASCADE, author_user_id → users CASCADE |
| 404 |
|
| 405 |
### issue_labels |
| 406 |
Per-repo label definitions with color. |
| 407 |
|
| 408 |
- **FK:** repo_id → git_repos CASCADE |
| 409 |
- **Constraint:** UNIQUE(repo_id, name) |
| 410 |
|
| 411 |
### issue_label_assignments |
| 412 |
- **PK:** (issue_id, label_id), both CASCADE |
| 413 |
|
| 414 |
--- |
| 415 |
|
| 416 |
## OTA Updates |
| 417 |
|
| 418 |
### ota_releases |
| 419 |
App versions published for over-the-air updates (Tauri-compatible protocol). |
| 420 |
|
| 421 |
- **FK:** app_id → sync_apps CASCADE |
| 422 |
- **Constraint:** UNIQUE(app_id, version) |
| 423 |
|
| 424 |
### ota_artifacts |
| 425 |
Binary artifacts per release. One per target/arch combination. |
| 426 |
|
| 427 |
- **FK:** release_id → ota_releases CASCADE |
| 428 |
- **Constraint:** UNIQUE(release_id, target, arch) |
| 429 |
- **Key columns:** s3_key, file_size |
| 430 |
|
| 431 |
### ota_build_configs |
| 432 |
Automated build configuration per app. |
| 433 |
|
| 434 |
- **FK:** app_id → sync_apps CASCADE (UNIQUE), repo_id → git_repos CASCADE |
| 435 |
- **Key columns:** build_command, artifact_path, signing_key_path, targets TEXT[], enabled |
| 436 |
|
| 437 |
### ota_builds |
| 438 |
Individual build runs. Status lifecycle: pending → building → succeeded / failed. |
| 439 |
|
| 440 |
- **FK:** config_id → ota_build_configs CASCADE, release_id → ota_releases SET NULL |
| 441 |
|
| 442 |
--- |
| 443 |
|
| 444 |
## Remaining Tables |
| 445 |
|
| 446 |
### custom_domains |
| 447 |
Creator vanity domains. Verified via DNS TXT record. |
| 448 |
|
| 449 |
- **FK:** user_id → users CASCADE |
| 450 |
- **Key columns:** domain UNIQUE, verified, verification_token |
| 451 |
|
| 452 |
### oauth_authorization_codes |
| 453 |
OAuth PKCE codes for SyncKit SDK clients. Short-lived (5 min), single-use. |
| 454 |
|
| 455 |
- **FK:** app_id → sync_apps CASCADE, user_id → users CASCADE |
| 456 |
- **Key columns:** code UNIQUE, code_challenge, redirect_uri, expires_at, used_at |
| 457 |
|
| 458 |
### creator_waves / creator_waitlist / invite_codes |
| 459 |
Creator onboarding pipeline: waves (batches), waitlist (applications), invite codes (referrals). |
| 460 |
|
| 461 |
- creator_waves: wave_number UNIQUE |
| 462 |
- creator_waitlist: user_id → users CASCADE (UNIQUE), wave_id → creator_waves SET NULL |
| 463 |
- invite_codes: creator_id → users CASCADE, code UNIQUE |
| 464 |
|
| 465 |
### reports |
| 466 |
User abuse reports. Status: open → resolved/dismissed. |
| 467 |
|
| 468 |
### media_files |
| 469 |
User media library for embedding in markdown content. S3-backed. |
| 470 |
|
| 471 |
- **FK:** user_id → users CASCADE |
| 472 |
- **Constraint:** UNIQUE(user_id, folder, filename) |
| 473 |
|
| 474 |
### import_jobs |
| 475 |
Bulk import from external platforms (Patreon, Ko-fi, Gumroad). Tracks progress rows. |
| 476 |
|
| 477 |
### project_categories |
| 478 |
Taxonomy for project categorization. Referenced by projects.category_id. |
| 479 |
|
| 480 |
--- |
| 481 |
|
| 482 |
## Cascade Summary |
| 483 |
|
| 484 |
**CASCADE (delete parent → delete children):** Most FK relationships. Deleting a user cascades to all their projects, items, content, sync data, sessions, keys, etc. |
| 485 |
|
| 486 |
**SET NULL (delete parent → null the FK):** Used where the child record should survive: transactions keep seller/item info (denormalized title/username), sync_apps keep project/item links optional, git repos keep project association optional. |
| 487 |
|
| 488 |
**RESTRICT (prevent parent delete):** subscription_tiers — cannot delete a tier that has active subscribers. |
| 489 |
|
| 490 |
## Search Infrastructure |
| 491 |
|
| 492 |
|
| 493 |
|
| 494 |
| items | tsvector GIN | title + description + body | |
| 495 |
| items | trigram GIN | title, description (separate) | |
| 496 |
| projects | trigram GIN | title, description (separate) | |
| 497 |
| tags | trigram GIN | name | |
| 498 |
|
| 499 |
All trigram indexes use `gin_trgm_ops` from the `pg_trgm` extension. |
| 500 |
|
| 501 |
## Key Paths |
| 502 |
|
| 503 |
- `migrations/` — numbered SQL files, applied in order |
| 504 |
- `src/db/` — query functions grouped by domain |
| 505 |
- `src/models/` — Rust structs matching table schemas |
| 506 |
|