Skip to main content

Developers

Build an app that belongs in Sovereign Workspace.

Your app runs in the workspace, talks to it over a small SDK, and matches its light and dark themes automatically. Everything on this page ships today — install two packages from npm and you have a working, native-looking app.

What you're building

A Sovereign Workspace app is an ordinary web app — React, Vue, Svelte, or plain HTML, your choice — that the workspace loads inside a sandboxed <iframe>. You host it and build it however you like. What makes it a workspace app is that it can call the workspace through the SDK and wear the workspace's look.

Because it runs sandboxed, your app can't reach into the workspace directly. Instead it speaks to the host over a small, validated message channel. The @sovereign-workspace/sdk-iframe package hides that channel behind a normal function-call API, so you never write postMessage plumbing yourself.

Through the SDK your app can:

  • Match the theme — read the active light/dark theme and follow changes.
  • Show notifications in the workspace.
  • Talk to other apps over IPC, when both sides consent.
  • Read runtime info — the signed-in user, whether it's the native desktop shell, and more.
  • Fetch its scoped token — a short-lived token the workspace mints for your app at launch.

Quick start

Install the two packages:

npm install @sovereign-workspace/sdk-iframe @sovereign-workspace/design-tokens

Then, from your app's entry point, connect to the workspace, match its theme, and use it:

import { createWorkspaceSdk } from '@sovereign-workspace/sdk-iframe';
import '@sovereign-workspace/design-tokens';

const sdk = createWorkspaceSdk();

// 1. Match the workspace theme on load, and follow every switch.
const applyTheme = (theme) =>
  document.documentElement.setAttribute('data-theme', theme === 'dark' ? 'dark' : '');
applyTheme(await sdk.system.getTheme());
sdk.system.onThemeChange(applyTheme);

// 2. Use the workspace: show a notification.
await sdk.notifications.show({ title: 'Hello', body: 'from my app', variant: 'info' });

// 3. Talk to another app (delivered only on mutual manifest consent).
await sdk.ipc.send('chat', { type: 'greeting', text: 'hi' });

That's a complete, theme-conforming app. Load it in the workspace and it will render in the right theme and switch with the workspace when the user toggles dark mode.

It runs standalone too. Opened outside the workspace (a direct URL, or npm run dev on its own port), createWorkspaceSdk() returns a no-op client: every call resolves to a safe default, and the theme falls back to the operating system's colour-scheme preference. So you can build and test the whole UI before ever embedding it.

The SDK

Create the SDK once and reuse it. Pass your workspace's origin to lock the connection down (recommended in production):

const sdk = createWorkspaceSdk('https://workspace.example.org');
CallWhat it does
system.getTheme()The active theme, 'light' or 'dark'.
system.onThemeChange(fn)Fires with the new theme on every switch. Returns an unsubscribe function.
system.getRuntimeContext()Signed-in user and runtime details.
system.getRuntimeMode()'standalone' when opened outside the workspace.
system.isNativeShell()true in the native desktop shell.
notifications.show(payload)Show a workspace notification.
ipc.send(recipient, data)Send a message to another app (mutual consent required).
ipc.on(recipient, fn)Receive messages addressed to your app.
ext.getToken()The scoped token minted for your app at launch, or null.

The workspace enforces identity and permissions on its side, so the SDK can't be tricked into doing more than your app is allowed to. Your app's identity is stamped by the host on every call — a message payload can never impersonate another app.

Matching the theme

This is the part that makes your app feel native. The @sovereign-workspace/design-tokens package is a single CSS file of the workspace's colours — surfaces, text, borders, accents — for both light and dark mode. Import it, then style everything with the var(--token) values instead of hard-coded colours:

.card {
  background: var(--surface-primary);
  color: var(--text-primary);
  border: 1px solid var(--border-primary);
  border-radius: 8px;
}
.card button {
  background: var(--accent);
  color: var(--selected-text);
}

The tokens ship a light palette by default; setting data-theme="dark" on <html> switches the whole file to the dark palette. Wire that to the SDK and your app tracks the workspace automatically — match on load, follow every toggle:

import { createWorkspaceSdk } from '@sovereign-workspace/sdk-iframe';
import '@sovereign-workspace/design-tokens';

