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:

WhereWhat it does
lib/main/main.tsapp.setAsDefaultProtocolClient(PROTOCOL_SCHEME) at startup registers the app as the OS handler.
electron-builder.ymlThe 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, so main.ts buffers URLs in pendingProtocolUrls and 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 through app.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 hosted BILLING_SUCCESS_URL page). The /billing route 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.

With the app running, trigger the scheme from a terminal:

open 'soar-electron://settings'
start soar-electron://settings
xdg-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.

On this page