Protocol Handlers
Progressive Web Apps (PWAs) have the ability to handle web protocols, which means they can respond to specific URL schemes. For example, a PWA can register to handle mailto: links, so that clicking on an email link can open a compose window in the PWA instead of opening the default mail client.
With protocol handlers, your PWA can provide a more integrated user experience, functioning more like a native application by becoming the default handler for specific protocols on the user's device.
Overview
Protocol handlers allow your PWA to:
Handle standard protocols:
mailto,tel,sms, etc.Register custom protocols: Using the
web+prefixIntercept protocol links: Capture clicks on protocol URLs
Provide integrated experiences: Open your PWA instead of native apps
Deepen user engagement: Make your PWA the default handler
Real-world examples:
Email clients handling
mailto:linksMessaging apps handling
sms:ortel:linksMusic apps handling
web+music:linksRecipe apps handling
web+recipe:linksGame launchers handling
web+game:links
Browser Support
Chrome (Desktop)
✅ Full
Since Chrome 96
Chrome (Android)
✅ Full
Since Chrome 96
Edge (Desktop)
✅ Full
Since Edge 96
Safari
❌ Not supported
-
Firefox
❌ Not supported
-
Limited Browser Support: Protocol handlers are currently only supported in Chromium-based browsers (Chrome, Edge). Always provide fallback experiences.
Configuration
Basic Configuration
pwa:
manifest:
enabled: true
protocol_handlers:
- protocol: "mailto"
url: "/compose?to=%s"
- protocol: "web+custom"
url:
path: "app_feature1"
params:
foo: "bar"Complete Example
pwa:
manifest:
enabled: true
name: "My Email App"
protocol_handlers:
# Standard mailto protocol
- protocol: "mailto"
url: "/mail/compose?to=%s"
# Custom recipe protocol with route
- protocol: "web+recipe"
url:
path: "app_recipe_view"
params:
recipe_id: "%s"
utm_source: "protocol_handler"
# Custom music protocol with placeholder
- protocol: "web+music"
placeholder: "track"
url: "/player?track=%s"Protocol Parameter
The protocol parameter specifies the protocol scheme your PWA can handle. It must follow these rules:
Standard Protocols
Standard web protocols can be registered without any prefix:
mailto - Email addresses
protocol_handlers:
- protocol: "mailto"
url: "/compose?to=%s"Usage: <a href="mailto:[email protected]">Email Me</a>
tel - Telephone numbers
protocol_handlers:
- protocol: "tel"
url: "/call?number=%s"Usage: <a href="tel:+1234567890">Call Us</a>
sms - SMS messages
protocol_handlers:
- protocol: "sms"
url: "/message?to=%s"Usage: <a href="sms:+1234567890">Text Us</a>
Custom Protocols
Custom protocols must start with the web+ prefix:
Valid custom protocols:
protocol_handlers:
- protocol: "web+jngl" # ✓ Valid
- protocol: "web+music" # ✓ Valid
- protocol: "web+recipe" # ✓ Valid
- protocol: "web+game" # ✓ ValidInvalid custom protocols:
protocol_handlers:
- protocol: "jngl" # ✗ Invalid - missing web+ prefix
- protocol: "custom" # ✗ Invalid - missing web+ prefix
- protocol: "myapp" # ✗ Invalid - missing web+ prefixCustom Protocol Rules: Custom protocols MUST use the web+ prefix to be valid. Protocols without this prefix are reserved for standard web protocols.
URL Parameter
The url parameter defines where to redirect when the protocol is triggered. The URL can include a placeholder %s that will be replaced with the protocol's argument.
Simple String Format
protocol_handlers:
- protocol: "mailto"
url: "/mail/compose?to=%s"How it works:
User clicks:
mailto:[email protected]PWA opens:
/mail/[email protected]
Advanced Object Format
Just like with shortcuts and start_url, you can use the full URL object configuration:
protocol_handlers:
- protocol: "web+recipe"
url:
path: "app_recipe_view" # Symfony route name
params:
recipe_id: "%s" # Protocol argument
utm_source: "protocol_handler"
utm_medium: "web+recipe"
path_type_reference: 1 # Absolute pathCorresponding Symfony route:
#[Route('/recipe/{recipe_id}', name: 'app_recipe_view')]
public function viewRecipe(string $recipe_id): Response
{
// recipe_id will contain the protocol argument
// Also receives utm_source and utm_medium parameters
}Path Type Reference
The path_type_reference option controls URL generation:
0
Absolute URL
https://app.com/foo/bar
1
Absolute path (default)
/foo/bar
2
Relative path
../bar
3
Network path
//app.com/foo/bar
Example:
protocol_handlers:
- protocol: "web+app"
url:
path: "app_feature"
path_type_reference: 0 # Generates full URL with domainPlaceholder Parameter
When you need more control over the placeholder format, you can use the placeholder parameter. The bundle will automatically format it as placeholder=%s:
protocol_handlers:
- protocol: "web+jngl"
placeholder: "type"
url: "/handle?type=%s"How it works:
User clicks:
web+jngl:examplePWA opens:
/handle?type=example
Without placeholder (default):
User clicks:
web+jngl:examplePWA opens:
/handle?example(not ideal)
This allows you to create more semantic URL patterns for your protocol handlers.
Use Cases
1. Email Client
Handle mailto: links to compose emails in your PWA:
pwa:
manifest:
name: "WebMail Pro"
protocol_handlers:
- protocol: "mailto"
url: "/compose?to=%s"<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class EmailController extends AbstractController
{
#[Route('/compose', name: 'app_compose')]
public function compose(Request $request): Response
{
$to = $request->query->get('to', '');
return $this->render('email/compose.html.twig', [
'to' => $to,
]);
}
}2. Phone Dialer
Handle tel: links for a calling app:
pwa:
manifest:
name: "WebPhone"
protocol_handlers:
- protocol: "tel"
placeholder: "number"
url: "/call?number=%s"#[Route('/call', name: 'app_call')]
public function call(Request $request): Response
{
$number = $request->query->get('number', '');
// Clean and validate phone number
$cleanNumber = preg_replace('/[^0-9+]/', '', $number);
return $this->render('phone/call.html.twig', [
'number' => $cleanNumber,
]);
}3. Recipe App
Handle custom web+recipe: protocol:
pwa:
manifest:
name: "RecipeBook"
protocol_handlers:
- protocol: "web+recipe"
url:
path: "app_recipe_view"
params:
id: "%s"#[Route('/recipe/{id}', name: 'app_recipe_view')]
public function view(string $id, RecipeRepository $repository): Response
{
$recipe = $repository->find($id);
if (!$recipe) {
throw $this->createNotFoundException('Recipe not found');
}
return $this->render('recipe/view.html.twig', [
'recipe' => $recipe,
]);
}Sharing recipes:
<a href="web+recipe:chocolate-chip-cookies">
Open in RecipeBook
</a>4. Music Player
Handle web+music: protocol for streaming:
pwa:
manifest:
name: "MusicStream"
protocol_handlers:
- protocol: "web+music"
placeholder: "track"
url: "/player?track=%s"#[Route('/player', name: 'app_music_player')]
public function player(Request $request): Response
{
$trackId = $request->query->get('track', '');
// Validate and load track
$track = $this->getTrackById($trackId);
return $this->render('music/player.html.twig', [
'track' => $track,
'autoplay' => true,
]);
}5. Multi-Protocol App
Support multiple protocols in one PWA:
pwa:
manifest:
name: "Communication Hub"
protocol_handlers:
# Email
- protocol: "mailto"
url: "/mail/compose?to=%s"
# Phone
- protocol: "tel"
url: "/phone/call?number=%s"
# SMS
- protocol: "sms"
url: "/sms/compose?to=%s"
# Video call
- protocol: "web+videocall"
placeholder: "room"
url: "/video/join?room=%s"Registration Process
1. User Installs PWA
When a user installs your PWA, the browser detects protocol handlers in the manifest.
2. Permission Request
The browser prompts the user to allow the PWA to handle specified protocols.
Chrome prompt example:
Allow "My Email App" to open mailto links?
[Allow] [Block]3. Default Handler
If the user allows, your PWA becomes an available handler for those protocols.
User can:
Set your PWA as the default handler
Choose on a case-by-case basis
Revoke permissions later
Testing
1. Test Protocol Links
Create test HTML page with protocol links:
<!DOCTYPE html>
<html>
<head>
<title>Protocol Handler Test</title>
</head>
<body>
<h1>Test Protocol Handlers</h1>
<h2>Standard Protocols</h2>
<ul>
<li><a href="mailto:[email protected]">Test mailto</a></li>
<li><a href="tel:+1234567890">Test tel</a></li>
<li><a href="sms:+1234567890">Test sms</a></li>
</ul>
<h2>Custom Protocols</h2>
<ul>
<li><a href="web+recipe:123">Test web+recipe</a></li>
<li><a href="web+music:track-456">Test web+music</a></li>
<li><a href="web+game:level-7">Test web+game</a></li>
</ul>
</body>
</html>2. Check Manifest in DevTools
1. Open DevTools (F12)
2. Go to Application → Manifest
3. Check "Protocol Handlers" section
4. Verify all protocols are listed
5. Verify URLs are correct3. Test Registration Flow
1. Install PWA (if not installed)
2. Click a protocol link
3. Observe permission prompt
4. Allow protocol handling
5. Verify PWA opens with correct URL
6. Check query parameters are populated4. JavaScript Testing
// Check if protocol handler is registered
function checkProtocolHandler(protocol) {
if ('getInstalledRelatedApps' in navigator) {
navigator.getInstalledRelatedApps()
.then(apps => {
console.log('Installed apps:', apps);
});
}
}
// Test protocol link
function testProtocol(protocol, value) {
const link = `${protocol}:${value}`;
console.log('Testing protocol:', link);
window.location.href = link;
}
// Examples
testProtocol('mailto', '[email protected]');
testProtocol('web+recipe', '123');User Management
Checking Permissions
Users can manage protocol handlers in browser settings:
Chrome:
chrome://settings/handlersEdge:
edge://settings/content/defaultProtocolHandlersRevoking Permission
Users can remove your PWA as a protocol handler:
Open browser settings
Go to Protocol handlers / Default apps
Find your PWA
Click "Remove" or "Block"
Best Practices
1. Request Only Needed Protocols
# ✓ Good - only essential protocols
protocol_handlers:
- protocol: "mailto"
url: "/compose?to=%s"
# ✗ Bad - too many protocols
protocol_handlers:
- protocol: "mailto"
- protocol: "tel"
- protocol: "sms"
- protocol: "web+custom1"
- protocol: "web+custom2"
# ... (requesting too many)2. Provide Clear Value
Explain why users should allow protocol handling:
<div class="protocol-prompt">
<h2>Make [App Name] your default email client</h2>
<p>Click "Allow" to open email links in [App Name] instead of your default mail client.</p>
<button onclick="testMailto()">Try it now</button>
</div>3. Handle Missing Parameters
#[Route('/compose', name: 'app_compose')]
public function compose(Request $request): Response
{
$to = $request->query->get('to');
// Handle case where 'to' is missing
if (!$to) {
return $this->render('email/compose.html.twig', [
'to' => '',
'error' => 'No recipient specified',
]);
}
// Validate email
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
return $this->render('email/compose.html.twig', [
'to' => $to,
'error' => 'Invalid email address',
]);
}
return $this->render('email/compose.html.twig', [
'to' => $to,
]);
}4. Sanitize Protocol Input
#[Route('/call', name: 'app_call')]
public function call(Request $request): Response
{
$number = $request->query->get('number', '');
// Sanitize phone number
$cleanNumber = preg_replace('/[^0-9+\-() ]/', '', $number);
// Validate format
if (!preg_match('/^[\d\s()+\-]+$/', $cleanNumber)) {
throw $this->createNotFoundException('Invalid phone number');
}
return $this->render('phone/call.html.twig', [
'number' => $cleanNumber,
]);
}5. Provide Fallback
Always ensure your app works even if protocol handlers aren't available:
// Feature detection
if ('launchQueue' in window) {
// Protocol handlers supported
console.log('Protocol handlers available');
} else {
// Provide alternative navigation
console.log('Protocol handlers not supported');
}6. Test Across Browsers
# Test matrix
- ✓ Chrome (Desktop) - Full support
- ✓ Chrome (Android) - Full support
- ✓ Edge (Desktop) - Full support
- ✗ Safari - Not supported (provide fallback)
- ✗ Firefox - Not supported (provide fallback)Security Considerations
1. HTTPS Required
Protocol handlers only work over HTTPS:
# ✓ Works - HTTPS
https://your-app.com
# ✗ Doesn't work - HTTP
http://your-app.com2. Validate Input
Always validate and sanitize protocol arguments:
public function handleProtocol(Request $request): Response
{
$input = $request->query->get('data', '');
// Validate and sanitize
if (!$this->isValid($input)) {
throw $this->createAccessDeniedException('Invalid input');
}
// Escape for output
$safeInput = htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
return $this->render('template.html.twig', [
'data' => $safeInput,
]);
}3. Prevent Open Redirects
public function handleUrl(Request $request): Response
{
$url = $request->query->get('url', '');
// Validate URL is from your domain
if (!$this->isInternalUrl($url)) {
throw $this->createAccessDeniedException('External URLs not allowed');
}
return $this->redirect($url);
}
private function isInternalUrl(string $url): bool
{
$parsed = parse_url($url);
$allowedHosts = ['yourdomain.com', 'www.yourdomain.com'];
return isset($parsed['host']) && in_array($parsed['host'], $allowedHosts);
}4. Respect User Choice
Always provide an easy way for users to opt-out:
<div class="settings">
<h3>Protocol Handlers</h3>
<p>You can disable protocol handling in your browser settings.</p>
<a href="chrome://settings/handlers" target="_blank">
Manage Protocol Handlers
</a>
</div>Common Issues
Protocol Handler Not Appearing
Problem: PWA doesn't appear as protocol handler option
Solutions:
Verify PWA is installed (not just bookmarked)
Check browser supports protocol handlers (Chrome/Edge only)
Ensure manifest includes
protocol_handlersClear browser cache and reinstall PWA
Check DevTools → Application → Manifest
Permission Not Requested
Problem: Browser doesn't prompt for protocol handler permission
Solutions:
User must click a protocol link first
Browser only prompts once per protocol
Check if permission was previously denied
Reset permissions in browser settings
Wrong URL Generated
Problem: Protocol handler opens wrong URL
Solutions:
Verify
%splaceholder is in URLCheck
path_type_referencevalueEnsure Symfony route exists
Check route parameters match configuration
Protocol Not Working
Problem: Clicking protocol link doesn't open PWA
Solutions:
Verify protocol format (must start with
web+for custom)Check PWA is set as default handler
Test in supported browser (Chrome/Edge)
Verify PWA is still installed
Troubleshooting
Debug Protocol Handling
// Monitor protocol launches
if ('launchQueue' in window) {
window.launchQueue.setConsumer((launchParams) => {
console.log('Launched with:', launchParams.targetURL);
// Extract protocol
const url = new URL(launchParams.targetURL);
console.log('Protocol:', url.protocol);
console.log('Full URL:', url.href);
});
}Check Protocol Registration
1. Open chrome://settings/handlers
2. Look for your PWA
3. Verify protocols are listed
4. Check "Allow" is selectedTest Without Installation
Protocol handlers only work for installed PWAs. To test:
Install PWA first
Then click protocol links
Or test from external page/app
Related Documentation
Shortcuts - App shortcuts configuration
Start URL - URL configuration
File Handlers - Handle file types
Resources
MDN Protocol Handlers: https://developer.mozilla.org/en-US/docs/Web/Manifest/protocol_handlers
Web.dev: https://web.dev/url-protocol-handler/
Chrome Platform Status: https://chromestatus.com/feature/5680742077038592
W3C Spec: https://w3c.github.io/manifest-app-info/#protocol_handlers-member
Last updated
Was this helpful?