# Service worker registration and scope

> How register() binds a script to a URL-prefix scope, how the default scope and Service-Worker-Allowed bound it, how overlaps resolve, and updateViaCache.

A service worker registration pairs one script URL with one *scope*, a URL prefix on the same origin; the browser offers the worker every navigation and subresource request whose URL starts with that prefix. `register()` has shipped unchanged since Chrome 40, Firefox 44, Safari 11.1, and Edge 17 (BCD `api.ServiceWorkerContainer.register`), and the `updateViaCache` option since Chrome 68, Firefox 57, and Safari 11.1.

## How it works

`navigator.serviceWorker.register(scriptURL, { scope, type, updateViaCache })` returns a `Promise<ServiceWorkerRegistration>`. The call is idempotent: repeating it with the same script and scope resolves to the existing registration without starting an install, so a page may call it on every load. A different script URL for the same scope replaces the registration (the new worker goes through the normal install and waiting sequence); a different scope creates a second registration.

The scope defaults to the directory of the script: `/sw.js` gives `/`, `/app/sw.js` gives `/app/`. A registration may narrow its scope with the option (`/app/sw.js` with `scope: '/app/admin/'`) but may not widen it beyond the script's directory unless the response that delivered the script carries a `Service-Worker-Allowed` header naming the wider path. Without that header, `register('/app/sw.js', { scope: '/' })` rejects with a `SecurityError` whose Chromium text is `The path of the provided scope ('/') is not under the max scope allowed ('/app/'). Adjust the scope, move the Service Worker script, or use the Service-Worker-Allowed HTTP header to allow the scope.`.

Scope matching is a plain string-prefix test on the URL, not a path-segment test: scope `/app` also matches `/application/`, so end scopes with a slash. When several registrations on one origin overlap, the browser selects the one with the longest matching scope for each request, so `/app/admin/` wins over `/app/` for `/app/admin/users`. The scope limits only which *clients* the worker controls; the worker's own `fetch()` calls are unrestricted.

`updateViaCache` governs the HTTP cache during update checks. `'imports'` (the default) fetches the main script bypassing the HTTP cache but lets `importScripts()` dependencies come from it; `'all'` allows the HTTP cache for both; `'none'` bypasses it for both. Independently of this setting, the browser ignores HTTP freshness older than 24 hours for the main script. The `type: 'module'` option (Chrome 91, Firefox 114, Safari 15) lets the script use `import` statements; module workers cannot call `importScripts()`.

Registration requires a secure context: `navigator.serviceWorker` is `undefined` on plain HTTP origins other than `localhost`, so the feature test `'serviceWorker' in navigator` is also the HTTPS test. The script must be served with a JavaScript MIME type; Chromium rejects `text/html` (the usual result of a 404 page or an SPA catch-all route) with `Failed to register a ServiceWorker for scope ('https://example.com/') with script ('https://example.com/sw.js'): The script has an unsupported MIME type ('text/html').`.

:::observed
In Chrome (English UI) a scope wider than the script's directory rejects with `DOMException: Failed to register a ServiceWorker: The path of the provided scope ('/') is not under the max scope allowed ('/app/'). Adjust the scope, move the Service Worker script, or use the Service-Worker-Allowed HTTP header to allow the scope.`; the sentence is assembled in Chromium's `service_worker_register_job.cc` (sources). Firefox reports the same case as `SecurityError: The operation is insecure.` with no further detail, so the Chrome message is the quicker way to diagnose a scope mistake.
:::

## Examples

The first example registers with an explicit scope and surfaces the two rejection causes above; the second shows two workers sharing an origin and how to confirm which one controls a page.

### Registering with an explicit scope and reporting failures

Registering after the `load` event keeps the script fetch out of the critical path of the first render. The `catch` branch distinguishes the MIME-type failure (a deployment problem) from a scope failure (a configuration problem) by inspecting the error name and message.

```js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js', {
        scope: '/',
        updateViaCache: 'none',
      });
      console.log('scope:', registration.scope); // "https://example.com/"
    } catch (err) {
      if (err.name === 'SecurityError') {
        console.error('scope not allowed; serve Service-Worker-Allowed or move sw.js', err.message);
      } else {
        console.error('registration failed', err.message); // MIME type, network, parse error
      }
    }
  });
} else {
  // Insecure origin or unsupported browser: the app stays online-only.
}
```

`updateViaCache: 'none'` costs one uncached request for the script and its imports on every navigation into scope; the gain is that a `Cache-Control: max-age=31536000` header on the script can no longer delay an update for up to a day.

### Two registrations on one origin

A marketing site and an app under one origin can run separate workers so that a cache change in one does not invalidate the other. The longest-scope rule routes each request; `navigator.serviceWorker.controller.scriptURL` in a page confirms the result.

```js
await navigator.serviceWorker.register('/sw-site.js', { scope: '/' });
await navigator.serviceWorker.register('/app/sw-app.js', { scope: '/app/' });

// In a page at /app/dashboard, after a reload:
console.log(navigator.serviceWorker.controller.scriptURL); // ".../app/sw-app.js"

// Enumerate every registration on the origin
const all = await navigator.serviceWorker.getRegistrations();
console.log(all.map((r) => r.scope));
```

The cost of two workers is two install and update cycles to reason about and no shared Cache Storage namespace by default (both see the same `caches`, so cache names must not collide). A single worker at `/sw.js` with routing inside the `fetch` handler avoids that at the cost of one larger script.

## See also

- [Service Workers specification: register(scriptURL, options)](https://w3c.github.io/ServiceWorker/#navigator-service-worker-register) (w3c.github.io)
- [ServiceWorkerRegistration: updateViaCache property](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/updateViaCache) (developer.mozilla.org)
- [Service worker lifecycle](/reference/service-worker/lifecycle/)
- [skipWaiting() and the update flow](/reference/service-worker/update-skipwaiting/)
- [scope manifest member](/reference/manifest/scope/)
- [Debugging service workers](/reference/service-worker/debugging/)