Open Your Desktop App from a Web Link
Package an Electron app that opens and updates from custom web links.
Introduction
30 Second Summary
A browser link can take you straight to the right page. Reaching the matching place inside a desktop app usually takes extra clicks.
In this project, you will build a packaged Electron desktop app with a custom link scheme. Clicking myapp:// links will open the app at the linked path or update the same window when it is already running.
What You'll Build
A click on your test page sends its myapp:// path into one focused desktop window.
By the end of this project, you'll have:
- A browser-to-app link that shows an open-app prompt before launching your packaged desktop app.
- A desktop window that displays the incoming link path when the app starts.
- A single-instance flow where a second link refreshes the path inside the existing focused window.
- Secret Mission: An optional challenge to push your desktop app skills further.
Are there any prerequisites?
You need Node.js 22.11+ on a desktop computer. The project targets Electron 35+.
Before We Start
Projects built with Electron depend on Node.js before they can run locally. Missing tools would stop your app before its first window opens.
You will install Node.js 22.11.0 or later. Its installer also provides the npm package manager.
You will also prepare a code editor for your project files. The browser displaying this guide already covers your browser requirement.
In this step, get ready to:
- Check that Node.js meets the minimum version of 22.11.0.
- Confirm that npm is available.
- Prepare a code editor for the Electron project files.
Check your Node.js version
The first check reveals whether your runtime is ready. It also confirms whether the matching package manager is available.
- Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
- Type Terminal on macOS or PowerShell on Windows.
- Press Enter to open the terminal.
- Check both installed versions by running these commands:
node --version
npm --version
What does this check?
- The node command reports your installed runtime version.
- The npm command reports the package manager version bundled with Node.js.
- A missing command means the installer has not added that tool to your system yet.
✔️ My Node.js version meets the minimum
Good news. A Node.js version of 22.11.0 or later is ready for the project.
- Keep the terminal open for the final verification.
ⓧ I see an older Node.js version
Your current installation needs an upgrade before you build the Electron app.
- Open the official installer page for Node.js version 22.11.0.
- Download the installer that matches your operating system.
- Select the installer that matches your computer architecture.
- Open the downloaded installer from your Downloads folder.
- Approve any operating system permission request.
- Keep the default installation options until the installer completes.
- Close the current terminal window after installation.
ⓧ The command is not found
Node.js is not available in your terminal yet. Installing it also adds npm to your computer.
- Open the official installer page for Node.js version 22.11.0.
- Download the installer that matches your operating system.
- Select the installer that matches your computer architecture.
- Open the downloaded installer from your Downloads folder.
- Approve any operating system permission request.
- Keep the default installation options until the installer completes.
- Close the current terminal window after installation.
Installer not completing?
- Close any other Node.js installer before opening the downloaded installer again.
- Confirm that you downloaded the installer for your operating system.
- Restart your computer if the installer finishes but the terminal still cannot find Node.js.
Ask for help with your specific computer: Why is the Node.js installer or version check failing on my computer?
Prepare a code editor and browser
A code editor keeps the project files organised while you build. Visual Studio Code is one option if you do not already have an editor.
✔️ I already have a code editor
- Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
- Type the name of your code editor into the search bar.
- Press Enter to open the editor.
- Confirm that the editor displays an option for opening a folder.
Your editor is ready to hold the project files.
ⓧ I need a code editor
The installer may ask for permission to change your computer. Approving that request allows the editor to finish installing.
- Open the official Visual Studio Code download page.
- Download the installer that matches your operating system.
- Open the downloaded installer from your Downloads folder.
- Approve any operating system permission request.
- Keep the default installation options until setup completes.
- Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
- Type Visual Studio Code into the search bar.
- Press Enter to open the editor.
You should see the editor window with an option for opening a folder.
Your visual workspace is ready. The editor holds your files while the browser handles your custom-link tests.
Verify your development tools
The final check proves that a fresh terminal can find both installed tools. Before you run it, do you expect to see two version numbers?
- Return to the terminal from earlier if Node.js was already installed.
- Open a new terminal through operating system search if you installed or upgraded Node.js.
- Verify Node.js and npm by running these commands:
node --version
npm --version
What should you see?
The first line shows your Node.js version. It should show v22.11.0 or a later version.
The second line shows an npm version number. That second result confirms the package manager is ready.
Your toolkit is ready for the first Electron window.
Still missing a version number?
- Close every terminal window before opening a fresh one.
- Restart your computer if a fresh terminal still cannot find the installed tools.
- Run the official Node.js installer again if only one command returns a version.
Get help with the output you see: Why can my terminal not find Node.js or npm after installation?
Your development tools are in place. Next, you will scaffold the Electron app and open its first desktop window.
Scaffold and Run the Electron App
A custom web link needs a desktop app ready to receive it. This step creates a working Electron shell before you add any link-handling logic.
Electron Forge creates the project structure. It also installs the dependencies needed to launch one blank BrowserWindow.
In this step, get ready to:
- Scaffold myapp-link-demo on your Desktop with Electron 35.2.0.
- Prepare an empty renderer in your code editor.
- Confirm the development start command opens one blank BrowserWindow.
Scaffold the project on your Desktop
The scaffold downloads Electron plus its development tooling. The first download can take a few minutes, so a quiet terminal does not mean it has stalled.
- Press Cmd+Space on macOS or the Windows key on Windows to open system search.
- Type Terminal into the search bar.
- Press Enter.
- Approve npm's download prompt if it appears.
- Keep the project easy to find on your Desktop by running these commands:
cd ~/Desktop
npx create-electron-app@latest myapp-link-demo --electron-version 35.2.0
cd myapp-link-demo
What do these commands do?
- The first line places your terminal on the Desktop.
- The create-electron-app scaffolder creates the project structure. It installs Electron 35.2.0 plus the development dependencies.
- The final line moves your terminal inside myapp-link-demo.
When the scaffold completes, you should see its setup tasks finish without an error.
Solid start. Your Desktop now contains the runnable myapp-link-demo project.
Did the scaffold stop early?
- Check that your internet connection can download packages from the npm registry.
- Move any existing myapp-link-demo folder away from your Desktop before running the scaffold again.
Ask for help with the exact terminal output: Why did my Electron Forge scaffold fail?
Open the scaffold in your code editor
The package.json file connects the start command to the scaffold's main process entry. The renderer file controls what appears inside the window.
- Press Cmd+Space on macOS or the Windows key on Windows to open system search.
- Type the name of your code editor into the search bar.
- Press Enter.
- Choose the control for opening a folder in your editor.
- Double-click your Desktop in the folder picker.
- Double-click the myapp-link-demo folder.
- Expand the src folder in the editor's file sidebar.
- Select index.html.
What are these scaffold files?
- package.json stores the development scripts plus the installed dependencies.
- src/index.js is the main-process entry that creates the BrowserWindow.
- src/index.html is the renderer loaded inside that window.
- Select all content in src/index.html by pressing Cmd+A on macOS or Ctrl+A on Windows.
- Press Backspace to clear the starter content.
- Save src/index.html by pressing Cmd+S on macOS or Ctrl+S on Windows.
You should see a blank src/index.html editor pane. The empty file gives this step its blank app window.
Can't find the renderer file?
- Confirm that your editor opened the myapp-link-demo folder from your Desktop.
- Expand the src folder before looking for index.html.
Ask for help with the files in your sidebar: Why can't I find src/index.html in my Electron Forge project?
Launch the empty Electron window
The start script launches Electron's main process. It keeps the terminal session active while the desktop window runs.
- Switch back to the terminal from earlier.
- Use Ctrl+C later when you want to stop the development process.
Before you run the app, do you expect the command to finish immediately or keep running while the window is open?
- Start the development app by running:
npm start
What does this command do?
- npm reads the start script from package.json.
- Electron Forge launches the scaffold's main process.
- The main process creates one BrowserWindow with the empty renderer you saved.
You should see one blank desktop window. The terminal stays busy because it is hosting the development process.
That's your first visible win. Your myapp-link-demo shell can now launch as a desktop app.
- Keep the blank app window open for the next step.
Didn't the window open?
- Confirm that the terminal is still inside myapp-link-demo.
- Check the terminal output for the first file path or dependency it could not load.
- Restore an empty src/index.html file if you deleted the file instead of clearing its contents.
Ask for help with the launch output: Why isn't my Electron BrowserWindow opening?
Your desktop shell is running. Next, you will make it enforce a single app instance when another launch arrives.
Prevent Duplicate App Instances
Your empty Electron window from the previous step proves the desktop app can run. A second launch could still start a competing process with another window.
A single-instance lock gives the first process ownership of the app. Later launches quit before creating another window.
In this step, get ready to:
- Request the single-instance lock before window creation.
- Quit any process that fails to acquire the lock.
- Bring the existing BrowserWindow into focus after another launch.
Request the single-instance lock
The lock must run before the app creates its window. This gives every new process an immediate ownership check.
- Switch back to the terminal running the development app.
- Stop the current process by pressing Ctrl+C.
The empty app window closes. You can now update the main process without leaving an older copy running.
- Return to the existing main-process entry file in your code editor.
- Locate the Electron import near the top of the file.
- Place this code after the import and before the existing window-creation logic:
const gotTheLock = app.requestSingleInstanceLock()
// Stop this process when another instance already owns the lock
if (!gotTheLock) {
app.quit()
}
What does this code do?
- The app.requestSingleInstanceLock() call asks Electron whether this process can become the primary instance.
- The gotTheLock value records whether this process owns the lock.
- The app.quit() call closes a later process when the primary instance already owns the lock.
- Save the main-process entry file.
- Switch back to the first terminal.
- Rerun the same development start command from the previous step.
You should see one empty app window open again. The process behind that window now owns the lock.
Before the duplicate test, do you think another window can open while the first process owns the lock?
- Create a second terminal inside the myapp-link-demo folder using your editor's terminal controls.
- Rerun the same development start command in the second terminal.
The second process exits without creating another window. Your first process remains open as the app owner.
Still seeing a second window?
- Confirm that the lock request appears before the existing window-creation logic.
- Check that app.quit() remains inside the failed-lock condition.
- Restart the primary development process after saving the main-process entry file.
Help me debug the single-instance lock.
Focus the existing window
Preventing a duplicate window solves only half of the launch experience. The primary process also needs to respond when another launch reaches it.
Electron sends that launch to the primary process through the second-instance event. The handler can restore the existing window before bringing it into focus.
- Switch back to the first terminal.
- Stop the current development process by pressing Ctrl+C.
The primary window closes. Your next launch will load the completed handler.
- Return to the existing main-process entry file.
- Locate the lock check you added above the window-creation logic.
- Place this event handler directly below that lock check:
if (gotTheLock) {
app.on('second-instance', () => {
// Reuse the BrowserWindow owned by the primary process
const existingWindow = BrowserWindow.getAllWindows()[0]
if (existingWindow) {
if (existingWindow.isMinimized()) {
existingWindow.restore()
}
existingWindow.show()
existingWindow.focus()
}
})
}
How does the handler reuse the window?
- The second-instance event runs in the process that already owns the lock.
- The BrowserWindow.getAllWindows()[0] expression retrieves the existing app window.
- The isMinimized() check decides whether the window needs restoring.
- The restore(), show(), and focus() calls return the existing window to the foreground.
- Save the main-process entry file.
- Switch back to the first terminal.
- Rerun the same development start command.
You should see one empty app window open. This window belongs to the primary process.
- Minimize the current app window using your operating system's window control.
Before the final launch, do you expect the minimized window to stay hidden or return to the foreground?
- Switch back to the second terminal.
- Rerun the same development start command.
You should see the minimized window return to the foreground with focus. No second BrowserWindow opens.
That is the duplicate-launch problem solved. One process now owns the app while every later launch returns to its existing window.
Window stays minimized?
- Confirm that the second-instance listener sits inside the successful-lock condition.
- Check that BrowserWindow.getAllWindows()[0] remains inside the event handler.
- Compare the spellings of isMinimized(), restore(), show(), and focus() with the reference below.
Help me debug the existing window handler.
✔️ Awesome, I've got everything!
Your main-process entry file now claims the lock before window creation. Double-check that you saved the file.
ⓧ I'd like to double check the full code
Compare this complete single-instance section with the code before the existing window-creation logic in your main-process entry file.
const gotTheLock = app.requestSingleInstanceLock()
// Stop this process when another instance already owns the lock
if (!gotTheLock) {
app.quit()
}
if (gotTheLock) {
app.on('second-instance', () => {
// Reuse the BrowserWindow owned by the primary process
const existingWindow = BrowserWindow.getAllWindows()[0]
if (existingWindow) {
if (existingWindow.isMinimized()) {
existingWindow.restore()
}
existingWindow.show()
existingWindow.focus()
}
})
}
Your app now keeps one process in control. Next, you will register myapp:// so web links can reach that process.
Handle Custom Protocol Links
Your single-instance lock already keeps one Electron window alive. The next challenge is getting every incoming link to that window.
A custom protocol can arrive through different operating system routes. macOS emits an event. Windows and Linux pass the link through startup arguments.
In this step, get ready to:
- Register the myapp protocol for development launches.
- Build one handler that displays the incoming URL path in the existing window.
- Route macOS events plus Windows and Linux arguments through the shared handler.
Register the myapp protocol
The operating system needs to associate myapp:// links with your application. A Windows development launch also needs the project entry path because it runs through the Electron executable.
- In the main-process entry file from earlier, find the primary-instance branch that begins after the duplicate process quits.
- Place this registration code above the existing second-instance listener:
const path = require('node:path')
// Register this project as the protocol handler during development
if (process.defaultApp && process.argv.length >= 2) {
app.setAsDefaultProtocolClient('myapp', process.execPath, [
path.resolve(process.argv[1]),
])
} else {
app.setAsDefaultProtocolClient('myapp')
}
What does this code do?
- The process.defaultApp check detects a development launch through the Electron executable.
- The resolved entry path tells that executable which project should receive the link.
- A packaged app registers its own executable with app.setAsDefaultProtocolClient().
Why Does Packaging Still Matter?
macOS and Linux custom links require a packaged app. This step prepares the handling logic before the packaging configuration declares the protocol to those operating systems.
- Save the main-process entry file.
- Return to the terminal running the development app.
- Stop the current process by pressing Ctrl+C.
- Restart the app by running:
npm start
What Should You See?
You should see one empty application window open again. The terminal should remain free of startup errors.
App No Longer Starting?
- Confirm the new registration code sits inside the primary-instance branch.
- Check that every opening brace has a matching closing brace.
- Check that node:path includes the colon.
Ask for help with the registration code.
Create one incoming-link handler
A link can arrive before the BrowserWindow exists. The handler stores an early link until the window is ready. It processes later links immediately.
- Return to the main-process entry file.
- Place this code after the protocol registration and before the existing second-instance listener:
// Hold a cold-start link until the window exists
let pendingUrl = process.argv.find((argument) =>
argument.startsWith('myapp://'),
)
const handleIncomingUrl = (incomingUrl) => {
if (!incomingUrl) return
if (!mainWindow) {
pendingUrl = incomingUrl
return
}
// Extract the path that the window needs to display
const { pathname } = new URL(incomingUrl)
const linkPath = pathname || '/'
// Bring the existing window to the foreground
if (mainWindow.isMinimized()) mainWindow.restore()
mainWindow.show()
mainWindow.focus()
// Render the path as plain text in the window
mainWindow.loadURL(
`data:text/plain;charset=utf-8,${encodeURIComponent(linkPath)}`,
)
}
// Capture macOS links before the app becomes ready
app.on('open-url', (event, url) => {
event.preventDefault()
handleIncomingUrl(url)
})
How Does the Handler Work?
- The process.argv.find() call scans startup arguments for a value beginning with myapp://.
- The URL object extracts the link path.
- The existing BrowserWindow is restored if it was minimized.
- A plain-text data URL displays the path without treating it as page markup.
- Save the main-process entry file.
- Return to the terminal running the development app.
- Stop the current process by pressing Ctrl+C.
- Restart the app by running:
npm start
What Should You See?
You should see the empty application window open without a syntax error. The handler is now loaded early enough to retain a cold-start link.
Seeing a Startup Error?
- Confirm mainWindow uses the same capitalization as the existing BrowserWindow variable.
- Check that the open-url listener appears before app.whenReady().
- Confirm the template literal around the data URL uses backticks.
Ask for help with the shared handler.
Route every argument source
The shared handler now covers macOS. Windows and Linux still need two routes. A cold start uses process.argv. A link sent while the app is running uses the arguments from second-instance.
- Find the existing second-instance listener in the main-process entry file.
- Replace that listener with this version:
app.on('second-instance', (_event, commandLine) => {
// Find the protocol URL without relying on argument order
const incomingUrl = commandLine.find((argument) =>
argument.startsWith('myapp://'),
)
handleIncomingUrl(incomingUrl)
})
Why Search the Arguments?
Electron can reorder arguments or append extra values. The find() call selects the argument with the protocol prefix wherever it appears.
The shared handler now performs the restore, show, focus, and display work that the old listener handled directly.
- Save the main-process entry file.
- Return to the terminal running the development app.
- Stop the current process by pressing Ctrl+C.
- Restart the app by running:
npm start
What Should You See?
You should see one application window open. The terminal should show no error from the updated second-instance listener.
Second-Instance Listener Failing?
- Remove the old focus logic if it remains below the new listener.
- Confirm the callback parameter is named commandLine everywhere inside the listener.
- Check that handleIncomingUrl matches the shared function name exactly.
Ask for help with the argument route.
A cold-start URL is already stored in pendingUrl. The final addition sends that stored value to the handler as soon as the BrowserWindow exists.
- Find the app.whenReady() callback that creates mainWindow.
- Place this conditional immediately after the BrowserWindow creation code:
// Process a link that launched the app from a closed state
if (pendingUrl) {
const incomingUrl = pendingUrl
pendingUrl = null
handleIncomingUrl(incomingUrl)
}
What Does This Complete?
The condition waits until mainWindow exists before displaying a startup path. Clearing pendingUrl prevents the same link from being processed twice.
Before you restart, do you expect the app to reach the empty window or stop with an error?
- Save the main-process entry file.
- Return to the terminal running the development app.
- Stop the current process by pressing Ctrl+C.
- Start the completed handler code by running:
npm start
What Should You See?
You should see one empty application window open. The terminal should remain free of startup errors.
That successful launch proves every handler is valid JavaScript. Packaged protocol tests come after the operating system has an installable app to open.
Window Does Not Open?
- Confirm the startup conditional sits after the line that assigns the BrowserWindow to mainWindow.
- Check that pendingUrl was declared inside the primary-instance branch.
- Confirm the previous development process stopped before you started a new one.
Ask for help with the complete startup flow.
✔️ Awesome, I've got everything!
- Save the main-process entry file with all three incoming-link routes in place.
ⓧ I'd like to double check the full code
const { app, BrowserWindow } = require('electron')
let mainWindow
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()
} else {
const path = require('node:path')
// Register this project as the protocol handler during development
if (process.defaultApp && process.argv.length >= 2) {
app.setAsDefaultProtocolClient('myapp', process.execPath, [
path.resolve(process.argv[1]),
])
} else {
app.setAsDefaultProtocolClient('myapp')
}
// Hold a cold-start link until the window exists
let pendingUrl = process.argv.find((argument) =>
argument.startsWith('myapp://'),
)
const handleIncomingUrl = (incomingUrl) => {
if (!incomingUrl) return
if (!mainWindow) {
pendingUrl = incomingUrl
return
}
// Extract the path that the window needs to display
const { pathname } = new URL(incomingUrl)
const linkPath = pathname || '/'
// Bring the existing window to the foreground
if (mainWindow.isMinimized()) mainWindow.restore()
mainWindow.show()
mainWindow.focus()
// Render the path as plain text in the window
mainWindow.loadURL(
`data:text/plain;charset=utf-8,${encodeURIComponent(linkPath)}`,
)
}
// Capture macOS links before the app becomes ready
app.on('open-url', (event, url) => {
event.preventDefault()
handleIncomingUrl(url)
})
app.on('second-instance', (_event, commandLine) => {
// Find the protocol URL without relying on argument order
const incomingUrl = commandLine.find((argument) =>
argument.startsWith('myapp://'),
)
handleIncomingUrl(incomingUrl)
})
app.whenReady().then(() => {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
})
// Process a link that launched the app from a closed state
if (pendingUrl) {
const incomingUrl = pendingUrl
pendingUrl = null
handleIncomingUrl(incomingUrl)
}
})
}
What Should Match?
The full file requests the single-instance lock before creating the window. Every matching myapp:// value reaches handleIncomingUrl().
The handler restores the existing window. It displays the URL path as plain text.
- Confirm the main-process entry file contains the early open-url listener.
- Confirm startup arguments are scanned with process.argv.find().
- Confirm second-instance arguments are scanned with commandLine.find().
- Confirm all three routes call handleIncomingUrl().
You now have one protocol pipeline for cold starts and links received while the app is running. The next step packages the app so your operating system can launch it from a web link.
Package the Protocol App
The previous step gave your Electron main process every route it needs for incoming links. Desktop operating systems read protocol ownership from a packaged application.
The electron-builder tool turns your project into an installable desktop app. Its packaging metadata tells the operating system that your app handles myapp:// links.
In this step, get ready to:
- Install electron-builder as a development dependency.
- Add the protocol packaging metadata to package.json.
- Produce an installable package for your current operating system.
Install electron-builder
The development app uses Electron directly from your project. Packaging requires a build tool that can create the desktop files your operating system understands.
- Close the development BrowserWindow from earlier so the packaged app can later acquire the single-instance lock.
- Start a new integrated terminal for myapp-link-demo in your code editor.
- Install the packaging dependency by running this command:
npm install electron-builder --save-dev
What Did the Install Add?
- The command adds electron-builder to the project's development dependencies.
- Development dependencies support project tooling without becoming part of the packaged app.
- The package creates installers for macOS or Windows. It creates application packages for Linux.
- Wait for the terminal prompt to return.
- Confirm package.json now lists electron-builder under devDependencies.
The returned prompt confirms that the build tool is available inside this project. Your existing Electron app code remains unchanged.
Installation Did Not Finish?
- Confirm the terminal is inside the myapp-link-demo project before trying the install again.
- Check your network connection if the package download stops before the terminal prompt returns.
- Remove any incomplete installation only after the package manager has stopped running.
Ask for help with an electron-builder installation error.
Add the protocol metadata
A packaged custom protocol needs an entry in the application's build metadata. The protocols configuration connects the packaged app to the myapp scheme.
- Select package.json from your code editor's file tree.
- Keep every existing top-level entry unchanged.
- Add the following build property at the top level of package.json:
"build": {
"protocols": [
{
"name": "myapp",
"schemes": ["myapp"]
}
]
}
What Does This Configuration Do?
- The build property gives electron-builder the packaging configuration.
- The protocols entry declares a URL scheme for the packaged app.
- The schemes array associates URLs beginning with myapp:// with that app.
- The name value provides a description for the protocol registration.
- Save package.json.
- Confirm the build property sits alongside existing entries such as scripts and devDependencies.
- Confirm the schemes array contains myapp.
Your saved configuration now carries the protocol declaration into the packaged application. The next build turns that declaration into operating-system metadata.
Seeing a JSON Syntax Problem?
- Check that a comma separates the preceding top-level property from build.
- Confirm each opening brace has a matching closing brace.
- Keep build outside the existing scripts object.
Ask for help checking the package.json structure.
Create the installable package
The build reads your application files plus the new protocol metadata. It writes the finished artifacts to the default dist/ output folder.
The first build downloads the packaging tools it needs, so a quiet pause is expected. Keep the terminal open until control returns to the prompt.
Before you run the build, what new project item do you expect a packaging tool to create?
- Create a distributable for your current operating system by running this command:
npx electron-builder
What Does This Command Do?
- The command reads the build configuration from package.json.
- It packages the Electron app for the operating system running the command.
- It writes the generated application plus its distributable artifacts to dist/.
The command finishes by returning control to your terminal. You should see a new dist/ folder containing an installable artifact for your operating system.
Build Did Not Complete?
- Check the terminal output for a reference to invalid JSON in package.json.
- Confirm electron-builder remains listed under devDependencies.
- Close the development app if a packaging file is reported as busy.
Ask for help with an electron-builder packaging failure.
A locally built package can trigger a platform security warning because it lacks a public distribution signature. The warning is expected for this private test build.
macOS
- Right-click the dist/ folder in your code editor's file tree.
- Choose the option that reveals the folder in Finder.
- Double-click the generated .dmg file.
- Drag the packaged app into the Applications folder shown in the disk image.
- Approve the macOS security prompt if it appears.
- Press Cmd+Space to open Spotlight.
- Type myapp-link-demo.
- Press Enter to open the packaged app.
Windows
- Right-click the dist/ folder in your code editor's file tree.
- Choose the option that reveals the folder in File Explorer.
- Double-click the generated .exe installer.
- Approve the Windows permission request if it appears.
- Follow the installer prompts until the installation completes.
- Press the Windows key to open search.
- Type myapp-link-demo.
- Select the packaged app from the search results.
Linux
- Right-click the dist/ folder in your code editor's file tree.
- Choose the option that reveals the folder in your file manager.
- Right-click the generated AppImage.
- Open the file properties.
- Enable the executable permission.
- Close the file properties.
- Double-click the AppImage to open the packaged app.
You'll see the packaged application window open from the generated artifact. Opening this packaged version gives the operating system an application it can associate with myapp://.
Packaged App Will Not Stay Open?
- Confirm the development Electron window from earlier is closed.
- Check that the npx build completed before opening an artifact from dist/.
- Open the artifact produced for your current operating system.
Ask for help with a packaged Electron app that closes immediately.
That is the packaging hurdle cleared. Your operating system now has a packaged app to associate with myapp://.
Next, you'll create a small web page that sends the first real link into your desktop app.
Open the App from a Web Link
Your packaged Electron app is installed. The operating system already associates myapp:// links with it.
A custom protocol becomes useful when a real web page can trigger it. In this step, you will prove that bridge with a local test page.
In this step, get ready to:
- Create a local test page with a myapp://open/first-link link.
- Open the packaged app through the browser's custom-protocol prompt.
- Confirm the app window displays /first-link.
Prepare a browser launch point
A small HTML page gives you a controlled place to test the registered scheme. Its link carries the exact path that your app should display.
- Use your code editor's new-file control to add test-links.html directly inside the myapp-link-demo folder.
You'll see test-links.html beside the existing project files in your code editor.
- Add the browser link to test-links.html by pasting this code:
<html>
<body>
<!-- This link asks the operating system to route the URL to the packaged app. -->
<a href="myapp://open/first-link">Open the first app link</a>
</body>
</html>
What does this page do?
- The <a> element turns the visible text into a clickable link.
- The href value sends myapp://open/first-link to the operating system.
- The /first-link path gives your incoming-link handler a visible value to display.
- Save test-links.html.
- Open test-links.html from the myapp-link-demo folder by choosing it through your browser's local-file control.
You'll see a page with an Open the first app link link. Your browser launch point is ready.
Can't see the test link?
- Confirm the file is named test-links.html with the .html extension.
- Check that the link line contains myapp://open/first-link exactly.
- Reload the local page after saving the file.
Help me troubleshoot why my local HTML page does not show its custom-protocol link.
✔️ Awesome, I've got everything!
Great. Your test page is saved with the custom-protocol link ready to click.
ⓧ I'd like to double check the full code
<html>
<body>
<!-- This link asks the operating system to route the URL to the packaged app. -->
<a href="myapp://open/first-link">Open the first app link</a>
</body>
</html>
Open the packaged app from the browser
The browser passes myapp:// requests to the operating system. Your protocol registration tells the operating system which packaged app should receive them.
Before you click, which path do you expect the app window to show? Keep that prediction in mind.
- Click Open the first app link on the local test page.
- Approve the browser's request to open the packaged app.
The packaged app opens or comes forward. You'll see /first-link in its window.
That browser-to-desktop handoff now works from a real web link.
Didn't the packaged app open?
- Confirm you clicked the link from the browser page instead of an editor preview.
- Check that the link begins with myapp://.
- On Windows, launch the installed packaged app once if no window appears.
- Return to the browser after that first launch.
Help me troubleshoot why my custom-protocol link does not open the packaged app.
Your browser can now launch the packaged app with a path. Next, you will send another link while the app is running to prove the same window updates.
Update the Running App with Another Link
Your first custom protocol link opened the packaged Electron app. The window displayed /first-link, which confirmed the operating system could deliver a link into your app.
A second click tests the tougher state because the app is already running. This step checks which process receives the new destination.
In this step, get ready to:
- Extend the local test page with another custom protocol destination.
- Trigger the new link while the packaged app remains open.
- Verify the active window handles the new path.
Add a second test link
A distinct path makes the two link deliveries easy to tell apart. The test page keeps the original URL so you can compare both destinations.
- Switch back to test-links.html in the code editor from earlier.
- Find the line containing myapp://open/first-link.
- Duplicate that entire line directly below itself.
- Replace only the URL in the duplicated line with myapp://open/second-link.
- Save test-links.html.
- Return to the local test page in your browser.
- Refresh the page.
You should see two links on the page. The lower link now targets myapp://open/second-link.
Good, your test page can now send a different destination without replacing the original.
Only see one link?
- Confirm the duplicated line sits directly below the original link in test-links.html.
- Confirm the duplicated URL is exactly myapp://open/second-link.
- Refresh the browser page after the editor shows test-links.html as saved.
Why does my test page still show only one link?
Click the second link
The existing packaged app window is the test subject. It currently shows /first-link, which gives you a clear before state.
Before you click the lower link, consider whether the current window will change or another app window will open.
- Click the lower link on the refreshed test page.
- Approve the browser's request to open the packaged app if a prompt appears.
You'll see the existing app window come to the foreground. Its displayed path changes from /first-link to /second-link.
No second app window opens. That's the running-app case solved with one process.
Why does one window update?
The single-instance lock ensures the original process remains in control. Any new destination is routed into that active process.
- On macOS, open-url delivers the new URL to the running app.
- On Windows and Linux, second-instance carries the new launch arguments to the original process.
- The shared handler focuses the existing BrowserWindow and displays the new path.
Still seeing the first path?
- Confirm the lower link in test-links.html targets myapp://open/second-link.
- Approve the browser's request to open the packaged app if it appears.
- If another window opens, verify the window showing /first-link belongs to the installed packaged app.
Why does my running app still show the first path?
Secret mission
Run a Repeated-Link Regression Test
A deep-link handler can work once but stumble when links arrive in quick succession. Run both existing links against the packaged app to confirm that the newest path takes over without another app window opening.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep the app installed, pause it, or remove it completely. This local Electron project has no ongoing service costs.
Resources you used:
- The installed packaged Electron app registered to handle myapp:// links.
- The local myapp-link-demo/ project folder.
Keep everything running
No action needed. Choose this if you are still testing how web links open your packaged app.
The installed app remains registered as the handler for myapp:// links. Your project files remain available in myapp-link-demo/.
Keeping everything costs nothing because the project uses only local files and processes.
Pause - I'll come back to this later
Stop the running app while preserving its protocol registration and project files.
- Quit the packaged app using its application menu.
The app window closes. The installed handler and the myapp-link-demo/ folder remain ready for your next session.
Delete - I don't want to use this again
Complete removal covers the installed app and the local project folder. Removing the installed app stops the operating system from opening it for myapp:// links.
macOS
- Quit the packaged app from its application menu.
- Open the Applications folder in Finder.
- Locate the packaged app you installed from myapp-link-demo/dist/.
- Move the packaged app to Trash.
- Empty Trash.
- Return to the existing test-links.html page in your browser.
- Click the link to myapp://open/second-link.
The packaged app should stay closed. This confirms the installed link handler is gone.
- Return to your code editor.
- Use your editor's file-reveal action for myapp-link-demo/ to show the folder in Finder.
- Close your code editor.
- Move the myapp-link-demo/ folder to Trash.
- Empty Trash again.
- Search Finder for myapp-link-demo to confirm the folder no longer exists.
You should find no myapp-link-demo/ folder. Your packaged app and its project files are now removed.
Windows
- Quit the packaged app from its application menu.
- Open the installed applications page in Windows Settings.
- Locate the packaged app you installed from myapp-link-demo/dist/.
- Choose the app's uninstall control.
- Confirm the uninstall request.
- Return to the existing test-links.html page in your browser.
- Click the link to myapp://open/second-link.
The packaged app should stay closed. This confirms the installed link handler is gone.
- Return to your code editor.
- Use your editor's file-reveal action for myapp-link-demo/ to show the folder in File Explorer.
- Close your code editor.
- Delete the myapp-link-demo/ folder.
- Empty the Recycle Bin.
- Search File Explorer for myapp-link-demo to confirm the folder no longer exists.
You should find no myapp-link-demo/ folder. Your packaged app and its project files are now removed.
Linux
Linux packages may be installed through a software manager or kept as a standalone app file. Follow the removal path that matches how you installed the package.
- Quit the packaged app from its application menu.
- Open your installed software manager if you used it to install the package.
- Locate the app installed from the package in myapp-link-demo/dist/.
- Choose the app's removal control.
- Confirm the removal request.
- Move the installed app file to Trash if you used a standalone package.
- Return to the existing test-links.html page in your browser.
- Click the link to myapp://open/second-link.
The packaged app should stay closed. This confirms the installed link handler is gone.
- Return to your code editor.
- Use your editor's file-reveal action for myapp-link-demo/ to show the folder in your file manager.
- Close your code editor.
- Move the myapp-link-demo/ folder to Trash.
- Empty Trash.
- Search your file manager for myapp-link-demo to confirm the folder no longer exists.
You should find no myapp-link-demo/ folder. Your packaged app and its project files are now removed.
Nice Work!
Nice Work!
You made a web link launch your packaged Electron app at the requested path. Your final test proved that a second link focuses the existing window without starting another copy.
You've learned how to:
- Package an Electron desktop app with the myapp:// custom link scheme registered with the operating system.
- Route incoming links from startup arguments or macOS events into the main process. The app window now displays the requested link path.
- Use a single-instance lock to prevent duplicate app windows. The second link focuses the existing window before updating it to /second-link.
- Secret Mission: Complete an optional challenge to push your custom-link handling skills further.
Ready to quiz yourself?