# Several PWAs on one origin

> How manifest id and scope distinguish several installed PWAs on one origin, where their boundaries overlap, and what pages can detect at runtime.

A single origin can host distinct installed web applications with separate manifests. Use the manifest
`id` for application identity and `scope` for the navigation boundary of each
application context ([Web Application Manifest](https://www.w3.org/TR/appmanifest/#id-member), W3C).

## How it works

A processed `id` is a URL with the same origin as the processed `start_url`. A user agent should
treat a manifest with an identity that does not match an installed application as a distinct
application. A matching identity signals that the manifest replaces the installed application's
manifest. If `id` is missing, empty, invalid, or cross-origin, its processed value remains the
processed `start_url` ([`id` member](https://www.w3.org/TR/appmanifest/#id-member), W3C).

`scope` is a separate URL. A document is within scope when its URL has the same origin as the
scope URL and its path starts with the scope URL's path. If `scope` is missing, empty, invalid,
or does not contain `start_url`, the default is `start_url` with its filename, query, and fragment
removed ([`scope` member](https://www.w3.org/TR/appmanifest/#scope-member), W3C).

Because matching uses a path prefix, `"/sales"` also contains `/sales-report`; use `"/sales/"`
when the intended boundary is that directory. Two application scopes can contain the same URL,
but `scope` does not identify an installed application. Use distinct, stable `id` values for
identity, and choose non-overlapping scope paths when each URL should remain within one
application context.

The cited BCD dataset records `id` support from Chrome 96, Safari 17 on macOS, and Safari
16.4 on iOS. Edge and Samsung Internet use mirrored data; that file lists no explicit versions
for them. Firefox parses the member without applying it (BCD `manifests.webapp.id`).

:::caution
Without an explicit `id`, the processed `start_url` becomes the identity. Chrome's manifest
identity guide describes using the computed App Id shown in DevTools as the safest way to
add an explicit `id` to an existing application. After adding it, `start_url` or the manifest
path can change while remaining on the same origin ([Uniquely identifying PWAs with the
web app manifest id property](https://developer.chrome.com/docs/capabilities/pwa-manifest-id),
Chrome for Developers).
:::

## Examples

Give sales and support applications separate identities and directory boundaries when
serving them from the same origin.

### Declaring the sales application

Link this manifest from documents that install the sales application.

```json
{
  "name": "Acme Sales",
  "id": "/apps/sales",
  "start_url": "/sales/",
  "scope": "/sales/",
  "display": "standalone"
}
```

The `id` is an identifier rather than a navigation target, so the specification does not require
it to be within `scope`.

### Declaring the support application

The support manifest uses another identity and a non-overlapping path prefix.

```json
{
  "name": "Acme Support",
  "id": "/apps/support",
  "start_url": "/support/",
  "scope": "/support/",
  "display": "standalone"
}
```

Keep the two `id` values stable when changing launch URLs or manifest URLs within the same origin.

### Detecting the linked manifest with a fallback

Checking the document does not reproduce the user agent's `id` or `scope` processing. It detects
whether the browser reports support for the manifest link type and the current document declares
a manifest link. If either check fails, it retains ordinary website navigation.
The link's `relList` is a `DOMTokenList`; its `supports()` method reports whether a token is supported
([DOMTokenList.supports()](https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList/supports), MDN). The `rel~=` selector matches `manifest` within the space-separated `rel` tokens
defined by HTML.

```js
const probe = document.createElement('link');
const supportsManifestLink = typeof probe.relList?.supports === 'function'
  && probe.relList.supports('manifest');
const manifestLink = document.querySelector('link[rel~="manifest"][href]');

if (!supportsManifestLink || !manifestLink) {
  document.documentElement.dataset.navigation = 'website';
} else {
  document.documentElement.dataset.navigation = 'app-candidate';
  console.info(`Linked manifest: ${manifestLink.href}`);
}
```

This check detects the document's declaration only. It does not claim that the browser processes
`id`, that the application is installed, or that the current URL is within the processed scope.

## See also

- [`id` manifest member](/reference/manifest/id/)
- [`scope` manifest member](/reference/manifest/scope/)
- [The N+1 install problem](/reference/installation/n-plus-one/)
- [Web Application Manifest: `id` member](https://www.w3.org/TR/appmanifest/#id-member) (W3C)
- [Web Application Manifest: `scope` member](https://www.w3.org/TR/appmanifest/#scope-member) (W3C)