The Core Difference
Both headers control which browser features (geolocation, microphone, camera, etc.) a page and its embedded iframes can use. Feature-Policy is the deprecated original; Permissions-Policy is the modern replacement with a different syntax.
Permissions-Policy uses structured field values with parenthesized allowlists:
Permissions-Policy: geolocation=(self), microphone=(), camera=(self "https://video.example.com")
```text
**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
# Permissions-Policy (current)
Permissions-Policy: geolocation=()
# Feature-Policy (deprecated)
Feature-Policy: geolocation 'none'
```text
### 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
# Permissions-Policy (current)
Permissions-Policy: geolocation=(self "https://maps.googleapis.com")
# Feature-Policy (deprecated)
Feature-Policy: geolocation 'self' https://maps.googleapis.com
```text
## Microphone
### Block Microphone Access
```http
# Permissions-Policy (current)
Permissions-Policy: microphone=()
# Feature-Policy (deprecated)
Feature-Policy: microphone 'none'
Allow Microphone for Video Chat
# Permissions-Policy (current)
Permissions-Policy: microphone=(self "https://meet.example.com")
# Feature-Policy (deprecated)
Feature-Policy: microphone 'self' https://meet.example.com
```text
## Camera
### Block Camera Access
```http
# Permissions-Policy (current)
Permissions-Policy: camera=()
# Feature-Policy (deprecated)
Feature-Policy: camera 'none'
Allow Camera for Same Origin
# Permissions-Policy (current)
Permissions-Policy: camera=(self)
# Feature-Policy (deprecated)
Feature-Policy: camera 'self'
```text
## 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)
Permissions-Policy: geolocation=(self), microphone=(), camera=(), payment=(self "https://checkout.stripe.com"), autoplay=(self)
```text
## 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
# Before (deprecated)
Header always set Feature-Policy "geolocation 'self'; microphone 'none'; camera 'none'"
# After (current)
Header always set Permissions-Policy "geolocation=(self), microphone=(), camera=()"
```text
### 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–87 |
| Edge | 88+ | 79–87 |
| Firefox | 74+ (partial) | 74+ (partial) |
| Safari | 15.4+ (partial) | Not supported |
| Opera | 74+ | 47–73 |
Chrome dropped Feature-Policy support entirely in version 88, so any site still sending only Feature-Policy headers gets no protection in modern Chrome.
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 Chrome and Edge ignore it completely. You must send Permissions-Policy for current browser coverage.