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-Policyis the old headerPermissions-Policyis 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:
Permissions-Policy: geolocation=(self), microphone=(), camera=(self "https://video.example.com")
Feature-Policy used semicolon-separated directives with quoted keywords:
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'
Allow Geolocation for Same Origin
# 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
Microphone
Block Microphone Access
# 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
Camera
Block Camera Access
# 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'
Multiple Features
Real-world policies typically combine multiple feature directives. Here’s a full migration example:
Feature-Policy (deprecated)
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)
Migration Checklist
- Replace the header name —
Feature-Policy→Permissions-Policy - Change separators —
;→, - Add
=()assignment —feature allowlist→feature=(allowlist) - Replace
'none'—'none'→() - Replace
'self'—'self'→self(no quotes, inside parentheses) - Quote specific origins —
https://example.com→"https://example.com"(inside parentheses) - Test in DevTools — verify features are correctly allowed or blocked
- Remove Feature-Policy — once you no longer need legacy browser support
Server Configuration
Nginx Migration
# 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 Chromium browsers ignore it completely. Current protection requires Permissions-Policy.