# Permissions-Policy vs Feature-Policy

> Understand the differences between Permissions-Policy and Feature-Policy headers. Compare syntax changes for geolocation, microphone, camera directives and learn how to migrate.

Source: https://howhttpworks.com/compare/permissions-policy-vs-feature-policy
Last reviewed: 2026-10-04

> **TL;DR:** Permissions-Policy is the current header. Feature-Policy is the deprecated older form. The feature names are familiar, but the syntax changed enough that copy-paste migrations often break silently.

## The Fastest Way To Think About This

If you are migrating old security headers, the important point is not “which features do these headers control?” It is:

- `Feature-Policy` is the old header
- `Permissions-Policy` is the modern header
- the syntax changed enough that a naive rename is wrong

## The Core Difference

Both headers control browser capabilities like geolocation, microphone, and camera for a page and its embedded contexts. `Feature-Policy` is the deprecated original. `Permissions-Policy` is the current replacement with structured syntax.

**Permissions-Policy** uses structured field values with parenthesized allowlists:

```http
Permissions-Policy: geolocation=(self), microphone=(), camera=(self "https://video.example.com")
```

**Feature-Policy** used semicolon-separated directives with quoted keywords:

```http
Feature-Policy: geolocation 'self'; microphone 'none'; camera 'self' https://video.example.com
```

The feature names (geolocation, microphone, camera, etc.) are identical — only the syntax around them changed.

## Syntax Comparison

| Aspect              | Permissions-Policy                | Feature-Policy                 |
| ------------------- | --------------------------------- | ------------------------------ |
| Directive separator | `,` (comma)                       | `;` (semicolon)                |
| Block all origins   | `feature=()`                      | `feature 'none'`               |
| Allow all origins   | `feature=*`                       | `feature *`                    |
| Same origin only    | `feature=(self)`                  | `feature 'self'`               |
| Specific origin     | `feature=("https://example.com")` | `feature https://example.com`  |
| Multiple origins    | `feature=(self "https://a.com")`  | `feature 'self' https://a.com` |
| Assignment syntax   | `feature=(allowlist)`             | `feature allowlist`            |

The biggest change is the move from quoted keywords (`'self'`, `'none'`) to structured values (`(self)`, `()`).

## Geolocation

The most commonly searched feature. Here's how the syntax maps:

### Block Geolocation Entirely

```http
# Permissions-Policy (current)
Permissions-Policy: geolocation=()

# Feature-Policy (deprecated)
Feature-Policy: geolocation 'none'
```

### Allow Geolocation for Same Origin

```http
# Permissions-Policy (current)
Permissions-Policy: geolocation=(self)

# Feature-Policy (deprecated)
Feature-Policy: geolocation 'self'
```

### Allow Geolocation for Specific Origins

```http
# Permissions-Policy (current)
Permissions-Policy: geolocation=(self "https://maps.googleapis.com")

# Feature-Policy (deprecated)
Feature-Policy: geolocation 'self' https://maps.googleapis.com
```

## Microphone

### Block Microphone Access

```http
# Permissions-Policy (current)
Permissions-Policy: microphone=()

# Feature-Policy (deprecated)
Feature-Policy: microphone 'none'
```

### Allow Microphone for Video Chat

```http
# Permissions-Policy (current)
Permissions-Policy: microphone=(self "https://meet.example.com")

# Feature-Policy (deprecated)
Feature-Policy: microphone 'self' https://meet.example.com
```

## Camera

### Block Camera Access

```http
# Permissions-Policy (current)
Permissions-Policy: camera=()

# Feature-Policy (deprecated)
Feature-Policy: camera 'none'
```

### Allow Camera for Same Origin

```http
# Permissions-Policy (current)
Permissions-Policy: camera=(self)

# Feature-Policy (deprecated)
Feature-Policy: camera 'self'
```

## Multiple Features

Real-world policies typically combine multiple feature directives. Here's a full migration example:

### Feature-Policy (deprecated)

```http
Feature-Policy: geolocation 'self'; microphone 'none'; camera 'none'; payment 'self' https://checkout.stripe.com; autoplay 'self'
```

### Permissions-Policy (current)

```http
Permissions-Policy: geolocation=(self), microphone=(), camera=(), payment=(self "https://checkout.stripe.com"), autoplay=(self)
```

## Migration Checklist

1. **Replace the header name** — `Feature-Policy` → `Permissions-Policy`
2. **Change separators** — `;` → `,`
3. **Add `=()` assignment** — `feature allowlist` → `feature=(allowlist)`
4. **Replace `'none'`** — `'none'` → `()`
5. **Replace `'self'`** — `'self'` → `self` (no quotes, inside parentheses)
6. **Quote specific origins** — `https://example.com` → `"https://example.com"` (inside parentheses)
7. **Test in DevTools** — verify features are correctly allowed or blocked
8. **Remove Feature-Policy** — once you no longer need legacy browser support

