Documentation

Passkeys: dialog, autofill, migration

Passkey-first MFA in a few lines: a ready-made approval dialog for any guarded action, passkeys offered in the email field's autofill, and a once-per-browser "Add a passkey" offer that moves password users over.

Drop-in approval dialog

<touchque-approval> (any page) and <TouchqueApproval> (React) call a route guarded by any server SDK and open only when it asks for an approval. They show the number to pick on the phone, the first-time link QR, the offline QR and code, the passkey step and refusals with their case code. Restyle with --tq-approval-* CSS variables; every text can be replaced.

Passkey autofill

The browser lists the user's passkeys for your site in the email field's suggestions (WebAuthn conditional mediation); picking one signs in with no email typed. It never opens a modal: browsers without it show nothing, so keep your normal form. The relay is asked for options with no email, which accept any passkey of your site, and the passkey picked names the account.

Adding a passkey proves the person, not the cookie. A user with a linked phone approves the new passkey there first (PASSKEY_ENROLL, number matched); touchqueRouter runs it and passkeys.upgrade() shows the number through onStep. Without a phone, prove the person yourself (the password just typed, or a sign-in from the last few minutes) before your relay's register route answers.
Moving your users off passwords
#StepHow
1Turn on passkeys for your siteDashboard → App user policy → "Passkeys on your site": your domain (RP ID) and sign-in page origins, then "Passwordless sign-in". Nothing changes for users yet.
2Mount the relaytouchqueRouter (Node), the Flask/Django/FastAPI blueprint, TouchqueRelay::routes (Laravel), NewRelayHandler (Go), MapTouchqueRelay (ASP.NET Core), the Spring Boot starter's relay or Touchque::Rack::Relay (Rails, Rack), with getUserId for the signed-in user. It serves sign-in, autofill, registration and approvals.
3Add autofill to the sign-in formautocomplete="username webauthn" on the email field and passkeys.autofill() when the form shows. Users who have a passkey pick it from the field; everyone else signs in as before.
4Offer "Add a passkey" after password sign-insshouldOfferPasskey() after a password or email-link sign-in, then passkeys.upgrade() or dismissPasskeyOffer(). Once per browser; "Not now" is remembered there.
5Watch adoptionPASSKEY_REGISTERED / PASSKEY_DELETED webhooks, or an Action on passkey.added, tell you who moved; GET /webauthn/credentials lists a user's passkeys.
6Then require it where it mattersRequire a passkey for critical actions (policy) or for admins (the require_passkey_for_role Action template). Keep the password and the phone as recovery until most users have two passkeys; Never Locked Out covers a lost phone.

@touchque/web 2.2.0, @touchque/react 2.1.0, server SDKs 5.2.0: the relay's usernameless options came in 4.0.0, and the names on this page (requireTouchque, touchqueRouter) are those of 5.0 (4.x spelled them TouchQue).

Server: relay and a guarded route
import { touchqueRouter, requireTouchque } from '@touchque/node';

// The relay the browser calls: passkey sign-in (autofill too), registration, approvals
app.use('/touchque', touchqueRouter(tq, {
  getUserId: (req) => req.session.user?.email,       // the signed-in user, for registering passkeys
  getLoginUser: (req) => req.session.passwordOk,     // the user who passed your password step
  onAuthenticated: (req, res, { externalUsername }) => { req.session.user = { email: externalUsername }; },
}));

// An action that needs the phone: one line
app.post('/api/transfer', requireTouchque('SEND_MONEY'), sendMoney);
Browser: the approval dialog
<!-- Any page, no framework -->
<touchque-approval base-url="/touchque"></touchque-approval>

<script type="module">
  import { defineTouchqueApproval } from '@touchque/web';
  defineTouchqueApproval();
  const dialog = document.querySelector('touchque-approval');
  const res = await dialog.fetch('/api/transfer', { method: 'POST', body });
</script>
Passkey autofill
<input type="email" name="email" autocomplete="username webauthn">

<script type="module">
  import { createTouchqueWeb } from '@touchque/web';
  const tq = createTouchqueWeb({ baseUrl: '/touchque' });
  const result = await tq.passkeys.autofill({ input: 'input[name=email]' });
  if (result?.ok) location.href = '/home';   // null: no autofill here, keep the form
</script>
Password → passkey offer
import { createTouchqueWeb, shouldOfferPasskey, dismissPasskeyOffer } from '@touchque/web';
const tq = createTouchqueWeb({ baseUrl: '/touchque', credentials: 'include' });

// Right after a password or email-link sign-in, before you leave the page:
if (await shouldOfferPasskey({ user: email })) {
  showOffer({
    add: () => tq.passkeys.upgrade({ user: email, onStep: showPhoneNumber }), // a linked phone approves first
    notNow: () => dismissPasskeyOffer(email),                                 // remembered in this browser
  });
}