Who played each performance, and when to the millisecond. Owner: src/listening-store.ts writes both (for src/listening.ts, which takes the player from the session cookie, never from the request's body); src/data.ts and src/activity.ts read them.
What is identifying here, and why (migrations/CLAUDE.md): a performance names the account that played it, when one was signed in, so a listener can find their own takes ("recently played", their profile) and everyone can see who played what (the activity feed, the data tab). Like every row it is public. A take outlives its player's account: deleting the account leaves the take, anonymous (ON DELETE SET NULL). Signed out, a take has no player, as every take did before this migration.
A performance of a listener's song is recorded too, its song being
"listener:<id>" (the id convention the MCPs and the page use); 0003's
CHECK on song (3 to 129 characters) already admits it.
When the take was stored, milliseconds since the epoch, UTC. Takes from
before this migration kept only their day: they are dated noon UTC of
it, which orders them correctly by day and says nothing false about the
hour to anyone who reads day beside it. SQLite's ALTER TABLE cannot add
a column that is NOT NULL without a default, so the column allows NULL,
and none stays: the backfill below dates every take stored before, and
the trigger every take the Worker live during the deploy stores without
it (the deploy migrates before it publishes, worker/migrations/CLAUDE.md).
29ALTER TABLE performances ADD COLUMN recorded_at INTEGER CHECK (recorded_at IS NULL OR recorded_at > 0); 30UPDATE performances SET recorded_at = CAST(strftime('%s', day || ' 12:00:00') AS INTEGER) * 1000 WHERE recorded_at IS NULL; 31CREATE TRIGGER performances_dated AFTER INSERT ON performances WHEN NEW.recorded_at IS NULL 32BEGIN 33 UPDATE performances SET recorded_at = CAST(strftime('%s', NEW.day || ' 12:00:00') AS INTEGER) * 1000 WHERE id = NEW.id; 34END;