Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions supabase/migrations/20260821170000_create_music_generations.sql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
-- Create music_generations: songs generated with MiniMax Music 3 on fal.ai
-- (recoupable/chat#1992, contract: recoupable/docs#308).
--
-- Nothing in this schema tracks generated media today, so every column is new
-- rather than an extension of an existing shape.
--
-- Why the row is also the run record: a generation takes roughly one to two
-- minutes, far past a request budget, so POST /api/music inserts a pending row
-- and hands the id to a Vercel Workflow. Same pattern as playcount_snapshots
-- (20260610010000) - the API reads the row, never the Workflow API.
--
-- Deliberately narrow. Anything another system already knows is not stored
-- here: generation parameters ride along as workflow arguments, the seed and
-- the step timeline are readable from fal_request_id and workflow_run_id, and
-- credits are accounted in usage_events. What stays is what the gallery has to
-- render without making a call per row.
--
-- Scope needs no second column. Organizations are accounts in this schema, so
-- an organization's song is one whose account_id is that organization; the
-- membership join tables already say which accounts are organizations.

CREATE TABLE IF NOT EXISTS public.music_generations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- The owning account, after the standard account_id override resolves.
-- A person or an organization; nothing here needs to know which.
account_id UUID NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
-- TEXT + CHECK, not a Postgres enum: only two enum types exist across all
-- of these migrations and both are legacy.
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
-- Provenance for immutable content. A song made by one model has to stay
-- attributable once a second model exists, and it cannot be backfilled.
model TEXT NOT NULL DEFAULT 'minimax/music-3',
prompt TEXT NOT NULL,
lyrics TEXT NOT NULL,
-- Actual length, reported by fal. Rendered on every gallery card, so it is
-- stored rather than fetched per row.
duration_seconds NUMERIC CHECK (duration_seconds IS NULL OR duration_seconds > 0),
-- Key inside the public-uploads bucket, once the audio is mirrored. NULL
-- until completed. UNIQUE because two rows pointing at one object would
-- make deletion unsafe.
storage_key TEXT UNIQUE,
-- Handles to the two external systems this generation touches: fal for the
-- request itself, Vercel Workflow for the run that drove it. Different
-- systems answer different questions, so both are kept.
fal_request_id TEXT,
workflow_run_id TEXT,
-- Why a generation failed, in terms a user can act on. The one thing the
-- workflow cannot answer cheaply: the gallery lists failures and cannot
-- make a call per row, and a failed row with no reason is a dead end.
error_message TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);

-- The gallery read: one account's generations, newest first.
CREATE INDEX IF NOT EXISTS music_generations_account_created_idx
ON public.music_generations (account_id, created_at DESC);

-- Sweeping for work still in flight.
CREATE INDEX IF NOT EXISTS music_generations_status_created_idx
ON public.music_generations (status, created_at DESC);

-- CREATE TRIGGER has no IF NOT EXISTS, so re-applying this file would fail
-- here even though every statement above is idempotent.
DROP TRIGGER IF EXISTS set_updated_at ON public.music_generations;
CREATE TRIGGER set_updated_at
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
BEFORE UPDATE ON public.music_generations
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

-- RLS on with zero policies: denies anon and authenticated outright while
-- service_role, which is how the API reads and writes, bypasses it. These rows
-- hold user-authored prompts and lyrics plus a storage key, so leaving the
-- table reachable through PostgREST with the anon key would expose one
-- account's songs to any other.
ALTER TABLE public.music_generations ENABLE ROW LEVEL SECURITY;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
-- Raise the public-uploads size limit from 25 MiB to 64 MiB so generated audio
-- fits (recoupable/chat#1992).
--
-- MiniMax Music 3 returns 44.1 kHz 16-bit stereo WAV, about 10.6 MB per
-- minute. Against the 25 MiB limit set in 20260508151035 that caps a mirrored
-- song at roughly 148 seconds, while the API accepts a requested duration of
-- up to 300. Without this, a long generation renders on fal, is charged for,
-- and then fails at the upload step.
--
-- 64 MiB is sized to the longest song we accept (300 seconds is about 50.5
-- MiB) and no further, rather than a round 100. The limit is per bucket, not
-- per MIME type, so every raise also raises the ceiling for the images, PDFs
-- and CSVs that share this bucket - keeping the number tight keeps that blast
-- radius small. A separate audio-only bucket would scope it exactly, at the
-- cost of a second bucket, its own keys and a second upload path; not worth it
-- for a 39 MiB difference on an API-gated bucket.
--
-- allowed_mime_types is untouched: audio/wav and audio/mpeg were permitted
-- from the start.
--
-- Idempotent: safe to re-apply.

update storage.buckets
set file_size_limit = 67108864 -- 64 MiB
where id = 'public-uploads'
and (file_size_limit is null or file_size_limit < 67108864);