Cross-Device Authentication (QR Login)
Enable users to sign in on a desktop device by scanning a QR code with their mobile phone (where their passkey is stored).
TryMellon provides a seamless, zero-friction flow for cross-device authentication without needing WebSockets or complex state synchronization on your end.
What “Bridge” means here
The word Bridge appears with three different meanings across the TryMellon surface. Whenever you read “bridge” in our docs, it refers to one of these:
- Bridge API — the SDK namespace
client.bridge.*and its backing HTTP endpoints/v1/bridge-{auth,enrollment}/*. Used to pair two devices with a short-lived secret (PIN) — seeentity-enrollment.mdfor the enrollment flow. - QR Bridge — the TryMellon-hosted approval page at
trymellonauth.com/bridge/<sessionId>. When a desktop user scans the QR produced below, the mobile device lands on this hosted page to approve. Configurable viaprimaryQrBaseUrlfor self-hosting. - CrossDeviceSession — the internal aggregate (7-state FSM) that models this flow on the backend. Not user-facing; referenced in ADRs and engineering docs as the authority for the state machine.
This guide covers the QR Bridge flow driven by the client.crossDevice.* SDK namespace (not client.bridge.*, which is the Bridge API). The underlying backend aggregate is the CrossDeviceSession.
1. Desktop: Initialize and Show QR
On the desktop interface (e.g., your login page), call start() to create a cross-device session. This returns a session_id and a qr_url.
approval_context (auth-link): To show a custom message on the mobile screen (e.g. “Access to orders”), send it when creating the session. The API accepts
POST /v1/auth/cross-device/initwith body{ "approval_context": "Your message" }(max 200 chars; use the sameAuthorization: Bearer <publishableKey>andOriginas for the SDK). The SDKstart()does not yet accept options; use the API directly for approval_context until the SDK exposes it.
import { TryMellon } from '@trymellon/js'
// 1. Create TryMellon Client
const clientResult = TryMellon.create({ appId: 'YOUR_APP_ID', publishableKey: 'cli_xxxx' });
if (!clientResult.ok) throw clientResult.error;
const client = clientResult.value;
// 2. Initialize cross-device session
const initResult = await client.crossDevice.start()
if (!initResult.ok) {
console.error("Failed to init QR flow:", initResult.error.message);
return;
}
const { session_id, qr_url, expires_at, polling_token } = initResult.value;
// 3. Render the QR code
// You can use libraries like 'qrcode.react', 'svelte-qrcode', or 'qrcode' to render 'qr_url'
renderQrCode(qr_url);
// 4. Wait for approval (Desktop waits here — uses SSE in browser, polling fallback otherwise)
const controller = new AbortController()
const pollResult = await client.crossDevice.waitForCompletion(
session_id,
controller.signal,
polling_token // authenticates the status subscription
)
if (!pollResult.ok) {
if (pollResult.error.code === 'TIMEOUT') {
console.error('QR code expired. Please refresh the page.')
}
return;
}
// 5. Success! Send the sessionToken to your backend
console.log('Session token:', pollResult.value.sessionToken)
[!WARNING] About
qr_urland the mobile app: The API returnsqr_urlin the form{baseUrl}/mobile-auth?session_id={session_id}. Your mobile web app must be deployed and its URL/origin allowed in the TryMellon dashboard for that application.
2. Mobile: Approve login screen
When the user scans the QR code, they are sent to your mobile page (e.g. /mobile-auth?session_id=...):
- GET context: Call
client.crossDevice.getContext(sessionId)to obtain session type, WebAuthn options, and optionallyapproval_context(a custom message set by the desktop app, e.g. “Authorize payment”) andapplication_namefrom the backend. - Show context: Display the application name and, if present, the
approval_contextso the user understands what they are approving before tapping the CTA. - Approve: Call
approve(sessionId). On success, show a confirmation message (e.g. “Done. You can close this window.”). On410/404(session expired or not found) or no passkey, prompt the user to go back and retry or register a passkey.
3. Mobile: Approve login (code)
On your mobile page (e.g., /mobile-auth), extract the ID and trigger the approval.
// 1. Extract session_id from URL
const urlParams = new URLSearchParams(window.location.search);
const sessionId = urlParams.get('session_id');
if (!sessionId) {
console.error("No session ID found in URL.");
return;
}
// 2. Create the TryMellon client (same as desktop)
const clientResult = TryMellon.create({ appId: 'YOUR_APP_ID', publishableKey: 'cli_xxxx' });
if (!clientResult.ok) throw clientResult.error;
const client = clientResult.value;
// 3. Trigger WebAuthn approval on mobile
const approveResult = await client.crossDevice.approve(sessionId)
if (approveResult.ok) {
// Passkey validated — show a confirmation, e.g. "Done. You can close this window."
alert('Process complete! You are now logged in on your desktop.')
} else {
alert('Failed to approve login: ' + approveResult.error.message)
}
RFC 8628 (Device Authorization Grant) mapping
The TryMellon cross-device flow is conceptually equivalent to the Device Authorization Grant (RFC 8628). If your team is familiar with device codes, here is the mapping:
| RFC 8628 | TryMellon cross-device |
|---|---|
device_code | session_id (UUID of the session) |
verification_uri | Your app’s base URL (e.g. https://app.yourdomain.com) |
verification_uri_complete | qr_url (full URL with ?session_id=... for the mobile-auth page) |
Poll POST /token with device_code | Poll GET /v1/auth/cross-device/status/:sessionId |
access_token in poll response | session_token in the body when status === 'completed' |
expires_in (device_code TTL) | Session TTL in Redis (e.g. 5 min); expires_at returned by init |
TryMellon does not expose user_code or interval. The SDK uses SSE (EventSource) in browser environments for push-based completion — no client-side polling interval needed. For Node.js or environments without EventSource, it falls back to polling at 3 s intervals with a 3-minute global timeout.
Tip: Pass the
polling_tokenfrominitResult.value.polling_tokenas the third argument towaitForCompletion(sessionId, signal, pollingToken)— this authenticates the polling/SSE request and is required by the rate-limited status endpoint.
QR Default Domain (Bridge)
Don’t have a /mobile-auth page yet? Use the QR default domain — TryMellon hosts the mobile approval screen for you.
With the bridge domain, start() returns a qr_url pointing to TryMellon’s hosted page. Your desktop app only needs the SDK; no mobile page deployment required.
When to use it
- Getting started fast: ship cross-device auth without building a mobile-auth page.
- POCs and demos: show the QR flow to stakeholders in minutes.
- Production with custom domain later: switch by changing
primary_qr_base_url— no user migration.
Migrating to your own domain
- Deploy your own
/mobile-authpage (see sections above for the mobile approval code). - In your app settings, set
primary_qr_base_urltohttps://yourdomain.com. - Add
https://yourdomain.comto Allowed origins.
Existing passkeys keep working — no re-registration needed.
[!TIP] For a complete integration guide with code snippets, dashboard config, and framework examples, see QR Default Domain — Integration Guide.
Under the Hood
start(): Creates a temporary challenge in the TryMellon Redis cache (valid for 5 minutes).waitForCompletion(sessionId, signal?, pollingToken?): In browser environments, uses SSE (Server-Sent Events) — the server pushes the completion event the instant the mobile device approves, with zero polling overhead. In Node.js or if SSE is unavailable, falls back to polling every 3 s. Gracefully times out after 3 minutes if the user abandons the flow. Pass anAbortSignalas the second argument to cancel early.approve(): Leverages the user’s mobile platform authenticator (FaceID / TouchID) to sign the challenge, converting it into a fullsessionTokenthat the desktop immediately receives.