Melovas external apps
Developer Guide
Build iframe-ready tools that run on apps.melovas.com, talk to the Melovas editor through
postMessage, and request only the permissions they need.
1. How external apps work
A Melovas external app is a static web app opened inside the main editor in a sandboxed iframe. The main editor controls which app is opened, which origin is trusted, and which capabilities are granted. The external app sends structured messages to the parent editor when it wants to apply notes, control playback, use microphone-led workflows, or play Melovas instrument sounds.
Static app code should not contain secrets. Any private, expensive, or AI-backed work should go through a Melovas backend API.
2. Registry / package entry
The Melovas app registry tells the editor which apps exist and where their hosted iframe entry point lives. For now this can be served by the static host or by the API later.
{
"id": "keyboard-player",
"displayName": "Keyboard Player",
"version": "1.6.0",
"description": "Play Melovas keyboard and synth sounds with your computer keyboard.",
"entryUrl": "https://apps.melovas.com/keyboard-player/1.6.0/index.html",
"focusMode": "exclusive",
"icon": "⌨",
"capabilities": [
"commit-notes",
"request-timeline-playback",
"keyboard-note-preview",
"host-instrument-playback",
"isolated-external-runtime"
],
"messageNamespace": "melovas.external-app"
}
The host will still apply its own app-id based allowlist and capability grants. Declaring a capability in the app metadata does not automatically grant it.
3. Capabilities / permissions
| Capability | Use it when your app needs to... | Main messages/features |
|---|---|---|
commit-notes | Send generated or recorded notes into the Melovas timeline. | melovas.external-app.commit-notes |
timeline-apply | Apply timeline changes. This is broader than note import and should be granted carefully. | commit-notes, project commands where granted |
commit-project-commands | Send structured project-edit commands such as creating tracks or adding blocks. | melovas.external-app.commit-project-commands |
request-timeline-playback | Start, stop, toggle, or restart host timeline playback from inside the iframe. | melovas.external-app.timeline-playback |
host-instrument-playback | Use the main app's real instrument sounds for previews/live performance. | Instrument catalog, note-on/off, instrument actions |
microphone-recording | Record audio from the user's microphone inside the iframe. | Browser microphone APIs inside the app |
voice-to-midi-analysis | Analyse recorded audio and create notes. | App-specific analysis + commit-notes |
computer-keyboard-performance | Capture computer keyboard input for playable pads. | Usually pairs with exclusive focus mode |
virtual-keyboard-performance | Offer clickable/touchable virtual keys or pads. | Usually pairs with host-instrument-playback |
custom-user-layouts | Let users customise layouts inside the external app. | Local app state; no special host message |
pad-action-editing | Let users map pads to notes/chords/instruments. | Local app state + instrument catalog |
isolated-external-runtime | Run as a hosted iframe app rather than as part of the main editor bundle. | Required for hosted external apps |
4. Messaging rules
Every app-to-host message should include the namespace melovas.external-app and a payload appId matching the app currently open in the iframe.
The parent Melovas app validates the iframe window, origin, app identity, and effective capabilities before doing anything.
Melovas appends the parent origin to the iframe URL as melovasHostOrigin and hostOrigin.
Your app should read this value and use it as the targetOrigin for window.parent.postMessage.
const params = new URLSearchParams(window.location.search);
const hostOrigin = params.get('melovasHostOrigin') || params.get('hostOrigin') || '*';
function postHostMessage(type, payload) {
window.parent.postMessage({
namespace: 'melovas.external-app',
type,
payload
}, hostOrigin === '*' ? '*' : new URL(hostOrigin).origin);
}
5. Commit notes to the timeline
Use this when your app has note-like results: hum-to-MIDI output, keyboard-player recordings, melody ideas, etc. Notes are imported as editable Melovas blocks. Keyboard/piano imports become one-shot blocks; synth imports keep their duration.
postHostMessage('melovas.external-app.commit-notes', {
appId: 'my-app-id',
title: 'My App Recording',
notes: [
{
id: 'note-1',
midi: 60,
midiNotes: [60, 64, 67],
startSeconds: 0,
durationSeconds: 1.2,
velocity: 0.9,
selected: true,
instrument: 'keyboard',
label: 'C'
}
]
});
Supported import instruments today are keyboard and synth. Omitted or unknown values default to keyboard/piano.
6. Request host instrument sounds
Apps with host-instrument-playback should request the host instrument catalog first.
The catalog is the contract for what Melovas can play. Do not guess note names, drum IDs,
guitar strings, frets, chord names, or action payloads.
postHostMessage('melovas.external-app.request-instrument-catalog', {
appId: 'my-app-id'
});
The host replies to the iframe with entries for keyboard, synth, drums, and guitar:
{
namespace: 'melovas.external-app',
type: 'melovas.external-app.instrument-catalog',
payload: {
instruments: [
{
instrumentId: 'keyboard',
liveNoteMode: 'one-shot',
noteNames: ['A0', 'As0', 'B0', 'C1', '...C8'],
actions: [{ action: 'play-note', messageType: 'instrument-note-on' }]
},
{
instrumentId: 'synth',
liveNoteMode: 'held',
noteNames: ['A0', 'As0', 'B0', 'C1', '...C8'],
actions: [{ action: 'play-note', messageType: 'instrument-note-on' }]
},
{
instrumentId: 'drums',
liveNoteMode: 'one-shot',
sounds: [{ id: 'Kick', label: 'Kick' }],
actions: [{ action: 'hit', messageType: 'instrument-action' }]
},
{
instrumentId: 'guitar',
liveNoteMode: 'one-shot',
stringIndexRange: [0, 5],
fretRange: [0, 12],
chordNames: ['C', 'Cm', 'C7', 'Cmaj7', '...'],
strumDirections: ['down', 'up'],
actions: [
{ action: 'pluck-string', messageType: 'instrument-action' },
{ action: 'strum-chord', messageType: 'instrument-action' },
{ action: 'strum-voicing', messageType: 'instrument-action' },
{ action: 'start-strum-pattern', messageType: 'instrument-action' },
{ action: 'stop-strum-pattern', messageType: 'instrument-action' }
]
}
]
}
}
The host currently uses Cs4, Ds4, etc. for sharps, not C#4.
instruments comes back empty if your app does not have the host-sound capability.
7. Instrument actions
External apps report musical actions. Melovas owns instrument behaviour. For playable notes, send
instrument-note-on and instrument-note-off; the host decides whether note-off matters.
Keyboard and synth
Use note lifecycle messages for both keyboard and synth. Keyboard is one-shot in the host, so note-off is ignored.
Synth is held, so note-off releases the matching voiceId.
postHostMessage('melovas.external-app.instrument-note-on', {
appId: 'my-app-id',
voiceId: 'pad-a-c4',
instrumentId: 'keyboard', // or 'synth'
noteName: 'C4',
velocity: 0.9
});
postHostMessage('melovas.external-app.instrument-note-off', {
appId: 'my-app-id',
voiceId: 'pad-a-c4',
releaseSeconds: 0.08
});
Drums
Use instrument-action with a soundId from the catalog.
postHostMessage('melovas.external-app.instrument-action', {
appId: 'my-app-id',
instrumentId: 'drums',
action: 'hit',
soundId: 'Kick',
velocity: 0.9
});
Guitar
Guitar strings use Melovas' sample order: 0 is the high/small E string and 5 is the low/thick E string.
Frets are integers from 0 to 12. Direction down means low/thick to high/small;
direction up means high/small to low/thick.
postHostMessage('melovas.external-app.instrument-action', {
appId: 'my-app-id',
instrumentId: 'guitar',
action: 'pluck-string',
stringIndex: 2,
fret: 5,
velocity: 0.9
});
postHostMessage('melovas.external-app.instrument-action', {
appId: 'my-app-id',
instrumentId: 'guitar',
action: 'strum-chord',
chordName: 'C',
direction: 'down',
velocity: 0.9,
spreadSeconds: 0.015
});
postHostMessage('melovas.external-app.instrument-action', {
appId: 'my-app-id',
instrumentId: 'guitar',
action: 'strum-voicing',
strings: [0, 1, 0, 2, 3, 'x'],
direction: 'up',
velocity: 0.9,
spreadSeconds: 0.015
});
A custom strings voicing must contain exactly six values in order
[String0, String1, String2, String3, String4, String5]. Use 'x' to mute a string.
Gated strumming pattern
Use start-strum-pattern when a held key/pad should play a finite down/up pattern. The host schedules
the relative strums, stops pending strums on stop-strum-pattern, and lets already-played strums ring naturally.
If the key stays held, the pattern plays once and then goes silent.
postHostMessage('melovas.external-app.instrument-action', {
appId: 'my-app-id',
instrumentId: 'guitar',
action: 'start-strum-pattern',
voiceId: 'guitar-c-pattern-key-a',
chordName: 'C',
patternName: 'DDUUD',
lengthBeats: 4,
events: [
{ beatOffset: 0, direction: 'down' },
{ beatOffset: 1, direction: 'down' },
{ beatOffset: 1.5, direction: 'up' },
{ beatOffset: 2.5, direction: 'up' },
{ beatOffset: 3, direction: 'down' }
],
velocity: 0.9,
spreadSeconds: 0.015
});
postHostMessage('melovas.external-app.instrument-action', {
appId: 'my-app-id',
instrumentId: 'guitar',
action: 'stop-strum-pattern',
voiceId: 'guitar-c-pattern-key-a'
});
Pattern names should describe the direction sequence where possible, for example DU, DDUUD, or DDUUDU.
8. Timeline playback control
Apps with request-timeline-playback can ask the host transport to start, stop, toggle, or restart.
postHostMessage('melovas.external-app.timeline-playback', {
appId: 'my-app-id',
action: 'start',
volume: 0.35
});
9. Safety and quality checklist
- Serve the app from an origin trusted by Melovas, currently
https://apps.melovas.com. - Declare only the capabilities the app genuinely needs.
- Never store API keys or secrets in static app files.
- Always request the instrument catalog before triggering host sounds.
- For playable key/pad controls, send note-on and note-off; let Melovas decide one-shot vs sustain.
- Use
instrument-actionfor drums and guitar; keeptrigger-instrument-noteonly for old/simple one-shot previews. - Send note-off for any voice IDs that are still active when the user stops playback, changes layout, or closes the app.
- Keep message payloads bounded. Large imports may be rejected by the host.