FSD Frontend System Design

Part II β€” The Shield Β· Lesson 3.12

Feature Policy | Permissions-Policy

In one lineSwitch off browser capabilities your app never uses β€” for your code and every frame you embed.

Where you've seen itA third-party support widget on your page. By default it can prompt for camera, microphone, and location β€” under your domain name.

  • Time12 min
  • DiagramCapability gate per origin and per frame
  • Chapter3 Β· Security

Diagram

diagram
CAPABILITY GATE β€” default is ON for everything

  WITHOUT the header
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ your-app.com ────────────┐
  β”‚  your JS        camera βœ“ mic βœ“ geo βœ“ β”‚
  β”‚  β”Œβ”€β”€β”€ iframe: widget.com ─────────┐  β”‚
  β”‚  β”‚  their JS   camera βœ“ mic βœ“ geo βœ“β”‚  β”‚ ← prompts say "your-app.com"
  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

  WITH  Permissions-Policy: camera=(), microphone=(), geolocation=(self)
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ your-app.com ────────────┐
  β”‚  your JS        camera βœ— mic βœ— geo βœ“ β”‚
  β”‚  β”Œβ”€β”€β”€ iframe: widget.com ─────────┐  β”‚
  β”‚  β”‚  their JS   camera βœ— mic βœ— geo βœ—β”‚  β”‚ ← API rejects, no prompt
  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
diagram
SYNTAX β€” an allowlist per feature

  Permissions-Policy:
    camera=(),                      nobody, not even you
    microphone=(),
    geolocation=(self),             only your own origin
    payment=(self "https://checkout.stripe.com"),
    fullscreen=*,                   everyone including frames
    interest-cohort=()              opt out of tracking cohorts

  Per-frame override (can only narrow, never widen):
    <iframe allow="geolocation 'none'; camera 'none'">

How it works

  1. The header ships from the server; the browser enforces it for the document and every nested frame.
  2. A frame can only receive capabilities the parent already has β€” permissions narrow going down, never widen.
  3. Blocked APIs reject or return empty rather than prompting, so failures are silent β€” test them.
  4. Start from deny-all for anything the product doesn't use, then add back per feature.

Trade-offs

FeatureSensible default
camera, microphone() unless you ship video calling
geolocation(self) for store locators, else ()
payment(self) plus the payment provider's origin
fullscreen(self) for video players
usb, serial, bluetooth() β€” almost never needed

Related isolation headers: Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp give you a cross-origin-isolated context β€” required for SharedArrayBuffer and precise timers.

Interview angle

"Why restrict features you never call?"

  • If XSS or a compromised third-party script lands, those APIs are available to it under your origin's trust.
  • Permission prompts show your domain, so abuse damages your reputation directly.
  • It's a one-line header covering every route, including ones added after you wrote it.

Recap

  • Everything is enabled by default; the header is how you opt out.
  • Frames inherit a narrowed set β€” a parent can never grant what it doesn't have.
  • Blocked APIs fail silently, so verify with real feature tests.