Back to Wails

Custom URL Protocols

docs/src/content/docs/guides/distribution/custom-protocols.mdx

2.14.017.6 KB
Original Source

import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';

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

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

Define custom protocols in your application options:

Custom protocols are declared in build/config.yml (which the platform packagers — NSIS macros on Windows, MSIX manifest, macOS CFBundleURLTypes, Linux .desktop/xdg-mime — consume at package time). There is no application.Protocol type and no Protocols field on application.Options.

yaml
# build/config.yml
protocols:
  - scheme: myapp
    description: "My Application Protocol"

In Go code, listen for launch-with-URL via the ApplicationLaunchedWithUrl event:

go
package main

import (
    "github.com/wailsapp/wails/v3/pkg/application"
    "github.com/wailsapp/wails/v3/pkg/events"
)

func main() {
    app := application.New(application.Options{
        Name:        "My Application",
        Description: "My awesome application",
    })

    app.Event.OnApplicationEvent(events.Common.ApplicationLaunchedWithUrl, func(e *application.ApplicationEvent) {
        handleCustomURL(e.Context().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

Listen for protocol events to handle incoming URLs:

go
app.Event.OnApplicationEvent(events.Common.ApplicationLaunchedWithUrl, func(e *application.ApplicationEvent) {
    url := e.Context().URL()

    // 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

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

Platform Registration

Custom protocols are registered differently on each platform.

<Tabs syncKey="platform"> <TabItem label="Windows" icon="seti:windows">

Windows NSIS Installer

Wails v3 automatically registers custom protocols when using NSIS installers.

Automatic Registration

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

  1. Automatically registers all protocols declared in build/config.yml under the protocols: key
  2. Associates protocols with your application executable
  3. Sets up proper registry entries
  4. Removes protocol associations during uninstall

No additional configuration required!

How It Works

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.

Manual Registry (Advanced)

If you need manual registration (outside NSIS):

batch
@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

Testing

Test your protocol registration:

powershell
# Open protocol URL from PowerShell
Start-Process "myapp://test/action"

# Or from command prompt
start myapp://test/action

Windows MSIX Package

Custom protocols are also automatically registered when using MSIX packaging.

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:

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

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.

<Aside type="note"> Web-to-App linking requires manual manifest configuration. Custom protocol schemes are automatically configured from `build/config.yml`, but associated domains must be added manually to your MSIX manifest. </Aside>

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

    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 ApplicationLaunchedWithUrl event as with custom protocol schemes.

</TabItem> <TabItem label="macOS" icon="apple">

Info.plist Configuration

On macOS, protocols are registered via your Info.plist file.

Automatic Configuration

Wails automatically generates the Info.plist with your protocols when you build with wails3 build.

The protocols declared in build/config.yml are added to:

xml
<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

bash
# Open protocol URL from terminal
open "myapp://test/action"

# Check registered handlers
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -dump | grep myapp

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.

<Aside type="caution"> Universal Links require your macOS app to be **code-signed** with a valid Apple Developer certificate and provisioning profile. Unsigned or ad-hoc signed builds will not be able to open Universal Links. Ensure your app is properly signed before testing. </Aside>

To enable Universal Links, follow the Apple guide on supporting Universal Links in your app. You'll need to:

  1. Add entitlements in your entitlements.plist:

    xml
    <key>com.apple.developer.associated-domains</key>
    <array>
      <string>applinks:myawesomeapp.com</string>
    </array>
    
  2. Add NSUserActivityTypes to Info.plist:

    xml
    <key>NSUserActivityTypes</key>
    <array>
      <string>NSUserActivityTypeBrowsingWeb</string>
    </array>
    
  3. Configure apple-app-site-association on your website: Host an apple-app-site-association file at https://myawesomeapp.com/.well-known/apple-app-site-association.

When a Universal Link triggers your app, you'll receive the same ApplicationLaunchedWithUrl event, making the handling code identical to custom protocol schemes.

</TabItem> <TabItem label="Linux" icon="linux">

Desktop Entry

On Linux, protocols are registered via .desktop files.

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:

ini
[Desktop Entry]
Type=Application
Name=My Application
Exec=/usr/bin/myapp %u
MimeType=x-scheme-handler/myapp;

Manual Registration

If needed, manually install the desktop file:

bash
# Copy desktop file
cp myapp.desktop ~/.local/share/applications/

# Update desktop database
update-desktop-database ~/.local/share/applications/

# Register protocol handler
xdg-mime default myapp.desktop x-scheme-handler/myapp

Testing

bash
# Open protocol URL
xdg-open "myapp://test/action"

# Check registered handler
xdg-mime query default x-scheme-handler/myapp
</TabItem> </Tabs>

Complete Example

Here's a complete example handling multiple protocol actions:

go
package main

import (
    "fmt"
    "net/url"
    "strings"

    "github.com/wailsapp/wails/v3/pkg/application"
    "github.com/wailsapp/wails/v3/pkg/events"
)

type App struct {
    app    *application.App
    window *application.WebviewWindow
}

func main() {
    // Protocol registration lives in build/config.yml (the platform packagers
    // consume it); the application code just listens for the launch event.
    app := application.New(application.Options{
        Name:        "DeepLink Demo",
        Description: "Custom protocol demonstration",
    })

    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.OnApplicationEvent(events.Common.ApplicationLaunchedWithUrl, func(e *application.ApplicationEvent) {
        a.handleDeepLink(e.Context().URL())
    })
}

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.Focus()

    // 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

Handle navigation events in your frontend:

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

Security Considerations

Validate All Input

Always validate and sanitize URLs from external sources:

go
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

Never execute URLs directly as code or SQL:

go
// ❌ 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)
    }
}

Testing

Manual Testing

Test protocol handlers during development:

Windows:

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

macOS:

bash
open "myapp://test/action?id=123"

Linux:

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

HTML Testing

Create a test HTML page:

html
<!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

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

Application Not Launching

Check logs:

go
app := application.New(application.Options{
    LogLevel: slog.LevelDebug, // requires `import "log/slog"`
    // ...
})

Common issues:

  • Application not installed in expected location
  • Executable path in registration doesn't match actual location
  • Permissions issues

Best Practices

✅ Do

  • 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

  • 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


Questions? Ask in Discord or check the examples.