Deep Links
Register the URL scheme and route incoming intents into the app.
The template registers a custom URL scheme so the OS can hand soar-electron://
links to the app. Incoming URLs are parsed into typed app intents, checked
against a route allow-list, and delivered to the renderer as validated Conveyor
events. Two real consumers depend on this path: Supabase auth redirects and the
billing success bounce.
The scheme is soar-electron today
(PROTOCOL_SCHEME in shared/constants.ts). pnpm rebrand --scheme <name>
rewrites it everywhere — see Branding. Every
example below uses soar-electron://.
Registration
The scheme is registered in two places that must agree:
| Where | What it does |
|---|---|
lib/main/main.ts | app.setAsDefaultProtocolClient(PROTOCOL_SCHEME) at startup registers the app as the OS handler. |
electron-builder.yml | The protocols block bakes the scheme into the installer so packaged builds own it after install. |
In development the registration is special-cased. Because the running binary is
Electron itself, main.ts passes the Electron executable plus the project
path so the OS can relaunch the app correctly:
// lib/main/main.ts
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient(PROTOCOL_SCHEME, process.execPath, [resolve(process.argv[1])])
}
} else {
app.setAsDefaultProtocolClient(PROTOCOL_SCHEME)
}How a URL becomes a navigation
Every platform ultimately funnels into one parser, parseArgvForIntents
(lib/main/argv-intents.ts), which turns a soar-electron://<route> string
(or a --route=<route> flag) into a typed intent and drops anything that
doesn't validate.
The OS delivers the URL
- macOS fires
app.on('open-url'). This can arrive before the app is ready on a cold start, somain.tsbuffers URLs inpendingProtocolUrlsand drains them once initialized. - Windows / Linux deliver the URL as an argv entry to a second process
launch. The single-instance lock (
app.requestSingleInstanceLock()) routes that throughapp.on('second-instance'), which focuses the existing window and parses the argv.
Parse into a typed intent
parseArgvForIntents extracts the route and validates it with
appIntentSchema. The route must be a member of navigationRouteSchema (the
allow-list) or the intent is discarded:
// lib/conveyor/schemas/event-schema.ts
export const navigationRouteSchema = z.enum([
'/home',
'/showcase', '/showcase/components', '/showcase/file', '/showcase/todos', // [template-demo]
'/account', '/settings', '/about', '/logs', '/update', '/billing',
])Dispatch to the renderer
AppIntentService.dispatch() (lib/main/app-intents.ts) re-validates the intent
and sends it to the renderer as the app-intent Conveyor event. If the renderer
isn't ready yet, intents queue and flush once it signals readiness
(app-intents-ready) — so a link that cold-starts the app still lands on the
right route.
The renderer navigates
app/app.tsx subscribes to the app-intent event and calls the router. Because
the route was already validated against the allow-list, the renderer can trust
it.
Tray and menu navigation reuse the exact same dispatch path — a tray "Settings"
click and a soar-electron://settings URL are indistinguishable by the time
they reach the renderer.
The two real consumers
Deep links aren't only for demos — the auth and billing flows depend on them:
- Supabase auth redirects. Email confirmation and password reset return to the desktop app via the scheme. Add the redirect URL to your Supabase project so the bounce lands back in the app — see Supabase Setup and Authentication.
- Billing success bounce. After Creem checkout in the browser, the flow
returns to
soar-electron://billing?status=success&orderId=…(optionally via a hostedBILLING_SUCCESS_URLpage). The/billingroute then refreshes entitlement from Supabase — see Billing.
The bundled supabase/pay-success.sample.html still builds a soar://billing
link. Treat soar-electron:// as correct and fix the sample when you host it.
Testing a deep link
With the app running, trigger the scheme from a terminal:
open 'soar-electron://settings'start soar-electron://settingsxdg-open 'soar-electron://settings'The window should focus and navigate to Settings. Try soar-electron://about or
soar-electron://billing too. An unknown route (e.g.
soar-electron://nope) is parsed, fails allow-list validation, and is silently
ignored — that's the intended behavior.
In dev, OS protocol registration depends on the special-cased
setAsDefaultProtocolClient call above and can be flaky compared to a packaged
install (especially on Linux desktop environments). If a link doesn't fire in
dev, verify against a pnpm build:unpack build before assuming a bug — see
Troubleshooting.
