Skip to main content

max / makenotwork

Restore the 29 applied migrations the exorcise sweep rewrote sqlx checksums a migration's whole file and refuses one whose bytes changed after it was applied. 562950d4 edited comments in 29 migrations prod had already run, and converted 019 from CRLF to LF on top of that, so migration_dry_run has failed on 'migration 19 previously applied but modified' and no server deploy has been possible since 2026-07-27. Restored to their pre-sweep bytes. Nothing else touched them in between, so this is an exact revert of that commit's damage to migrations/ and nothing more. An applied migration is immutable, comments included.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-30 21:56 UTC
Signed with PGP, not checked
Commit: 1c6b695de357c5cd0366f563eac0bd9f436f896b
Parent: a3741e2
29 files changed, +98 insertions, -98 deletions
@@ -1,55 +1,55 @@
1 - -- Unified promo_codes table replacing discount_codes + download_codes.
2 - -- Supports three code purposes:
3 - -- discount percentage or fixed price reduction
4 - -- free_access, grants free access to item (replaces download_codes)
5 - -- free_trial N days free on a subscription tier
6 -
7 - CREATE TABLE promo_codes (
8 - id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
9 - creator_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
10 - code TEXT NOT NULL,
11 - code_purpose TEXT NOT NULL CHECK (code_purpose IN ('discount', 'free_access', 'free_trial')),
12 - -- Discount fields (required when purpose = 'discount')
13 - discount_type TEXT CHECK (discount_type IN ('percentage', 'fixed')),
14 - discount_value INT,
15 - min_price_cents INT NOT NULL DEFAULT 0,
16 - -- Trial fields (required when purpose = 'free_trial')
17 - trial_days INT,
18 - -- Scope
19 - item_id UUID REFERENCES items(id) ON DELETE CASCADE,
20 - project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
21 - tier_id UUID REFERENCES subscription_tiers(id) ON DELETE CASCADE,
22 - -- Usage
23 - max_uses INT,
24 - use_count INT NOT NULL DEFAULT 0,
25 - expires_at TIMESTAMPTZ,
26 - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
27 - -- Integrity
28 - CONSTRAINT chk_discount_fields CHECK (
29 - code_purpose != 'discount' OR (discount_type IS NOT NULL AND discount_value > 0)
30 - ),
31 - CONSTRAINT chk_trial_fields CHECK (
32 - code_purpose != 'free_trial' OR (trial_days IS NOT NULL AND trial_days > 0)
33 - )
34 - );
35 -
36 - CREATE UNIQUE INDEX idx_promo_codes_creator_code ON promo_codes(creator_id, upper(code));
37 - CREATE INDEX idx_promo_codes_item ON promo_codes(item_id);
38 - CREATE INDEX idx_promo_codes_project ON promo_codes(project_id);
39 - CREATE INDEX idx_promo_codes_tier ON promo_codes(tier_id);
40 -
41 - -- Migrate existing data
42 - INSERT INTO promo_codes (id, creator_id, code, code_purpose, discount_type, discount_value,
43 - min_price_cents, item_id, project_id, max_uses, use_count, expires_at, created_at)
44 - SELECT id, seller_id, code, 'discount', discount_type, discount_value,
45 - min_price_cents, item_id, project_id, max_uses, use_count, expires_at, created_at
46 - FROM discount_codes;
47 -
48 - INSERT INTO promo_codes (id, creator_id, code, code_purpose, item_id, max_uses,
49 - use_count, expires_at, created_at)
50 - SELECT id, created_by_id, code, 'free_access', item_id, max_uses,
51 - use_count, expires_at, created_at
52 - FROM download_codes;
53 -
54 - DROP TABLE download_codes;
55 - DROP TABLE discount_codes;
1 + -- Unified promo_codes table replacing discount_codes + download_codes.
2 + -- Supports three code purposes:
3 + -- discount — percentage or fixed price reduction
4 + -- free_access — grants free access to item (replaces download_codes)
5 + -- free_trial — N days free on a subscription tier
6 +
7 + CREATE TABLE promo_codes (
8 + id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
9 + creator_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
10 + code TEXT NOT NULL,
11 + code_purpose TEXT NOT NULL CHECK (code_purpose IN ('discount', 'free_access', 'free_trial')),
12 + -- Discount fields (required when purpose = 'discount')
13 + discount_type TEXT CHECK (discount_type IN ('percentage', 'fixed')),
14 + discount_value INT,
15 + min_price_cents INT NOT NULL DEFAULT 0,
16 + -- Trial fields (required when purpose = 'free_trial')
17 + trial_days INT,
18 + -- Scope
19 + item_id UUID REFERENCES items(id) ON DELETE CASCADE,
20 + project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
21 + tier_id UUID REFERENCES subscription_tiers(id) ON DELETE CASCADE,
22 + -- Usage
23 + max_uses INT,
24 + use_count INT NOT NULL DEFAULT 0,
25 + expires_at TIMESTAMPTZ,
26 + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
27 + -- Integrity
28 + CONSTRAINT chk_discount_fields CHECK (
29 + code_purpose != 'discount' OR (discount_type IS NOT NULL AND discount_value > 0)
30 + ),
31 + CONSTRAINT chk_trial_fields CHECK (
32 + code_purpose != 'free_trial' OR (trial_days IS NOT NULL AND trial_days > 0)
33 + )
34 + );
35 +
36 + CREATE UNIQUE INDEX idx_promo_codes_creator_code ON promo_codes(creator_id, upper(code));
37 + CREATE INDEX idx_promo_codes_item ON promo_codes(item_id);
38 + CREATE INDEX idx_promo_codes_project ON promo_codes(project_id);
39 + CREATE INDEX idx_promo_codes_tier ON promo_codes(tier_id);
40 +
41 + -- Migrate existing data
42 + INSERT INTO promo_codes (id, creator_id, code, code_purpose, discount_type, discount_value,
43 + min_price_cents, item_id, project_id, max_uses, use_count, expires_at, created_at)
44 + SELECT id, seller_id, code, 'discount', discount_type, discount_value,
45 + min_price_cents, item_id, project_id, max_uses, use_count, expires_at, created_at
46 + FROM discount_codes;
47 +
48 + INSERT INTO promo_codes (id, creator_id, code, code_purpose, item_id, max_uses,
49 + use_count, expires_at, created_at)
50 + SELECT id, created_by_id, code, 'free_access', item_id, max_uses,
51 + use_count, expires_at, created_at
52 + FROM download_codes;
53 +
54 + DROP TABLE download_codes;
55 + DROP TABLE discount_codes;
@@ -3,7 +3,7 @@
3 3 ALTER TABLE users ADD COLUMN max_file_override_bytes BIGINT;
4 4 ALTER TABLE users ADD COLUMN grandfathered_until TIMESTAMPTZ;
5 5
6 - -- File sizes on items (audio + cover, versions already have file_size_bytes)
6 + -- File sizes on items (audio + cover — versions already have file_size_bytes)
7 7 ALTER TABLE items ADD COLUMN audio_file_size_bytes BIGINT;
8 8 ALTER TABLE items ADD COLUMN cover_file_size_bytes BIGINT;
9 9
@@ -1,6 +1,6 @@
1 1 -- Remove download fingerprinting and streaming session tracking tables.
2 - -- These tables were created in migration 051 but never populated. The
3 - -- write paths were never implemented. Shelving the mechanism entirely.
2 + -- These tables were created in migration 051 but never populated —
3 + -- the write paths were never implemented. Shelving the mechanism entirely.
4 4
5 5 DROP TABLE IF EXISTS download_fingerprints;
6 6 DROP TABLE IF EXISTS streaming_sessions;
@@ -1,4 +1,4 @@
1 - -- Fix: batch_id unique index was too restrictive, a single push batch
1 + -- Fix: batch_id unique index was too restrictive — a single push batch
2 2 -- inserts multiple sync_log rows (one per change), all sharing the same
3 3 -- batch_id. Replace the unique index with a regular index used only by
4 4 -- the idempotency MAX(seq) query.
@@ -15,4 +15,4 @@
15 15 WHERE status = 'completed' AND project_id IS NOT NULL;
16 16
17 17 -- Fix: get_bundle_items should exclude soft-deleted items.
18 - -- No schema change needed, fix is in the query.
18 + -- No schema change needed — fix is in the query.
@@ -18,7 +18,7 @@
18 18 DELETE FROM tags;
19 19
20 20 -- ============================================================================
21 - -- 2. Type-level tags (depth 1), not assignable to items
21 + -- 2. Type-level tags (depth 1) — not assignable to items
22 22 -- ============================================================================
23 23
24 24 INSERT INTO tags (name, slug, parent_id, sort_order, path) VALUES
@@ -30,7 +30,7 @@
30 30 ('Education', 'education', NULL, 5, 'education');
31 31
32 32 -- ============================================================================
33 - -- 3. Category-level tags (depth 2), not assignable to items
33 + -- 3. Category-level tags (depth 2) — not assignable to items
34 34 -- ============================================================================
35 35
36 36 -- Audio categories
@@ -68,7 +68,7 @@
68 68 ('Format', 'education.format', (SELECT id FROM tags WHERE slug = 'education'), 1, 'education.format');
69 69
70 70 -- ============================================================================
71 - -- 4. Value-level tags (depth 3). These are assignable to items
71 + -- 4. Value-level tags (depth 3) — these are assignable to items
72 72 -- ============================================================================
73 73
74 74 -- audio.genre.*
@@ -6,6 +6,6 @@
6 6 --
7 7 -- Nullable: not every issue has a corresponding MT thread (e.g., orphan
8 8 -- repos with no `project_id`, or MT client not configured at issue-creation
9 - -- time). The bridge fails open, missing thread doesn't block the issue.
9 + -- time). The bridge fails open — missing thread doesn't block the issue.
10 10
11 11 ALTER TABLE issues ADD COLUMN mt_thread_id UUID;
@@ -10,17 +10,17 @@
10 10 -- + key_cap × $0.02 (per_key mode only))
11 11 --
12 12 -- Knobs the developer sets:
13 - -- - storage_gb_cap total GB of blob storage available to the app
14 - -- - egress_multiple monthly egress quota = storage_gb_cap × this
15 - -- - key_cap only meaningful in per_key enforcement mode
13 + -- - storage_gb_cap — total GB of blob storage available to the app
14 + -- - egress_multiple — monthly egress quota = storage_gb_cap × this
15 + -- - key_cap — only meaningful in per_key enforcement mode
16 16 --
17 17 -- The base fee is pro-rated: developers always pay at least $5/mo, but usage
18 18 -- fees count against it. Once usage exceeds $5, the floor disappears.
19 19 --
20 20 -- Enforcement modes (orthogonal to pricing):
21 - -- - per_key when key_cap reached, new key claims refused; existing keys
21 + -- - per_key — when key_cap reached, new key claims refused; existing keys
22 22 -- keep working until storage/egress caps hit.
23 - -- - app_wide, any cap hit returns quota_exceeded for the whole app.
23 + -- - app_wide — any cap hit returns quota_exceeded for the whole app.
24 24 --
25 25 -- "Key" is a developer-defined opaque unit. The dev's backend calls
26 26 -- claim_key/release_key as their app's logic dictates (user, workspace,
@@ -28,7 +28,7 @@
28 28 --
29 29 -- First-party apps (GO/BB/AF) are marked `is_internal` and bypass billing
30 30 -- entirely. Their end-user billing (formerly app_sync_subscriptions) is
31 - -- removed. Those apps will grow their own subscription code outside SyncKit.
31 + -- removed — those apps will grow their own subscription code outside SyncKit.
32 32
33 33 -- ── sync_apps: billing columns ──
34 34 ALTER TABLE sync_apps ADD COLUMN is_internal BOOLEAN NOT NULL DEFAULT FALSE;
@@ -7,10 +7,10 @@
7 7 --
8 8 -- Two modes (the `enforcement_mode` column drives both pricing and limits):
9 9 --
10 - -- bulk developer sets `storage_gb_cap`. Price = storage_gb_cap × rate.
10 + -- bulk — developer sets `storage_gb_cap`. Price = storage_gb_cap × rate.
11 11 -- When storage fills, the whole app's uploads degrade.
12 12 --
13 - -- per_key developer sets `key_cap` AND `gb_per_key`.
13 + -- per_key — developer sets `key_cap` AND `gb_per_key`.
14 14 -- Price = key_cap × gb_per_key × rate.
15 15 -- Each key gets its own gb_per_key allotment. A full key
16 16 -- degrades only that key; other keys keep working.
@@ -8,7 +8,7 @@
8 8 --
9 9 -- JWT minting now requires a `key` claim (see `synckit_auth.rs`), so every
10 10 -- new blob upload knows its key. Existing sync_blobs rows are first-party
11 - -- internal app data (GO/BB/AF). We don't need to preserve them and a
11 + -- internal app data (GO/BB/AF) — we don't need to preserve them and a
12 12 -- backfill default would not be meaningful, so we drop and recreate.
13 13
14 14 -- ── Per-key live counters ──
@@ -32,7 +32,7 @@
32 32
33 33 -- ── sync_blobs: attribute each blob to a key ──
34 34 -- Drop existing rows (internal-app data only; not preserved across this
35 - -- schema change). The `key` column is NOT NULL with no default, every
35 + -- schema change). The `key` column is NOT NULL with no default — every
36 36 -- future write must supply it.
37 37 DELETE FROM sync_blobs;
38 38 ALTER TABLE sync_blobs ADD COLUMN key TEXT NOT NULL;
@@ -9,7 +9,7 @@
9 9 -- the next billing cycle (no mid-cycle proration surprises).
10 10 --
11 11 -- The pricing model is now formula-driven (see `payments::synckit_app_pricing`)
12 - --, no tier table, the user picks any cap and the server quotes a price.
12 + -- — no tier table, the user picks any cap and the server quotes a price.
13 13 CREATE TABLE app_sync_subscriptions (
14 14 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
15 15 user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
@@ -4,7 +4,7 @@
4 4 -- size could change underfoot (or the HEAD could fail entirely), producing
5 5 -- a wrong decrement and silently corrupting `creator_tiers.storage_used`.
6 6 --
7 - -- NULL means "size not recorded", pre-migration rows fall back to a
7 + -- NULL means "size not recorded" — pre-migration rows fall back to a
8 8 -- best-effort S3 HEAD until the next replace stores a real value.
9 9
10 10 ALTER TABLE projects
@@ -4,7 +4,7 @@
4 4 -- [::1]:PORT/, or localhost:PORT/ regardless of an app's registered
5 5 -- redirect_uris list. That's correct for native (desktop/mobile) apps that
6 6 -- can't reserve known ports, but it widens the attack surface for any
7 - -- web-only app, a phishing URL pointing /oauth/authorize at the attacker's
7 + -- web-only app — a phishing URL pointing /oauth/authorize at the attacker's
8 8 -- loopback can hijack the auth flow even with PKCE in play (attacker
9 9 -- initiates and supplies their own code_verifier).
10 10 --
@@ -1,5 +1,5 @@
1 1 -- At most one running build globally. Enforces what
2 - -- `claim_pending_build`'s NOT EXISTS subquery checks at app level, without
2 + -- `claim_pending_build`'s NOT EXISTS subquery checks at app level — without
3 3 -- this index, two replicas racing through the claim path can each pass the
4 4 -- check and end up with two concurrent running builds. The losing INSERT/
5 5 -- UPDATE surfaces as a 23505 unique violation; `claim_pending_build` swallows
@@ -1,6 +1,6 @@
1 1 -- Mark a session row's role so the "log out everywhere" sweep
2 2 -- (`delete_all_sessions_for_user`) can also catch sessions stuck in the
3 - -- 2FA-pending intermediate state, a phisher who has the password but not
3 + -- 2FA-pending intermediate state — a phisher who has the password but not
4 4 -- the TOTP code holds an authenticated-pending session that previously was
5 5 -- session-storage-only and invisible to user_sessions sweeps.
6 6 ALTER TABLE user_sessions
@@ -1,6 +1,6 @@
1 1 -- Reject negative `duration_seconds` at the DB level. A negative duration cast
2 2 -- to u64 in `routes/storage/downloads.rs::stream_url` becomes ~u64::MAX,
3 - -- producing a presigned URL with a multi-decade expiry, effectively a
3 + -- producing a presigned URL with a multi-decade expiry — effectively a
4 4 -- permanent credential leak for the underlying object. The route now clamps
5 5 -- defensively, but pinning the invariant in the schema means no future
6 6 -- writer (admin tool, bulk import, manual SQL) can reintroduce it.
@@ -3,7 +3,7 @@
3 3 -- The personal RSS feed URL (`/feed/{user_id}?v={version}&sig={hmac}`) is
4 4 -- authenticated by an HMAC over `feed:{user_id}:{version}`. Bumping this column
5 5 -- changes the signed message, so the user's previously-issued feed URL stops
6 - -- verifying, a leaked or compromised feed link can be revoked in isolation
6 + -- verifying — a leaked or compromised feed link can be revoked in isolation
7 7 -- without rotating the global signing secret (which would invalidate EVERY
8 8 -- user's feed at once). Starts at 0; the "Regenerate feed URL" action in
9 9 -- dashboard settings increments it.
@@ -1,5 +1,5 @@
1 1 -- Personal access tokens for git over HTTPS (Basic auth). The plaintext token
2 - -- is shown once at creation and never persisted, only its SHA-256 hex hash is
2 + -- is shown once at creation and never persisted — only its SHA-256 hex hash is
3 3 -- stored. `can_push` gates write (receive-pack) access; `expires_at` NULL means
4 4 -- no expiry. Lookups are by token_hash (UNIQUE → indexed); listing is by user.
5 5 CREATE TABLE IF NOT EXISTS git_access_tokens (