Two integration modes — pick the one that matches your product:
| Mode | Best for | Isolation | Entry point |
|---|---|---|---|
| Iframe | Embedding one specific app with URL-driven state; sandboxed from your page's JS | Full (same-origin or cross-origin) | <iframe src="/online/webapp/{app}"> -- this doc |
| In-page (no iframe) | Running SGApps apps directly inside your page's DOM: multi-window desktop, drop-in alongside your UI, no postMessage round-trip | None (shares your page's globals and styles) | exporter.js + wm.open(...) -- In-page section + Window Manager docs |
Every SGApps web application is available at:
https://sgapps.io/online/webapp/{app-name}Embed with a standard <iframe>:
<iframe
id="sgapps-editor"
src="https://sgapps.io/online/webapp/photo-editor"
width="100%"
height="600"
frameborder="0"
allow="camera; microphone; clipboard-write"
></iframe>allow Attributes| Permission | Needed By |
|---|---|
camera | Recorder |
microphone | Recorder, ChatBox |
clipboard-write | Code Editor, File Manager |
fullscreen | All apps (fullscreen button) |
/online/webapp/{app-name}Opens the app with its default demo arguments (if configured). For Photo Editor this loads a sample image.
/online/webapp/{app-name}/url/{base64-encoded-url}The third segment is the URL base64-encoded with btoa():
var url = "https://example.com/image.jpg";
var src = "https://sgapps.io/online/webapp/photo-editor/url/" + btoa(url);/online/webapp/{app-name}/file/{base64-encoded-path}For authenticated users, opens a file from their GridFS storage.
By default, the app renders with window chrome (title bar, controls). For a cleaner embed, enable embed mode via the WindowSocket API:
socket.fire("webapp::instance::embed-mode", true, function (err, isEmbedded) {
// App is now maximized with no title bar
});This is a compound preset that:
webapp-instance--embed-mode class to the bodyDisable embed mode:
socket.fire("webapp::instance::embed-mode", false, function (err, isEmbedded) {
// Window chrome is restored
});For finer control than the all-or-nothing embed-mode preset, each chromeaspect is exposed on its own WindowSocket channel. Pass a boolean to setthe state, omit it to read the current state. Every endpoint returns theresulting state via cb(null, state). These are composable with embed-mode.
| Channel | Effect |
|---|---|
webapp::instance::window:controls | Show/hide the entire title bar (win.controls(bool)) |
webapp::instance::window:header | Show/hide the header: icon + title + action buttons (win.controlsHeader(bool)) |
webapp::instance::window:header-actions | Show/hide the close/minimize/maximize/fullscreen buttons only, keep the title (win.controlsHeaderActions(bool)) |
webapp::instance::window:footer | Show/hide the footer strip (win.controlsFooter(bool)) |
webapp::instance::window:maximize | Enter fullscreen (true → win.maximize(true)) / exit fullscreen (false → win.restore()) |
Keep title bar, drop only the window buttons:
socket.fire("webapp::instance::window:header-actions", false, function (err, state) {
// close/minimize/maximize/fullscreen icons gone, title still visible
});Toggle fullscreen on demand (no header change):
// Enter fullscreen
socket.fire("webapp::instance::window:maximize", true);
// Exit fullscreen
socket.fire("webapp::instance::window:maximize", false);Read current state without changing it:
socket.fire("webapp::instance::window:footer", null, function (err, isFooterVisible) {
console.log("footer visible:", isFooterVisible);
});Minimal chrome (full canvas, keep the fullscreen escape button):
socket.fire("webapp::instance::window:header", true); // keep header
socket.fire("webapp::instance::window:header-actions", true); // keep buttons
socket.fire("webapp::instance::window:footer", false); // drop footer
socket.fire("webapp::instance::window:maximize", true); // fullscreenThe WindowSocket bridge serializes arguments through its own typed encoder(not postMessage structured clone directly). The encoder understands:
string, number, boolean, bigint, undefined, nullArrayError (name / message / stack)ArrayBuffer (use this for binary payloads)function (callback marshalling with persistent callId)Blob and File instances are not recognized — they fall into the plain-objectbranch, for..in yields no enumerable keys, and the receiver sees {} withno bytes. Convert client-side before fire:
file.arrayBuffer().then(function (buffer) {
socket.fire("webapp::instance::request", "fs:write",
"input://uploads/" + file.name, buffer,
function (err) { /* ... */ }
);
});The receiving handlers (e.g. file-manager's fs:write) wrap incomingArrayBuffers in new Blob([content]) internally, so a plain-string payloadalso works for text content:
socket.fire("webapp::instance::request", "fs:write",
"input://notes/hello.txt", "plain text content",
function (err) { /* ... */ }
);The webapp container auto-resizes to fill the iframe viewport. A polling loop (500ms) adjusts the container height to account for the top bar and footer. For best results, set the iframe to fill its parent:
iframe {
width: 100%;
height: 100vh;
border: none;
}For integrations that need SGApps apps inside your page -- same JS globals, shared stylesheet, zero postMessage latency, multi-window desktop UI -- load exporter.js from the SGApps CDN. It bootstraps the full client runtime (ApplicationPrototype core, module aliases, icons, translator, optional WindowSocket + GridFS) and exposes a global Application(callback) fired as soon as the stack is ready. The script detects its own src at runtime, so RootHost is always correct even when your page lives on a different domain.
<!doctype html>
<html>
<body>
<div id="wm-root" style="position:relative;height:100vh;"></div>
<script src="https://sgapps.io/scripts/modules-webapp/exporter.js"></script>
<script>
Application(function (App) {
// Load the full stack (styles + modules + libs + WindowSocket + fs + i18n).
// Lighter variants exist -- see Window Manager docs.
App["exporter:configuration:full"]().then(function () {
App.require(["wm :: sgapps-webapps.b2b.window-manager"]).then(function (libs) {
// Mount the WM's DOM host wherever you want windows to live.
document.getElementById("wm-root").appendChild(libs.wm.root());
// Open an app directly -- no iframe, no URL, no base64 payload.
libs.wm.open({
run: ["sgapps/applications/code-editor"],
SKIP_APP_LIBRARY: true,
width: 900,
height: 600
});
});
});
});
</script>
</body>
</html>Pass the module path as the first entry of run. The same arguments the URL-mode exposes work here as positional strings:
// Code Editor with a preloaded file
libs.wm.open({
run: ["sgapps/applications/code-editor", "input://notes.md"],
SKIP_APP_LIBRARY: true
});
// Writer with a .docx (binary, read client-side via the SGApps fs)
libs.wm.open({
run: ["sgapps/applications/writer", "input://report.docx"],
SKIP_APP_LIBRARY: true
});
// Photo Editor with a sample image, custom size
libs.wm.open({
run: ["sgapps/applications/photo-editor", "web://images/sample.jpg"],
SKIP_APP_LIBRARY: true,
width: 1200, height: 800
});Paths accepted by apps that take a file:
input://… -- browser-memory sandbox (same as iframe mode; writes to it are visible to every app running in that page)web://… -- same-origin HTTP fetch (web://foo/bar.png → https://sgapps.io/foo/bar.png)https://… URL (CORS applies)In-page mode skips the postMessage relay entirely. The handle returned by wm.open(...) is the live WindowApplication -- you call its methods directly:
var win = libs.wm.open({ run: ["sgapps/applications/code-editor"], SKIP_APP_LIBRARY: true });
// Wait for the app inside the window to be fully constructed.
win.on("api-request::attached", function () {
// Every `api-request::instance:*` event documented in the per-app pages
// works here too -- emit it on the window directly.
win.emit("api-request::instance:active-file:codeEditor:value",
"console.log('hello from in-page');",
function (err) { if (err) console.error(err); }
);
});The event names (active-file:codeEditor:value, fs:write, files:open, …) are identical to the ones listed in each app's docs page under Events Reference -- the only difference is you emit them on the win handle instead of firing them through a WindowSocket relay. Callbacks are the same (err, result) node-style shape.
Sometimes you want an app rendered as a plain DOM subtree -- no WindowManager, no title bar, no window buttons -- but still want the postMessage/WindowSocket API to drive it. That's what the sgapps-webapps.b2b.* aliases below give you: each one is a thin factory producing an ApplicationPrototype whose .node() is a custom-element host containing a single <iframe> pointed at /online/webapp/<slug>. When the embedded app pings back, the factory flips it into embed-mode and resolves a Promise exposing the live WindowSocket client. Zero chrome, zero wm, full API.
<script src="https://sgapps.io/scripts/modules-webapp/exporter.js"></script>
<script>
Application(function (App) {
App["exporter:configuration:full"]().then(function () {
App.require([
"writer :: sgapps-webapps.b2b.editor.writer"
]).then(function (libs) {
var writerApp = libs.writer(); // ApplicationPrototype
document.body.appendChild(writerApp.node()); // mount the host
writerApp.editor().then(function (editor) {
// editor = { node: <iframe>, window: iframeContentWindow,
// client: WindowSocket-Client, visible: fn(state) }
editor.visible(true); // show + require-redraw
// Same `api-request::instance:*` events as iframe mode.
editor.client.fire(
"webapp::instance::request",
"setContent",
"<h1>Hello from an embedded writer</h1>",
function (err) { if (err) console.error(err); }
);
});
});
});
});
</script>The handle shape is identical across every wrapper:
app.node() -- the custom-element host; drop it anywhere in your DOM. Size it with CSS; the iframe fills it 100%.app.editor() -- Promise<{node, window, client, visible}>. Resolves once the WindowSocket handshake lands.editor.client -- the WindowSocket Client; .fire("webapp::instance::request", "<event-name>", args..., cb) works for every event listed in each app's per-app doc page.editor.visible(true | false | null) -- toggles pointer-events + visibility + require-redraw. null keeps the host visible but hides the iframe (useful while the app is still booting).Available aliases:
| Alias | Opens | Per-app events |
|---|---|---|
sgapps-webapps.b2b.editor.writer | /online/webapp/writer | Writer events |
sgapps-webapps.b2b.editor.spreadsheet | /online/webapp/spreadsheet | Spreadsheet events |
sgapps-webapps.b2b.editor.code-editor | /online/webapp/code-editor | Code Editor events |
sgapps-webapps.b2b.editor.photo-editor | /online/webapp/photo-editor | Photo Editor events |
sgapps-webapps.b2b.editor.gcode-editor | /online/webapp/gcode-editor | GCode Editor events |
sgapps-webapps.b2b.viewer.pdf-browser | /online/webapp/pdf-browser | PDF Browser events |
sgapps-webapps.b2b.viewer.markdown-browser | /online/webapp/markdown-browser | Markdown Browser events |
sgapps-webapps.b2b.file-manager | /online/webapp/file-manager | File Manager events |
modules--sgapps-web-apps/editors/writer.js, etc.) are each ~8 lines pulling it in via module.require(["x :: ../_create-webapp-wrapper"]). For a route not on the list, the plain iframe embed + WindowSocket still works -- or ask us to publish a new b2b.* alias for it.wm.open(...) call -- the wrapper is lighter (no window chrome, no window-manager singleton) and keeps the iframe round-trip, so you still get sandboxing. wm.open has no sandboxing (apps share your JS globals) but has zero postMessage overhead and lets you compose a desktop with several SGApps' own apps + your own custom apps in one WindowManager. Pick the wrapper for a single drop-in app embedded as a plain DOM subtree; pick wm.open for a multi-window product.exporter:configuration:full bundles seven independent loaders. If you don't need the WindowSocket bridge (iframe embedding), the GridFS client, or the translator, load only the pieces you use. See Window Manager -- Piecewise loading for the full list and dependency chain.
history.pushState on the iframe URL.wm.open({run:["my-app"]}) with a module you authored), you want zero cross-frame overhead on frequent calls (e.g. a code-editor's fs:write on every keystroke in your UI), or you want to re-skin SGApps' chrome using your page's CSS variables.The apps are hosted on sgapps.io. When embedding in iframe mode from a different domain, the WindowSocket API uses postMessage which works cross-origin -- no special CORS headers are needed for the iframe itself. If you load files from your domain into the app, they must be accessible via a public URL.
In-page mode is also cross-origin friendly: exporter.js loads all its own assets from sgapps.io (CORS enabled on the SGApps CDN), so you can drop the <script src="https://sgapps.io/scripts/modules-webapp/exporter.js"> tag on any domain and it works.
The URL slug is used for iframe mode (/online/webapp/{slug}). The module path is used for in-page mode (wm.open({run:["{path}"]})).
| App Name | URL Slug | In-page Module Path |
|---|---|---|
| Photo Editor | photo-editor | sgapps/applications/photo-editor |
| SVG Editor | svg-editor | sgapps/applications/svg-editor |
| Code Editor | code-editor | sgapps/applications/code-editor |
| Writer | writer | sgapps/applications/writer |
| Spreadsheet | spreadsheet | sgapps/applications/spreadsheet |
| Archive Viewer | archive-viewer | sgapps/applications/archive-viewer |
| Media Player | media-player | sgapps/applications/media-player |
| Recorder | recorder | sgapps/applications/recorder |
| File Manager | file-manager | sgapps/applications/file-manager |
| Presentation | presentation | sgapps/applications/presentation |
| GCode Editor | gcode-editor | sgapps/applications/gcode-editor |
| CNC GCode Simulator | cnc-gcode-simulator | sgapps/applications/cnc-gcode-simulator |
| ChatBox | chatbox | sgapps/applications/chatbox |
| SVFS Manager | svfs-manager | sgapps/applications/fs/svfs-manager |
| SSH Web Client | ssh-web-client | sgapps/applications/ssh-web-client |