Skip to content

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.

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.

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)
}

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)
}
})

Design clear, hierarchical URL structures:

myapp://action/resource?param=value
Examples:
myapp://open/document?id=123
myapp://settings/theme?mode=dark
myapp://user/profile?username=john

Best 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

Custom protocols are registered differently on each platform.

Wails v3 automatically registers custom protocols when using NSIS installers.

When you build your application with wails3 build, the NSIS installer:

  1. Automatically registers all protocols defined in application.Options.Protocols
  2. Associates protocols with your application executable
  3. Sets up proper registry entries
  4. Removes protocol associations during uninstall

No additional configuration required!

The NSIS template includes built-in macros:

  • wails.associateCustomProtocols - Registers protocols during installation
  • wails.unassociateCustomProtocols - Removes protocols during uninstall

These macros are automatically called based on your Protocols configuration.

If you need manual registration (outside NSIS):

Terminal window
@echo off
REM Register custom protocol
REG ADD "HKEY_CURRENT_USER\SOFTWARE\Classes\myapp" /ve /d "URL:My Application Protocol" /f
REG ADD "HKEY_CURRENT_USER\SOFTWARE\Classes\myapp" /v "URL Protocol" /t REG_SZ /d "" /f
REG ADD "HKEY_CURRENT_USER\SOFTWARE\Classes\myapp\shell\open\command" /ve /d "\"%1\"" /f

Test your protocol registration:

Terminal window
# Open protocol URL from PowerShell
Start-Process "myapp://test/action"
# Or from command prompt
start myapp://test/action

Custom protocols are also automatically registered when using MSIX packaging.

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>

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:

  1. 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>
  2. Configure windows-app-web-link on your website: Host a windows-app-web-link file at https://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.

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,
})
}

Handle navigation events in your frontend:

import { Events } from '@wailsio/runtime'
// Listen for navigation events from protocol handler
Events.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}`)
}
})

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
}

Never execute URLs directly as code or SQL:

// ❌ DON'T: Execute URL content
func badHandler(url string) {
exec.Command("sh", "-c", url).Run() // DANGEROUS!
}
// ✅ DO: Parse and validate
func 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)
}
}

Test protocol handlers during development:

Windows:

Terminal window
Start-Process "myapp://test/action?id=123"

macOS:

Terminal window
open "myapp://test/action?id=123"

Linux:

Terminal window
xdg-open "myapp://test/action?id=123"

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>

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.plist in 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

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
  • Use descriptive scheme names - mycompany-myapp instead of mca
  • 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 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

Questions? Ask in Discord or check the examples.