One saved jam. Immutable once written — there is no edit path, only save and delete.
any
Any valid record key
Properties
audio
string
Required
Repo-relative path to the recording, always nodes/jam-capture/sessions/<id>/audio.webm. The file is GITIGNORED, so this path can point at nothing on a fresh clone — the page renders such a row with its text intact and no player rather than hiding it.
maxLength: 300 bytescheat
array
of
ref
#cheatRow
Optional
The cheat sheet: each distinct chord name once, in order of first appearance, with how many times it recurred. The shape a person actually wants when reopening a jam — the chord changes, without the timeline.
chords
array
of
ref
#chord
Optional
Notes struck within 180ms of each other, clustered and named. Derived at STOP by the page, then frozen into the row: re-deriving on read would let a change to the naming table silently rewrite what Jake played last month.
createdAt
string
datetime
Required
When the jam was SAVED, not when recording started — the two differ by however long the name box sat open. ISO-8601. The three migrated rows carry the save-time epoch the old server.mjs stored as a number, converted; nothing else was reconstructible.
maxLength: 64 bytesdurationMs
integer
Required
How long the recording ran, measured by the page's own clock (performance.now deltas), not read back off the audio file. A jam whose audio failed to flush would still carry a duration here.
minimum: 0id
string
Required
The jam's id and its directory name under sessions/: "jam-" + the save time in base 36 + a short random tail, so ids sort chronologically as plain strings. Minted server-side; the browser never picks one.
maxLength: 64 bytesnoteEvents
array
of
ref
#noteEvent
Optional
Every MIDI note struck during the jam, in the order they were released. Raw capture — `chords` and `cheat` are both derived from this array and nothing else.
title
string
Required
What Jake typed into the name box when he saved. Free text, and "untitled jam" when he saved without typing — which is what all three of the original 2026-08-12 jams did, so an untitled row is normal and not a defect.
maxLength: 200 bytestranscript
string
Optional
The rough lyrics: every final Web Speech segment joined with spaces. Machine transcription of singing, so it is wrong often and interestingly — it is kept as a SEARCH INDEX for finding a jam again ("where was that one about the tractor"), never as a record of what was sung. Empty when the browser had no speech recognition or the mic caught nothing.
transcriptSegs
array
of
ref
#seg
Optional
The same transcript, still cut into the segments the recognizer emitted, each stamped with its offset into the jam. This is what makes the words seekable against the audio; the joined `transcript` above is the flattening of it.
View raw schema
{
"key": "any",
"type": "record",
"record": {
"type": "object",
"required": [
"id",
"title",
"createdAt",
"durationMs",
"audio"
],
"properties": {
"id": {
"type": "string",
"maxLength": 64,
"description": "The jam's id and its directory name under sessions/: \"jam-\" + the save time in base 36 + a short random tail, so ids sort chronologically as plain strings. Minted server-side; the browser never picks one."
},
"audio": {
"type": "string",
"maxLength": 300,
"description": "Repo-relative path to the recording, always nodes/jam-capture/sessions/<id>/audio.webm. The file is GITIGNORED, so this path can point at nothing on a fresh clone — the page renders such a row with its text intact and no player rather than hiding it."
},
"cheat": {
"type": "array",
"items": {
"ref": "#cheatRow",
"type": "ref"
},
"description": "The cheat sheet: each distinct chord name once, in order of first appearance, with how many times it recurred. The shape a person actually wants when reopening a jam — the chord changes, without the timeline."
},
"title": {
"type": "string",
"maxLength": 200,
"description": "What Jake typed into the name box when he saved. Free text, and \"untitled jam\" when he saved without typing — which is what all three of the original 2026-08-12 jams did, so an untitled row is normal and not a defect."
},
"chords": {
"type": "array",
"items": {
"ref": "#chord",
"type": "ref"
},
"description": "Notes struck within 180ms of each other, clustered and named. Derived at STOP by the page, then frozen into the row: re-deriving on read would let a change to the naming table silently rewrite what Jake played last month."
},
"createdAt": {
"type": "string",
"format": "datetime",
"maxLength": 64,
"description": "When the jam was SAVED, not when recording started — the two differ by however long the name box sat open. ISO-8601. The three migrated rows carry the save-time epoch the old server.mjs stored as a number, converted; nothing else was reconstructible."
},
"durationMs": {
"type": "integer",
"minimum": 0,
"description": "How long the recording ran, measured by the page's own clock (performance.now deltas), not read back off the audio file. A jam whose audio failed to flush would still carry a duration here."
},
"noteEvents": {
"type": "array",
"items": {
"ref": "#noteEvent",
"type": "ref"
},
"description": "Every MIDI note struck during the jam, in the order they were released. Raw capture — `chords` and `cheat` are both derived from this array and nothing else."
},
"transcript": {
"type": "string",
"description": "The rough lyrics: every final Web Speech segment joined with spaces. Machine transcription of singing, so it is wrong often and interestingly — it is kept as a SEARCH INDEX for finding a jam again (\"where was that one about the tractor\"), never as a record of what was sung. Empty when the browser had no speech recognition or the mic caught nothing."
},
"transcriptSegs": {
"type": "array",
"items": {
"ref": "#seg",
"type": "ref"
},
"description": "The same transcript, still cut into the segments the recognizer emitted, each stamped with its offset into the jam. This is what makes the words seekable against the audio; the joined `transcript` above is the flattening of it."
}
}
},
"description": "One saved jam. Immutable once written — there is no edit path, only save and delete."
}