# Phone/passkey QR sign-in `PhoneSignIn` is an optional browser helper for apps that already use `Api`. It does not replace username/password sign-in. It keeps the QR flow's guest token separate from the app's signed-in token, and verifies the approved token with the API before adopting it. ## Server prerequisites 1. The spoke must run the API WebAuthN QR endpoints (`qrStart`, `qrStatus`, `qrScan`, `qrChoose`, `qrApprove`, `qrDeny`) and the API-hosted `/api/webAuthN/setup.php` and `/api/webAuthN/qr.php` pages. 2. The spoke database must have `auth.authenticator_handle` and `webauthn_credentials` with a non-unique `auth` index. Verify the schema before enabling a new spoke; do not copy the API repository's destructive `_scratch/comp_y.sql` example into a migration. 3. All API replicas must share Redis for the short-lived QR session. The credential records remain in the spoke's MySQL database. Private passkey material stays on the user's device. 4. The API and setup pages must be HTTPS. WebAuthN runs on the spoke API host, which is the relying-party ID. The app may be on another allowed origin, such as `lynk-forms.apps.auxiliumgroup.com`. Browser WebAuthN does **not** accept wildcard relying-party origins. Check the API's CORS policy for the app origin and its `Authorization` header. ## Add the client flow ```ts import {Api, PhoneSignIn} from '@auxilium/datalynk-client'; const spoke = 'srsm'; const api = new Api('https://srsm.auxiliumgroup.com', {socket: false}); const phone = new PhoneSignIn(api, spoke); // Keep your existing password button: await api.auth.login(spoke, login, password). // Link/QR for first-time enrollment; open on the phone that will hold the passkey. setupLink.href = phone.setupUrl; async function showNewCode() { const qr = await phone.start(); renderQr(qr.url); // Use your app's QR renderer. number.textContent = qr.number; // User must match this on their phone. const stop = setInterval(async () => { try { const status = await phone.status(qr.code); if(status.state === 'approved' && status.token) { clearInterval(stop); if(await phone.adopt(status.token)) enterApp(); else showPasswordSignIn('That phone session expired. Sign in again.'); } else if(status.state === 'expired') { clearInterval(stop); showNewCode(); } } catch(error) { clearInterval(stop); showPasswordSignIn('Phone sign-in is unavailable; use your password.'); } }, 2000); } ``` Start a fresh QR code when its `ttlMs` elapses or after an error. Never put the approved user token in a URL, log, or QR. `PhoneSignIn` sends no ambient API cookies and never stores its guest token as the app session. ## Reauthentication after revocation A locally saved JWT can still be unexpired after the server cancels its token. Apps with a custom sign-in screen should validate a restored session before loading protected data, clear it on HTTP 401, and offer password or phone sign-in again. For such apps, set `manageSession: false` so the SDK does not also reload the page; the app owns this transition. Apps using the SDK's `auth.handleLogin()` can keep default session management. ```ts const api = new Api(spokeUrl, {manageSession: false}); if(api.token) { try { await api.auth.current(api.token, true); // Force a server check, not cached profile. } catch(error: any) { if(error.code !== 401) throw error; api.token = null; } } if(!api.token) showPasswordOrPhoneSignIn(); ``` On the API-hosted setup page, a revoked session returns the user to its username/password form. The account may register multiple passkeys; the page lists registered devices, but credential removal/renaming is not yet exposed. The Sint Maarten Platform implementation is a working reference: `st-maarten-public-health/Platform/modules/auth/signin.js` and `Platform/core/datalynk.js`. Test a new integration on its intended spoke, not on Vista by default.