Templates
Template versioning
How schema_version works, how Universal Controller MIDI migrates older templates, and why your v1 files still load on the latest build.
Updated
The schema_version field is non-negotiable. It tells the app which migration path to apply on load and gates marketplace uploads. Today's value is 2. Older v1 files still load — they get migrated in place on next save — but the marketplace only accepts current-schema uploads.
Treat schema_version like a database migration number: incremented exactly once when the shape of stored data changes, never edited by hand, never skipped. The whole system rests on it being correct.
Why a schema version at all
Without a version, every schema change is a breaking change. With one, the loader can detect 1, run a migration, and hand the app a 2-shaped object. Older files keep working — newer features stay accessible.
The cost of bumping is one switch statement in the loader. The cost of not bumping is angry users with broken libraries after every update. Version your data.
The v1 → v2 migration matrix
Here's every field that changed between schema versions, what it was, what it is, and how the migration fills the gap:
| Field | v1 shape | v2 shape | Migration rule |
|---|---|---|---|
schema_version | 1 | 2 | Bumped on first save |
poll_hz | — | 30–250 | Default 100 |
deadzone | optional | optional | Default 0.05 if missing |
left_stick_corners | { notes: [...] } | { enabled, n, notes, r_enter, r_exit } | enabled: true, n from notes length, hysteresis defaults 0.9 / 0.7 |
right_stick_corners | — | same as left | enabled: false |
haptics | { l2, r2 } | split fields | l2_haptic_effect + r2_haptic_effect |
l2_haptic_effect | — | string|null | From haptics.l2 or null |
r2_haptic_effect | — | string|null | From haptics.r2 or null |
osc | — | object | enabled: false |
touchpad | — | object | enabled: false |
What changed in plain English
- Stick corners — v1 had only
left_stick_corners.notes. v2 addsn,r_enter,r_exit, andenabled. - Haptics — v1 had a single
hapticsobject. v2 splits intol2_haptic_effectandr2_haptic_effect. - OSC — v1 had no OSC at all. v2 introduces the
oscblock. - Poll rate —
poll_hzwas fixed at 60 in v1; v2 lets you override per-template. - Touchpad — v1 routed touchpad through axes implicitly; v2 gives it a dedicated block.
Migration is automatic
When the app loads a v1 file it rewrites it in place to v2 on next save. You don't have to do anything. The migration is lossless — every v1 field has a v2 equivalent.
{
"name": "Old File",
"schema_version": 1,
"midi_channel": 0,
"buttons": { "0": 36 },
"left_stick_corners": { "notes": [60, 62, 64, 65, 67, 69, 71, 72] },
"haptics": { "l2": "trigger_resist", "r2": null }
} ...becomes...
{
"name": "Old File",
"schema_version": 2,
"midi_channel": 0,
"deadzone": 0.05,
"poll_hz": 100,
"buttons": { "0": 36 },
"left_stick_corners": {
"enabled": true,
"n": 8,
"notes": [60, 62, 64, 65, 67, 69, 71, 72],
"r_enter": 0.9,
"r_exit": 0.7
},
"right_stick_corners": { "enabled": false },
"touchpad": { "enabled": false },
"osc": { "enabled": false },
"l2_haptic_effect": "trigger_resist",
"r2_haptic_effect": null
} Forward compatibility
If a future v3 ships and you open a v3 file on an older v2 build, the app refuses to load it rather than guess. The status strip shows a clear schema too new warning. Update the app, the template loads. Forward-incompatible failures are loud on purpose — silently dropping fields you don't understand corrupts user libraries.
Never hand-edit schema_version upward without adding the new fields. The loader trusts the number — set it to 2 on a v1-shaped file and corners will silently break.
If you maintain a personal library of templates across multiple machines, commit them to a private git repo. The schema_version + migration model means diffs stay clean across app updates — your library survives every version bump.
Marketplace and versioning
The marketplace only accepts uploads with the current schema_version. v1 files are migrated client-side before upload, so authors don't have to think about it. The Supabase bridge_presets row stores the migrated v2 JSON, never the original v1 — so the day v3 lands, the marketplace bulk-migrates everything on the server side and you're already current. See share to the marketplace.
Real-world template patterns around versioning
- Legacy library upgrade — drop your v1 folder into Settings → Reveal library, the app migrates everything on first load.
- Git-tracked personal library — commit the JSON after migration, diff future schema bumps cleanly.
- Marketplace re-share — if you find your own v1 template floating around online, re-import locally to v2, then re-upload with the current schema.
- Cross-version testing — when authoring a template for marketplace, open it on the oldest supported app version to verify the migration is symmetric.
- Pinning a schema — never edit
schema_versiondownward. To roll back, restore from backup, don't manually decrement.