const sdk = createWorkspaceSdk();

function applyTheme(theme) {
  // The tokens ship a light palette by default; data-theme="dark" switches it.
  document.documentElement.setAttribute('data-theme', theme === 'dark' ? 'dark' : '');
}

applyTheme(await sdk.system.getTheme());   // match on load
sdk.system.onThemeChange(applyTheme);      // …and whenever the user toggles

The token file is generated from the workspace's own stylesheet, so the colours you get are always the real, current ones — they can't drift. Available tokens include --surface-primary, --text-primary, --text-secondary, --border-primary, --accent, and the semantic --color-error / success / info / warning. The full list is in the imported file.

Talking to other apps

Apps can message each other, but only when both agree. A message from your app to another is delivered only if your manifest lists that app as a send target and that app's manifest lists yours in its accepts list. Anything else is denied and audited — so no app can be messaged by a stranger.

// Receive messages addressed to your app.
const unsubscribe = sdk.ipc.on('my-app', (env) => {
  console.log('from', env.sender, env.data);
});

// Send to another app. The workspace stamps YOUR identity as the sender —
// a payload cannot spoof it. The message is delivered only if both manifests
// consent: your manifest lists 'chat' as a send target, and 'chat' lists
// 'my-app' in its accepts allowlist. Otherwise the workspace denies and audits.
await sdk.ipc.send('chat', { type: 'greeting', text: 'hi' });

Where your app runs

Your app is a standard web app that the workspace embeds in a sandboxed iframe, so you need to serve it somewhere reachable over HTTPS. There are two hosting models:

1. Self-hosted at a URL. You deploy the app on your own infrastructure and register its HTTPS URL with the workspace. Fastest to ship — but requests and user data flow to your servers. Fine for a stateless tool; less appropriate for a data-heavy app in a workspace built around keeping data in-house.

2. Shipped as a container. You publish a Docker image and the workspace operator runs it on their own infrastructure, alongside the workspace, so data stays on their network. More packaging work for you, but it's the right model for anything data-sensitive — and the one that fits the product's sovereignty goal.

Either way, your app must permit framing by the workspace. Serve a Content-Security-Policy: frame-ancestors (or an X-Frame-Options) that allows the workspace origin. If framing is refused, the workspace falls back to opening your app in a new tab — where the postMessage bridge is unavailable, so you lose the SDK entirely (no theme, notifications, IPC, or token).

In the desktop (native) app: the workspace runs as a native shell and embeds your app in an OS webview rather than an HTML iframe, so the framing headers above don't apply there — your app loads even if it disallows framing. One thing to know: the SDK bridge currently targets the browser client, and SDK access from inside the desktop app's native webview is on the roadmap. Build your integration to degrade gracefully when the SDK isn't available — the standalone fallback in createWorkspaceSdk() already does this for you.

Offline & caching

Because the iframe keeps your app's own origin, the usual browser storage and caching APIs are available:

  • Register a service worker to cache your app shell and assets so the UI loads without a connection.
  • Persist data locally with IndexedDB or localStorage.

Two constraints worth designing around:

  • Caching covers your front-end, not your backend. Offline handles the UI, but calls to your own API still need it reachable. If your app has a backend, the container model (option 2) keeps it on the workspace's network — reachable even when the public internet isn't.
  • This isn't an installable PWA. The workspace is the installed shell; your app is a surface inside it, so there's no separate web-app-manifest or install-prompt to ship — just use a service worker and local storage for resilience.

Caveat: as a cross-origin iframe, your storage and service worker may be partitioned under the top-level workspace origin in some browsers' privacy modes. It works, but the cache is keyed per workspace — test offline behaviour against your actual deployment target.

Getting your app into a workspace

External apps are added by a workspace administrator, who registers your app's URL and allocates it to the user groups that should see it. The workspace then loads it in the sandboxed iframe, mints its scoped token at launch, and applies the IPC consent rules above.

The one-click, submit-a-manifest install flow is still being built. If you're building an app today, get in touch and we'll help you register it — and tell you the moment self-service registration ships.

Packages

Building something for the workspace?

We want third-party apps to feel first-party. Tell us what you're building and we'll help you register it and get the theme right.

Email hello@sovereignworkspace.org Try the live demo