Skip to main content

max / makenotwork

17.3 KB · 506 lines History Blame Raw
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 | Domain | Tables | Purpose |
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 | Column | Type | Notes |
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 | Column | Type | Notes |
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 | Column | Type | Notes |
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 | Column | Type | Notes |
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 | Column | Type | Notes |
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 | Column | Type | Notes |
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 | Target | Index Type | Columns |
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