## Server Configuration

### Nginx Migration

```nginx
# Before (deprecated)
add_header Feature-Policy "geolocation 'self'; microphone 'none'; camera 'none'";

# After (current)
add_header Permissions-Policy "geolocation=(self), microphone=(), camera=()";
```

### Apache Migration

```apache
# Before (deprecated)
Header always set Feature-Policy "geolocation 'self'; microphone 'none'; camera 'none'"

# After (current)
Header always set Permissions-Policy "geolocation=(self), microphone=(), camera=()"
```

### Express.js Migration

```javascript
// Before (deprecated)
app.use((req, res, next) => {
  res.set(
    "Feature-Policy",
    "geolocation 'self'; microphone 'none'; camera 'none'",
  );
  next();
});

// After (current)
app.use((req, res, next) => {
  res.set("Permissions-Policy", "geolocation=(self), microphone=(), camera=()");
  next();
});
```

## Browser Support

| Browser | Permissions-Policy | Feature-Policy |
| ------- | ------------------ | -------------- |
| Chrome  | 88+                | 60+ (legacy, still recognized) |
| Edge    | 88+                | 79+ (legacy) |
| Firefox | Partial (subset of features) | See MDN compatibility table |
| Safari  | 15.4+ (partial)    | See MDN compatibility table |
| Opera   | 74+                | 47+ (legacy) |

Feature-Policy is deprecated and no longer developed, and its syntax differs, so a site that sends only the old header will not get any of the newer Permissions-Policy features (for example origin allowlists in the structured syntax). Send Permissions-Policy.

## Common Mistakes

**Mixing old and new syntax** — using `Permissions-Policy: geolocation 'self'` (Feature-Policy syntax with the Permissions-Policy header name) is silently ignored by browsers. The value must use the new structured field syntax.

**Forgetting to quote origins** — in Permissions-Policy, specific origins must be quoted: `geolocation=(self "https://example.com")`. Unquoted origins are silently ignored.

**Using semicolons as separators** — `Permissions-Policy: geolocation=(self); microphone=()` is incorrect. Use commas: `Permissions-Policy: geolocation=(self), microphone=()`.

**Only sending Feature-Policy** — if you only send Feature-Policy, modern Chromium browsers ignore it completely. Current protection requires Permissions-Policy.

## FAQ

### Is Feature-Policy deprecated?

Yes. Feature-Policy was renamed to Permissions-Policy. Chromium shipped Feature-Policy first (Chrome 60) and has kept recognizing the old header alongside Permissions-Policy, but the spec is deprecated and new code should send Permissions-Policy only. Check MDN's compatibility table before depending on Feature-Policy in any browser.

### What changed between Feature-Policy and Permissions-Policy?

The syntax changed significantly. Semicolons became commas as separators. Quoted keywords like 'self' and 'none' became unquoted self and empty parentheses (). Space-separated allowlists are now wrapped in parentheses. Feature names stayed the same.

### How do I migrate geolocation from Feature-Policy to Permissions-Policy?

Change Feature-Policy: geolocation 'self' to Permissions-Policy: geolocation=(self). To block entirely, change geolocation 'none' to geolocation=(). To allow specific origins, change geolocation 'self' https://maps.example.com to geolocation=(self "https://maps.example.com").

### How do I migrate microphone and camera from Feature-Policy to Permissions-Policy?

Change Feature-Policy: microphone 'none'; camera 'none' to Permissions-Policy: microphone=(), camera=(). The pattern is the same for all features — replace 'none' with (), 'self' with (self), and wrap origin lists in parentheses.

### Can I use both Feature-Policy and Permissions-Policy at the same time?

Yes, but it is not recommended. If both headers are present, Permissions-Policy takes precedence in browsers that support it. Older browsers that only understand Feature-Policy will use that header. During migration, you can send both temporarily, but remove Feature-Policy once you no longer need to support very old browsers.

### Do all browsers support Permissions-Policy?

Chrome and Edge 88+ and Opera 74+ support the Permissions-Policy header. Firefox and Safari support it with a smaller set of recognized features, and unknown feature names are ignored. Support differs per feature, so check the MDN compatibility table for the specific directives you set.

## References

- [MDN Web Docs: Permissions-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy)
- [W3C Permissions Policy Specification](https://www.w3.org/TR/permissions-policy-1/)
- [Chrome Platform Status: Permissions-Policy](https://chromestatus.com/feature/5745992911552512)
