The Isolated Web Apps (IWA) proposal defines a security model and distribution packaging system (Signed Web Bundles) that enables web applications to run with heightened security privileges and offline robustness. However, a single packaged installation does not always map neatly to the diverse, modular applications a developer needs to present to a user.
Developers frequently want to deliver:
- Suite-like experiences (such as office productivity suites containing an email client, a word processor, and a spreadsheet editor).
- Multi-app streaming portals (where a client hosts multiple distinct virtualized applications like a code editor, a browser, and a terminal).
Under a standard Web App installation, all these modules must share a single launcher icon, application name, and shelf window presence, or they must be installed as entirely separate IWAs (incurring massive bundle overhead, distinct update cycles, and separate storage origins).
The Sub Apps API solves this problem by allowing a single parent IWA to programmatically install, list, and remove auxiliary applications (Sub Apps) that:
- Appear to the OS and user as fully distinct applications (separate launcher icons, distinct taskbar/shelf windows, and individual OS integrations like protocol and file handlers).
- Share the underlying Signed Web Bundle resources, origin identification, storage, permissions, and update lifecycle of the parent IWA.
This enables seamless modularity without the friction of multiple installation payloads, fragmented storage, or divergent updates.
- Background & Introduction
- Use cases
- Security and Privacy Considerations
- API Design
- Examples
- Resource Provisioning & Installation
- Detailed Design Discussion
- Considered Alternatives
- References & acknowledgements
A virtual desktop application streams remote applications to the client. The user wants to interact with these remote apps (e.g., a terminal, a text editor, a browser) as if they were running natively on their local operating system. Using the Sub Apps API, the parent IWA can programmatically install a sub-app launcher icon for each remote application. When launched, each remote application runs in its own standalone window with its own distinct name, custom icon, and taskbar item, completely disconnected visually from the parent application.
An office suite consists of an email client, a document processor, and a spreadsheet editor. Rather than forcing the user to install three separate heavyweight IWAs or bundle them all under a single confusing window/icon, the developer installs one parent IWA. The parent IWA then programmatically installs individual Sub Apps. The user gains distinct entry points in their application launcher and can run multiple document/email windows in parallel, each with their proper visual identity, while sharing a single IndexedDB database and cached bundle.
Because Sub Apps operate inside the security perimeter of the parent IWA, they are designed with strong guardrails to prevent origin confusion and capabilities leakage.
A Sub App does not possess a separate security origin. It shares the exact same origin, and local data stores (Cookies, IndexedDB, LocalStorage, and Cache Storage) with the parent IWA. Therefore, standard web security boundaries (such as the Same-Origin Policy) treat the parent and all its Sub Apps as a single entity.
All permissions are shared across the parent and its Sub Apps:
- Granting a permission (e.g., camera, file system access) to a Sub App automatically grants it to the parent, and vice versa.
- To access the Sub Apps API, the parent IWA must explicitly declare the permission policy
sub-apps(e.g.,Permissions-Policy: sub-apps=(self)). Permission policies declared for a sub app have no effect.
- Installation Prompts: When
window.subApps.add()is called, a system permission prompt displays all requested sub-apps in a unified installation dialog. If multiple sub-apps are added at once, they are presented inside a scroll container in a single prompt to avoid dialog spam.
Since developers can customize the names and icons of Sub Apps, there is a risk that a malicious IWA could create a sub-app that mimics system dialogs or trusted third-party applications.
- Mitigation: The Sub Apps API is restricted to Isolated Web Apps, which have integrity and signature verification, also they are only allowed to execute code that is bundled, meaning no dynamic JS invocation is possible.
Sub apps have the ability to register their own OS integrations (such as protocol handlers or file type associations). This means an IWA could potentially extend its reach into the OS far beyond what was declared or audited in the parent app's primary manifest, because sub app web manifest can be even generated dynamically using Service Worker provisioning which means our ability to inspect the manifest is limited. This is mitigated by the fact that most OS integrations require user approval before working. For example if a file handler is specified, it does not mean that the app will instantly handle files by default, first the user will need to select the sub app as the default app and click “Open always with”.
The subApps object is exposed on the Window interface:
[
Exposed=Window,
SecureContext,
IsolatedContext
] partial interface Window {
[SameObject] readonly attribute SubApps subApps;
};
dictionary SubAppsAddResponse {
record<InstallPath, ManifestId> installedApps;
record<InstallPath, DOMException> failedApps;
};
dictionary SubAppsRemoveResponse {
sequence<USVString> removedApps;
record<USVString, DOMException> failedApps;
};
dictionary SubAppsListResult {
required DOMString appName;
};
[
Exposed=Window,
SecureContext,
IsolatedContext
] interface SubApps {
Promise<SubAppsAddResponse> add(sequence<USVString> install_paths);
Promise<SubAppsRemoveResponse> remove(sequence<USVString> manifest_ids);
Promise<record<USVString, SubAppsListResult>> list();
};- Arguments: A sequence of relative paths to the sub-app's
index.htmlthat must reference a web manifest. Must start with '/'. - Asynchronous Rejections: Returns a Promise that resolves to a
SubAppsAddResponse. There are no synchronous exceptions. The entire promise can be rejected with aDOMExceptionfor global/batch-level failures:TypeError: If invalid relative paths are passed.SecurityError: If thesub-appspermissions policy is missing or not allowed.NotSupportedError: If the API is called inside of another sub app frame.NotAllowedError: The user declined the batch installation prompt.QuotaExceededError: The number of sub-apps exceeds the platform limit per parent app.OperationError: A generic failure occurred.
- Returned Object:
SubAppsAddResponsecontains records mapping each inputInstallPathprovided to the call to either its success or failure state. Developers must use theInstallPathkey to identify which call failed and which succeeded:installedApps: A record mapping theInstallPathto the successfully installed sub-app'sManifestId.ManifestIdis a stable identified of a sub app, it is later used insubApps.removeandsubApps.listcalls. For what isManifestIdexactly, please, consult Sub App identity section.failedApps: A record mapping theInstallPathto aDOMExceptionexplaining why that individual sub-app failed to install. Possible exceptions include:ConstraintError: Provided sub app scope web manifest property overlaps with scopes of other sub apps or the parent app or the provided install url pointed to the app web manifest (itself).DataError: The referenced web manifest was invalid or could not be parsed.InvalidStateError: The sub-app is already installed.OperationError: A generic system or database failure occurred during the installation of this specific sub-app.
- Arguments: A sequence of
USVStringvalues to uninstall. - Asynchronous Rejections: Returns a Promise that resolves to a
SubAppsRemoveResponse. There are no synchronous exceptions. The entire promise can be rejected with aDOMExceptionfor global/batch-level failures:TypeError: If invalid URLs are provided.SecurityError: If thesub-appspermissions policy is missing or not allowed.NotSupportedError: If the API is called inside of another sub app frame.OperationError: A generic deletion failure occurred.
- Returned Object:
SubAppsRemoveResponsecontains the successfully removed IDs and records mapping any failedUSVStringto its failure state:removedApps: A sequence (sequence<USVString>) of successfully removedManifestIdvalues.failedApps: A record mapping the inputManifestIdto aDOMExceptionexplaining why that individual sub-app failed to remove. Possible exceptions include:NotFoundError: No sub-app with the given ID is installed under this parent IWA, it belongs to a different parent IWA, or it was already removed.OperationError: A generic deletion failure occurred for this specific sub-app.
- Asynchronous Rejections: Returns a Promise. There are no synchronous exceptions. If the operation fails, the Promise rejects with a
DOMException:SecurityError: If thesub-appspermissions policy is missing or not allowed.NotSupportedError: If the API is called inside of another sub app frame.OperationError: A generic failure occurred.
- Returned Object: The function returns a record mapping
ManifestIdtoSubAppsListResult. This mapping (instead of a sequence/list) makes it convenient for developers to use dictionary operations on the result (e.g., to directly check if it contains a specificmanifest_id).SubAppsListResult.appNameis the name of the sub-app extracted from its web manifest.
To install sub-apps, you call window.subApps.add() with one or more relative URLs pointing to the sub-app entry point. Each entry point must be a valid HTML page that links to a web manifest.
async function installSubApps() {
try {
const { installedApps, failedApps } = await window.subApps.add(["/calc", "/docs"]);
for (const [installPath, id] of Object.entries(installedApps)) {
console.log(`Success: ${installPath} -> ${id}`);
}
for (const [installPath, exception] of Object.entries(failedApps)) {
console.error(`Failed: ${installPath} due to ${exception.name}`);
}
} catch (error) {
console.error("Failed to execute add call:", error);
}
}Example manifest linked by /calculator_app/index.html (calculator.webmanifest):
{
"start_url": "/calculator_app/index.html",
"name": "Calculator",
"version": "3.0.0",
"display": "standalone",
"icons": [
{
"src": "/images/calculator.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "any maskable"
}
],
"protocol_handlers": [
{
"protocol": "web+calc",
"url": "/calculator_app/index.html?query=%s"
}
],
"launch_handler": {
"client_mode": "focus-existing"
}
}You can retrieve all currently installed sub-apps associated with the parent app:
async function printInstalledSubApps() {
try {
const installedApps = await window.subApps.list();
for (const [manifestId, info] of Object.entries(installedApps)) {
console.log(`Sub App Manifest ID: ${manifestId}`);
console.log(`Sub App Name: ${info.appName}`);
}
} catch (error) {
console.error("Error listing sub-apps:", error);
}
}To uninstall sub-apps programmatically, call window.subApps.remove() with the list of target manifest IDs:
async function uninstallSubApps(calculatorId, docsId) {
try {
const { removedApps, failedApps } = await window.subApps.remove([calculatorId, docsId]);
for (const id of removedApps) {
console.log(`Successfully removed Sub App: ${id}`);
}
for (const [id, exception] of Object.entries(failedApps)) {
console.error(`Failed: ${id} due to ${exception.name}`);
}
} catch (error) {
console.error("Failed to execute remove call:", error);
}
}Sub app resources can be provided in two ways: Static and Dynamic.
- Static Provisioning: The parent IWA packages all resources into itself. The minimum requirement is the start HTML file and the web manifest it references. In this case to update the sub app, the parent IWA must be updated first.
- Dynamic Provisioning: The IWA hosts dynamically generated resources and a web manifest via a Service Worker. The app provides a
start_urlthat the Service Worker intercepts. This approach eliminates the need to update the IWA for sub app updates. Even though the sub app is dynamic, it can be implemented in a way that is sufficiently auditable from the parent IWA bundle, because IWA itself cannot execute JS that was not provided in the bundle in the first place.
The full example shows how to dynamically install a sub app using a Service Worker.
// ServiceWorker.js script
declare var self: ServiceWorkerGlobalScope;
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
const htmlMatch = url.pathname.match(/^\/dynamic\/(.+)\/app\.html$/);
const manifestMatch = url.pathname.match(/^\/dynamic\/(.+)\/app\.webmanifest$/);
if (htmlMatch) {
const appName = htmlMatch[1];
const html = appHtml(appName);
event.respondWith(localResponse("text/html; charset=utf-8", html));
} else if (manifestMatch) {
const appName = manifestMatch[1];
const manifest = appManifest(appName);
const json = JSON.stringify(manifest);
event.respondWith(localResponse("application/manifest+json", json));
}
});
const appHtml = (appName: string) => `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="manifest" href="/dynamic/${appName}/app.webmanifest" />
<title>Dynamic sub app</title>
</head>
<body>
<h1>Dynamic sub app</h1>
<p>App: <b>${capitalize(appName)}</b></p>
</body>
</html>
`;
const appManifest = (name: string) => ({
"name": capitalize(name),
"start_url": `/dynamic/${name}/app.html`,
"version": "0.0.0",
"display": "standalone",
"icons": [
{
"src": appIcon(name),
"type": "image/png",
"sizes": "512x512",
"purpose": "any maskable"
}
]
});
const appIcon = (appName: string) => "data:image/jpeg;base64,..."// Base64 encoded icon stringTwo distinct options were considered for defining sub-apps:
- Install URL (Chosen): Pass the
install_pathof the sub-app, which references a standard hosted/provided web manifest. - Manifest String: Feed a JSON manifest object directly as an argument to
window.subApps.add().
The Install path approach was chosen because it aligns natively with standard web-manifest parsing, validation. Passing raw manifest objects would require introducing custom ID generators, complex update/refresh mechanisms, and completely bypassing standard manifest security audits, creating a high maintenance and security overhead.
Two distinct options were considered for the return types of the add() and remove() methods:
Promise<_Response>(Chosen): Returns a single promise that resolves once the batch operation concludes. The resulting response dictionary maps individual items to either successful confirmation identifiers or detailed exception records in the event of partial failures.sequence<Promise>: Returns an array of promises where each promise corresponds to a single sub-app operation. While this approach allows individual calls to resolve slightly earlier if one completes faster than the others, sub-app installations and removals are generally extremely fast, rarely involving network latency or intensive computation.
Promise<_Response> was chosen primarily for superior API ergonomics and simplicity. Returning a sequence of promises would place an unnecessary burden on developers, requiring them to manage and resolve multiple concurrent promises via loops.
Sub apps rely on standard web manifest identity fields, but enforce specific isolation and overlap rules to guarantee they do not conflict with each other or the parent application.
| Identifier | Relationship to Parent / Generation Rule |
|---|---|
| Origin | Identical to the parent IWA. |
| SignedWebBundleId | Identical to the parent IWA. |
| Start URL | Must be defined in the web manifest. Must be unique between sub-apps and the parent app. |
| Scope | Might be defined in the web manifest. Scopes must not overlap between sub-apps. The scope of a sub-app must not overlap with or cover the parent app's scope. Otherwise, the installation/update fails. |
| URL | Parent IWA origin + / + sub_app_start_url. Example: isolated-app://pl2ctdpnkf7ltse22mpjdb376etd3ydo7s72lgspuopgzcwl5tkqaaic/sub/calculator.html |
| AppId | It is unique between all apps of a particular user (regardless if it is a sub app or usual app). It is derived from ManifestId. |
| ManifestId | Unique for each sub app. Defined in the web manifest, if it is empty, it falls back to the start_url without the reference/hash fragment. Example: /sub/calculator.html |
protocol_handlers, file_handlers, and launch_handler function individually for each sub app.
Standard web resources (HTML, JS, CSS, images) apply normally.
Sub apps utilize standard web app manifest properties (W3C App Manifest).
Notable web manifest properties are OS Integrations:
- Protocol handlers: Allow a sub app to capture links with custom protocols like
+web_app_protocol://(W3C Protocol Handlers). - File handlers: Open specific types of files like
.docxvia a sub app (MDN File Handlers). - Launch handler: Specify launch behavior on app click, whether new window of app will be opened or existing one reused and brought to focus (Web App Launch).
- Scope extensions: Allow a sub app to capture
https://links that are associated with developer origin websites (Scope Extensions).
To protect the host operating system and the user's application launcher from potential exhaustion or abuse, the platform enforces a hard limit of 20 installed sub-apps per parent IWA.
- Why a total limit: Malicious or runaway applications could easily flood the user's operating system with hundreds of sub-apps. Enforcing a per-call or per-prompt limit would fail to prevent this, as developers could simply trigger successive permission prompts.
- Quota Rejection: If a batch installation call exceeds the platform limit, the entire
add()call rejects with aQuotaExceededErrorDOMException.
The legacy Chrome Apps platform allowed applications to open new windows with bespoke titles and window icons. While simple, this approach has severe drawbacks:
- No OS Integration: It operates only at the visual level and does not expose full OS capability integrations such as launcher searchability, shelf pinning, custom protocol handlers, or file-type associations.
- Inconsistent Launcher UX: Windows opened this way do not behave as individual independent apps in the system app manager or application list, frustrating users who expect standard operating system behaviors.
This specification builds heavily on the PWA manifest update process and standard web platform patterns.
Many thanks to the following individuals for their advice, feedback, and review:
- Dominic Farolino
- Andrew Rayskiy
- Edman Anjos
- Dominik Bylica
- Dan Murphy