Custom URL Protocols
Custom URL protocols (also called URL schemes) allow your application to be launched when users click links with your custom protocol, such as myapp://action or myapp://open/document.
Overview
Section titled “Overview”Custom protocols enable:
- Deep linking: Launch your app with specific data
- Browser integration: Handle links from web pages
- Email links: Open your app from email clients
- Inter-app communication: Launch from other applications
Example: myapp://open/document?id=123 launches your app and opens document 123.
Configuration
Section titled “Configuration”Define custom protocols in your application options:
package main
import "github.com/wailsapp/wails/v3/pkg/application"
func main() { app := application.New(application.Options{ Name: "My Application", Description: "My awesome application", Protocols: []application.Protocol{ { Scheme: "myapp", Description: "My Application Protocol", Role: "Editor", // macOS only }, }, })
// Register handler for protocol events app.Event.On(application.Events.ApplicationOpenedWithURL, func(event *application.ApplicationEvent) { url := event.Context().ClickedURL() handleCustomURL(url) })
app.Run()}
func handleCustomURL(url string) { // Parse and handle the custom URL // Example: myapp://open/document?id=123 println("Received URL:", url)}Protocol Handler
Section titled “Protocol Handler”Listen for protocol events to handle incoming URLs:
app.Event.On(application.Events.ApplicationOpenedWithURL, func(event *application.ApplicationEvent) { url := event.Context().ClickedURL()
// Parse the URL parsedURL, err := parseCustomURL(url) if err != nil { app.Logger.Error("Failed to parse URL:", err) return }
// Handle different actions switch parsedURL.Action { case "open": openDocument(parsedURL.DocumentID) case "settings": showSettings() case "user": showUser Profile(parsedURL.UserID) default: app.Logger.Warn("Unknown action:", parsedURL.Action) }})URL Structure
Section titled “URL Structure”Design clear, hierarchical URL structures:
myapp://action/resource?param=value
Examples:myapp://open/document?id=123myapp://settings/theme?mode=darkmyapp://user/profile?username=johnBest practices:
- Use lowercase scheme names
- Keep schemes short and memorable
- Use hierarchical paths for resources
- Include query parameters for optional data
- URL-encode special characters
Platform Registration
Section titled “Platform Registration”Custom protocols are registered differently on each platform.
Windows NSIS Installer
Section titled “Windows NSIS Installer”Wails v3 automatically registers custom protocols when using NSIS installers.
Automatic Registration
Section titled “Automatic Registration”When you build your application with wails3 build, the NSIS installer:
- Automatically registers all protocols defined in
application.Options.Protocols - Associates protocols with your application executable
- Sets up proper registry entries
- Removes protocol associations during uninstall
No additional configuration required!
How It Works
Section titled “How It Works”The NSIS template includes built-in macros:
wails.associateCustomProtocols- Registers protocols during installationwails.unassociateCustomProtocols- Removes protocols during uninstall
These macros are automatically called based on your Protocols configuration.
Manual Registry (Advanced)
Section titled “Manual Registry (Advanced)”If you need manual registration (outside NSIS):
@echo offREM Register custom protocolREG ADD "HKEY_CURRENT_USER\SOFTWARE\Classes\myapp" /ve /d "URL:My Application Protocol" /fREG ADD "HKEY_CURRENT_USER\SOFTWARE\Classes\myapp" /v "URL Protocol" /t REG_SZ /d "" /fREG ADD "HKEY_CURRENT_USER\SOFTWARE\Classes\myapp\shell\open\command" /ve /d "\"%1\"" /fTesting
Section titled “Testing”Test your protocol registration:
# Open protocol URL from PowerShellStart-Process "myapp://test/action"
# Or from command promptstart myapp://test/actionWindows MSIX Package
Section titled “Windows MSIX Package”Custom protocols are also automatically registered when using MSIX packaging.
Automatic Registration
Section titled “Automatic Registration”When you build your application with MSIX, the manifest automatically includes protocol registrations from your build/config.yml protocols configuration.
The generated manifest includes:
<uap:Extension Category="windows.protocol"> <uap:Protocol Name="myapp"> <uap:DisplayName>My Application Protocol</uap:DisplayName> </uap:Protocol></uap:Extension>Universal Links (Web-to-App Linking)
Section titled “Universal Links (Web-to-App Linking)”Windows supports Web-to-App linking, which works similarly to Universal Links on macOS. When deploying your application as an MSIX package, you can enable HTTPS links to launch your app directly.
To enable Web-to-App linking, follow the Microsoft guide on web-to-app linking. You’ll need to:
-
Manually add App URI Handler to your MSIX manifest (
build/windows/msix/app_manifest.xml):<uap3:Extension Category="windows.appUriHandler"><uap3:AppUriHandler><uap3:Host Name="myawesomeapp.com"/></uap3:AppUriHandler></uap3:Extension> -
Configure
windows-app-web-linkon your website: Host awindows-app-web-linkfile athttps://myawesomeapp.com/.well-known/windows-app-web-link. This file should contain your app’s package information and the paths it handles.
When a Web-to-App link launches your application, you’ll receive the same ApplicationOpenedWithURL event as with custom protocol schemes.
Info.plist Configuration
Section titled “Info.plist Configuration”On macOS, protocols are registered via your Info.plist file.
Automatic Configuration
Section titled “Automatic Configuration”Wails automatically generates the Info.plist with your protocols when you build with wails3 build.
The protocols from application.Options.Protocols are added to:
<key>CFBundleURLTypes</key><array> <dict> <key>CFBundleURLName</key> <string>My Application Protocol</string> <key>CFBundleURLSchemes</key> <array> <string>myapp</string> </array> <key>CFBundleTypeRole</key> <string>Editor</string> </dict></array>Testing
Section titled “Testing”# Open protocol URL from terminalopen "myapp://test/action"
# Check registered handlers/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -dump | grep myappUniversal Links
Section titled “Universal Links”In addition to custom protocol schemes, macOS also supports Universal Links, which allow your app to be launched by regular HTTPS links (e.g., https://myawesomeapp.com/path). Universal Links provide a seamless user experience between your web and desktop app.
To enable Universal Links, follow the Apple guide on supporting Universal Links in your app. You’ll need to:
-
Add entitlements in your
entitlements.plist:<key>com.apple.developer.associated-domains</key><array><string>applinks:myawesomeapp.com</string></array> -
Add NSUserActivityTypes to Info.plist:
<key>NSUserActivityTypes</key><array><string>NSUserActivityTypeBrowsingWeb</string></array> -
Configure
apple-app-site-associationon your website: Host anapple-app-site-associationfile athttps://myawesomeapp.com/.well-known/apple-app-site-association.
When a Universal Link triggers your app, you’ll receive the same ApplicationOpenedWithURL event, making the handling code identical to custom protocol schemes.
Desktop Entry
Section titled “Desktop Entry”On Linux, protocols are registered via .desktop files.
Automatic Configuration
Section titled “Automatic Configuration”Wails generates a desktop entry file with protocol handlers when you build with wails3 build.
Fixed in v3: Linux desktop template now properly includes protocol handling.
The generated desktop file includes:
[Desktop Entry]Type=ApplicationName=My ApplicationExec=/usr/bin/myapp %uMimeType=x-scheme-handler/myapp;Manual Registration
Section titled “Manual Registration”If needed, manually install the desktop file:
# Copy desktop filecp myapp.desktop ~/.local/share/applications/
# Update desktop databaseupdate-desktop-database ~/.local/share/applications/
# Register protocol handlerxdg-mime default myapp.desktop x-scheme-handler/myappTesting
Section titled “Testing”# Open protocol URLxdg-open "myapp://test/action"
# Check registered handlerxdg-mime query default x-scheme-handler/myappComplete Example
Section titled “Complete Example”Here’s a complete example handling multiple protocol actions:
package main
import ( "fmt" "net/url" "strings" "github.com/wailsapp/wails/v3/pkg/application")
type App struct { app *application.Application window *application.WebviewWindow}
func main() { app := application.New(application.Options{ Name: "DeepLink Demo", Description: "Custom protocol demonstration", Protocols: []application.Protocol{ { Scheme: "deeplink", Description: "DeepLink Demo Protocol", Role: "Editor", }, }, })
myApp := &App{app: app} myApp.setup()
app.Run()}
func (a *App) setup() { // Create window a.window = a.app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "DeepLink Demo", Width: 800, Height: 600, URL: "http://wails.localhost/", })
// Handle custom protocol URLs a.app.Event.On(application.Events.ApplicationOpenedWithURL, func(event *application.ApplicationEvent) { customURL := event.Context().ClickedURL() a.handleDeepLink(customURL) })}
func (a *App) handleDeepLink(rawURL string) { // Parse URL parsedURL, err := url.Parse(rawURL) if err != nil { a.app.Logger.Error("Failed to parse URL:", err) return }
// Bring window to front a.window.Show() a.window.SetFocus()
// Extract path and query path := strings.Trim(parsedURL.Path, "/") query := parsedURL.Query()
// Handle different actions parts := strings.Split(path, "/") if len(parts) == 0 { return }
action := parts[0]
switch action { case "open": if len(parts) >= 2 { resource := parts[1] id := query.Get("id") a.openResource(resource, id) }
case "settings": section := "" if len(parts) >= 2 { section = parts[1] } a.openSettings(section)
case "user": if len(parts) >= 2 { username := parts[1] a.openUserProfile(username) }
default: a.app.Logger.Warn("Unknown action:", action) }}
func (a *App) openResource(resourceType, id string) { fmt.Printf("Opening %s with ID: %s\n", resourceType, id) // Emit event to frontend a.app.Event.Emit("navigate", map[string]string{ "type": resourceType, "id": id, })}
func (a *App) openSettings(section string) { fmt.Printf("Opening settings section: %s\n", section) a.app.Event.Emit("navigate", map[string]string{ "page": "settings", "section": section, })}
func (a *App) openUserProfile(username string) { fmt.Printf("Opening user profile: %s\n", username) a.app.Event.Emit("navigate", map[string]string{ "page": "user", "user": username, })}Frontend Integration
Section titled “Frontend Integration”Handle navigation events in your frontend:
import { Events } from '@wailsio/runtime'
// Listen for navigation events from protocol handlerEvents.On('navigate', (event) => { const { type, id, page, section, user } = event.data
if (type === 'document') { // Open document with ID router.push(`/document/${id}`) } else if (page === 'settings') { // Open settings router.push(`/settings/${section}`) } else if (page === 'user') { // Open user profile router.push(`/user/${user}`) }})Security Considerations
Section titled “Security Considerations”Validate All Input
Section titled “Validate All Input”Always validate and sanitize URLs from external sources:
func (a *App) handleDeepLink(rawURL string) { // Parse URL parsedURL, err := url.Parse(rawURL) if err != nil { a.app.Logger.Error("Invalid URL:", err) return }
// Validate scheme if parsedURL.Scheme != "myapp" { a.app.Logger.Warn("Invalid scheme:", parsedURL.Scheme) return }
// Validate path path := strings.Trim(parsedURL.Path, "/") if !isValidPath(path) { a.app.Logger.Warn("Invalid path:", path) return }
// Sanitize parameters params := sanitizeQueryParams(parsedURL.Query())
// Process validated URL a.processDeepLink(path, params)}
func isValidPath(path string) bool { // Only allow alphanumeric and forward slashes validPath := regexp.MustCompile(`^[a-zA-Z0-9/]+$`) return validPath.MatchString(path)}
func sanitizeQueryParams(query url.Values) map[string]string { sanitized := make(map[string]string) for key, values := range query { if len(values) > 0 { // Take first value and sanitize sanitized[key] = sanitizeString(values[0]) } } return sanitized}Prevent Injection Attacks
Section titled “Prevent Injection Attacks”Never execute URLs directly as code or SQL:
// ❌ DON'T: Execute URL contentfunc badHandler(url string) { exec.Command("sh", "-c", url).Run() // DANGEROUS!}
// ✅ DO: Parse and validatefunc goodHandler(url string) { parsed, _ := url.Parse(url) action := parsed.Query().Get("action")
// Whitelist allowed actions allowed := map[string]bool{ "open": true, "settings": true, "help": true, }
if allowed[action] { handleAction(action) }}Testing
Section titled “Testing”Manual Testing
Section titled “Manual Testing”Test protocol handlers during development:
Windows:
Start-Process "myapp://test/action?id=123"macOS:
open "myapp://test/action?id=123"Linux:
xdg-open "myapp://test/action?id=123"HTML Testing
Section titled “HTML Testing”Create a test HTML page:
<!DOCTYPE html><html><head> <title>Protocol Test</title></head><body> <h1>Custom Protocol Test Links</h1>
<ul> <li><a href="myapp://open/document?id=123">Open Document 123</a></li> <li><a href="myapp://settings/theme?mode=dark">Dark Mode Settings</a></li> <li><a href="myapp://user/profile?username=john">User Profile</a></li> </ul></body></html>Troubleshooting
Section titled “Troubleshooting”Protocol Not Registered
Section titled “Protocol Not Registered”Windows:
- Check registry:
HKEY_CURRENT_USER\SOFTWARE\Classes\<scheme> - Reinstall with NSIS installer
- Verify installer ran with proper permissions
macOS:
- Rebuild application with
wails3 build - Check
Info.plistin app bundle:MyApp.app/Contents/Info.plist - Reset Launch Services:
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill
Linux:
- Check desktop file:
~/.local/share/applications/myapp.desktop - Update database:
update-desktop-database ~/.local/share/applications/ - Verify handler:
xdg-mime query default x-scheme-handler/myapp
Application Not Launching
Section titled “Application Not Launching”Check logs:
app := application.New(application.Options{ Logger: application.NewLogger(application.LogLevelDebug), // ...})Common issues:
- Application not installed in expected location
- Executable path in registration doesn’t match actual location
- Permissions issues
Best Practices
Section titled “Best Practices”- Use descriptive scheme names -
mycompany-myappinstead ofmca - Validate all input - Never trust URLs from external sources
- Handle errors gracefully - Log invalid URLs, don’t crash
- Provide user feedback - Show what action was triggered
- Test on all platforms - Protocol handling varies
- Document your URL structure - Help users and integrators
❌ Don’t
Section titled “❌ Don’t”- Don’t use common scheme names - Avoid
http,file,app, etc. - Don’t execute URLs as code - Huge security risk
- Don’t expose sensitive operations - Require confirmation for destructive actions
- Don’t assume protocols work everywhere - Have fallback mechanisms
- Don’t forget URL encoding - Handle special characters properly
Next Steps
Section titled “Next Steps”- Windows Packaging - Learn about NSIS installer options
- File Associations - Open files with your app
- Single Instance - Prevent multiple app instances