Understanding the execution model before writing code will save you from confusion later. Plugins are not JavaScript modules injected into the internote page — they are isolated web applications that communicate with the internote runtime through a restricted channel.
The Sandbox
Every plugin runs in a sandboxed <iframe>. The frame has these restrictions applied at the browser level:
- No same-origin access. The plugin cannot read or write the parent page's DOM,
localStorage, sessionStorage, cookies, or IndexedDB. - No top-level navigation. The plugin cannot redirect the browser or open popups.
- No form submission. Plugins cannot exfiltrate data through form targets.
- Script execution is permitted. Your plugin JavaScript runs normally, including
fetch, WebSocket, requestAnimationFrame, and any third-party library you bundle.
The sandbox attribute applied to the frame is:
sandbox="allow-scripts allow-pointer-lock"
allow-pointer-lock is included to support plugins that use the Pointer Lock API for mouse-capture interactions such as 3D navigators or drawing tools.
External Requests
Plugins are permitted to make fetch requests to external servers. If your plugin sends data to a remote endpoint, this must be disclosed in your manifest's permissions field and in your plugin description. Undisclosed external requests will cause your plugin to be rejected during review.
Communication
All state exchanged between the internote and the plugin flows through postMessage. The Plugin SDK wraps this protocol — you do not call postMessage directly.
Messages the runtime sends to the plugin:
- init — the plugin has loaded; delivers initial attrs, dimensions, and animation state
- resize — the element has been resized by the internote layout
- attrs-update — one or more parameter-driven attr values changed
- animation — the animation timeline state changed (time, pause state, cut index)
- pointer — a pointer event forwarded from the host page
- keyboard — a keyboard event forwarded from the host page
- unload — the plugin element is being removed
Messages the plugin sends to the runtime:
- ready — the plugin has painted its first frame and is ready to display
- storage.get / storage.set / storage.delete / storage.clear — storage requests and responses
- error — an unhandled error in the plugin (shown as an error state in the editor)
Execution Lifecycle
1. Educator inserts a plugin element in Chalk
2. Runtime fetches the plugin manifest from the registry
3. Runtime validates the element's attrs against the manifest schema
4. Runtime creates a sandboxed iframe and points it at the plugin entry URL
5. Plugin JS executes; SDK calls your onInitialize handler with attrs and dimensions
6. Plugin renders its initial frame and calls internote.ready()
7. Runtime reveals the frame to the student
─────────────────────────────────────────────────────────────
8. Student views the internote
9. Parameter sliders, animation timeline, and interactions send messages to the plugin
10. Plugin responds and updates its render
─────────────────────────────────────────────────────────────
11. Student navigates away; runtime sends the unload message
12. Plugin's onUnload handler runs
13. Runtime tears down the iframe
Rendering
The plugin frame is a blank HTML document. You control the entire document — its root element, all styling, and everything rendered. The frame is sized to match the size attribute declared in the plugin's Chalk element. When the internote layout changes the runtime sends a resize message.
There is no prescribed rendering method. You can render using:
- HTML and CSS — direct DOM manipulation or a framework like React or Svelte
- Canvas 2D — create a
<canvas> and draw to its CanvasRenderingContext2D - WebGL — full WebGL or a library like Three.js or PixiJS
- SVG — inline SVG manipulated via the DOM
Heavy computation such as physics simulation steps or vector field calculations can be offloaded to a Worker spawned from within your plugin. Web Workers are permitted inside the sandbox.
The sandbox does not support SharedArrayBuffer or Atomics because those require Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers that the host internote page does not set. Design multi-threaded data sharing around postMessage from within your Worker instead.
Integration Points
Plugins are not standalone — they integrate with the internote's live systems through the SDK:
- Parameter sliders placed in the same detail panel feed resolved numeric values to your plugin whenever the student moves a slider, enabling live reactivity without any custom infrastructure
- The animation timeline delivers its state — elapsed time, pause status, cut index, and restart counter — to every plugin in the scene, so your rendering can stay in sync with
cue{} and step-through navigation - Storage persists per-user, per-internote data so students can return to where they left off
- Interaction forwarding relays pointer and keyboard events from the host page when your frame does not have focus
Each of these is documented separately in the SDK Reference.