Event-Driven Integration Guide
Overview
The MasterKey2 SDK provides a complete event-driven architecture that allows you to build completely decoupled authentication UIs. Instead of using callbacks, you listen to events and update your UI accordingly.
This guide covers the why and how of the event-driven approach. For the full API reference (all methods, types, components, config), see SDK_DOCUMENTATION.md.
Why Event-Driven?
Traditional Callback Approach
await auth.passkey.authenticate({
onSuccess: (result) => { /* update UI */ },
onError: (error) => { /* update UI */ },
});
Limitations:
- Callbacks are scoped to one call site
- Hard to coordinate multiple UI components
- Difficult to add cross-cutting concerns (analytics, logging)
- Tight coupling between trigger and handler
Event-Driven Approach
// Set up listeners once
auth.on(MK2Events.AUTH_SUCCESS, (e) => { /* update UI */ });
auth.on(MK2Events.AUTH_ERROR, (e) => { /* update UI */ });
// Trigger actions without callbacks
await auth.passkey.authenticate(); // Events handle the rest
Benefits:
- Complete decoupling — listeners don’t know about triggers
- Multiple listeners per event
- Easy to add/remove listeners at runtime
- Natural fit for component-based frameworks (React, Vue, Svelte)
- Cross-cutting concerns (analytics, logging) are trivially added
When to Use Which Approach
| Approach | Best for |
|---|---|
| Callbacks | Simple apps, single handler per action, quick prototyping |
| Events | Multi-component UIs, framework integration, analytics, complex flows |
| Web Components | Zero-JS HTML, drop-in UI, server-rendered pages |
All three work together. You can mix callbacks and events in the same app — both fire for every action.
Event System API
import MasterKey2, { MK2Events } from './masterkey2.js';
const auth = new MasterKey2({
apiBaseUrl: 'https://auth.example.com'
});
// Add event listener
auth.on(MK2Events.AUTH_SUCCESS, handler);
// Remove event listener
auth.off(MK2Events.AUTH_SUCCESS, handler);
// One-time listener (auto-removes after first call)
auth.once(MK2Events.AUTH_SUCCESS, handler);
All handlers receive a CustomEvent with a typed detail property:
auth.on(MK2Events.QR_GENERATED, (event) => {
console.log(event.detail.qrUrl);
console.log(event.detail.challengeId);
});
Events Reference
Global Events
These fire for any authentication method (passkey, QR, password). Use them for common actions like redirecting or showing errors.
MK2Events.AUTH_SUCCESS
Fired when any authentication succeeds.
auth.on(MK2Events.AUTH_SUCCESS, (e) => {
console.log('User:', e.detail.user);
window.location.href = e.detail.redirectUrl || '/';
});
Detail:
{
redirectUrl?: string;
user?: {
id: string; // bv-masterkey2 internal ID
external_id: string; // Tenant-provided identifier (email, username, etc.)
display_name?: string;
};
}
MK2Events.AUTH_ERROR
Fired when any authentication fails.
auth.on(MK2Events.AUTH_ERROR, (e) => {
if (e.detail.code === 'user_cancelled') return; // User dismissed dialog
showErrorMessage(e.detail.error.message);
});
Detail:
{
error: Error | string;
code?: string; // MK2ErrorCode value (e.g. 'user_cancelled', 'server_unreachable')
}
Passkey Events
MK2Events.PASSKEY_START
Fired when passkey authentication begins (before the browser dialog appears).
auth.on(MK2Events.PASSKEY_START, (e) => {
showSpinner();
disableButton();
});
Detail: { timestamp: number }
MK2Events.PASSKEY_SUCCESS
Fired when passkey authentication succeeds. AUTH_SUCCESS also fires.
auth.on(MK2Events.PASSKEY_SUCCESS, (e) => {
console.log('Passkey auth for:', e.detail.user?.external_id);
});
Detail:
{
redirectUrl?: string;
user?: { id: string; external_id: string; display_name?: string };
}
MK2Events.PASSKEY_ERROR
Fired when passkey authentication fails. AUTH_ERROR also fires.
auth.on(MK2Events.PASSKEY_ERROR, (e) => {
if (e.detail.code === 'user_cancelled') return;
console.error('Passkey error:', e.detail.error.message);
});
Detail: { error: Error | string, code?: string }
QR Code Events
MK2Events.QR_GENERATED
Fired when a QR code is created and ready to display.
auth.on(MK2Events.QR_GENERATED, (e) => {
document.getElementById('qr-img').src = e.detail.qrUrl;
});
Detail: { qrUrl: string, challengeId: string }
MK2Events.QR_STATUS_CHANGE
Fired when the cross-device status changes (mobile scanned, completed, etc.).
auth.on(MK2Events.QR_STATUS_CHANGE, (e) => {
if (e.detail.status === 'scanned') {
showMessage('QR scanned! Complete authentication on your phone...');
}
});
Detail: { status: string, message?: string }
Status values: pending | scanned | completed | expired | cancelled | failed | error
MK2Events.QR_SUCCESS
Fired when QR authentication completes. AUTH_SUCCESS also fires.
Detail:
{
redirectUrl?: string;
user?: { id: string; external_id: string; display_name?: string };
}
MK2Events.QR_ERROR
Fired when QR authentication fails. AUTH_ERROR also fires for non-cancellation errors.
When code is 'user_cancelled', the mobile user dismissed the WebAuthn prompt. The composite components auto-regenerate a fresh QR in this case.
Detail: { error: Error | string, code?: string }
MK2Events.QR_EXPIRED
Fired when the QR code expires (client-side timeout).
Detail: { challengeId: string }
MK2Events.QR_REFRESHED
Fired when a QR code is refreshed (manually via session.refresh() or automatically before expiration).
Detail: { qrUrl: string, challengeId: string }
Passkey Management Events
MK2Events.PASSKEY_ADDED
Fired when a new passkey is successfully registered.
Detail: { passkeyId: string }
MK2Events.PASSKEY_DELETED
Fired when a passkey is deleted.
Detail: { passkeyId: string }
MK2Events.PASSKEYS_LOADED
Fired when the passkey list is fetched.
Detail: { count: number }
Event Flow Diagrams
Passkey Authentication
User clicks "Sign in"
|
auth.passkey.authenticate()
|
PASSKEY_START -----> Update UI (show spinner, disable button)
|
Browser WebAuthn dialog
|
+--[user completes]----> PASSKEY_SUCCESS --> AUTH_SUCCESS --> redirect
|
+--[user dismisses]----> PASSKEY_ERROR (code: user_cancelled)
| AUTH_ERROR (code: user_cancelled)
| --> reset UI silently
|
+--[no credential]-----> PASSKEY_ERROR (code: credential_not_found)
AUTH_ERROR --> show "register a passkey" prompt
QR Code Authentication
User clicks "QR Login"
|
auth.qr.startAuth()
|
QR_GENERATED -----------> Display QR image
|
WebSocket connects (falls back to polling after 2 min)
|
QR_STATUS_CHANGE --------> Update status text
|
+--[mobile scans]-------> QR_STATUS_CHANGE (status: scanned)
| --> show "completing on phone..."
|
| +--[mobile completes]--> QR_SUCCESS --> AUTH_SUCCESS --> redirect
| |
| +--[mobile cancels]----> QR_ERROR (code: user_cancelled)
| --> auto-regenerate QR (composite components)
|
+--[timeout]-------------> QR_EXPIRED --> show refresh prompt
| (auto-refreshes 1 min before expiration)
|
+--[network error]-------> QR_ERROR (code: server_unreachable)
AUTH_ERROR --> show error
Using Error Codes in Event Handlers
The code field in error events is a MK2ErrorCode value. Use it to drive differentiated UX:
import { MK2Events, MK2ErrorCode } from './masterkey2.js';
auth.on(MK2Events.AUTH_ERROR, (e) => {
switch (e.detail.code) {
case 'user_cancelled':
// User dismissed dialog -- reset UI silently, no error message
resetButton();
break;
case 'server_unreachable':
showBanner('Cannot reach authentication server. Check your connection.');
break;
case 'credential_not_found':
showBanner('No passkey found for this account. Register one first.');
break;
case 'user_disabled':
showBanner('Your account has been suspended. Contact support.');
break;
case 'challenge_expired':
// QR_EXPIRED also fires for this case
break;
default:
showBanner('Authentication failed: ' + e.detail.error.message);
}
});
Complete Examples
Example 1: Simple Login with Global Events
import MasterKey2, { MK2Events } from './masterkey2.js';
const auth = new MasterKey2({
apiBaseUrl: 'https://auth.example.com'
});
// One handler for ALL auth methods
auth.on(MK2Events.AUTH_SUCCESS, (e) => {
window.location.href = e.detail.redirectUrl || '/';
});
// One error handler for ALL auth methods
auth.on(MK2Events.AUTH_ERROR, (e) => {
if (e.detail.code === 'user_cancelled') return;
document.getElementById('error').textContent = e.detail.error.message;
});
// Button click -- NO callbacks needed
document.getElementById('login-btn').onclick = () => {
auth.passkey.authenticate(); // Events handle the rest
};
Example 2: QR Code with Status Updates
const qrImg = document.getElementById('qr-img');
const statusEl = document.getElementById('status');
auth.on(MK2Events.QR_GENERATED, (e) => {
qrImg.src = e.detail.qrUrl;
qrImg.style.display = 'block';
statusEl.textContent = 'Scan the QR code with your phone';
});
auth.on(MK2Events.QR_STATUS_CHANGE, (e) => {
if (e.detail.status === 'scanned') {
statusEl.textContent = 'QR scanned! Complete authentication on your phone...';
qrImg.style.opacity = '0.3';
}
});
auth.on(MK2Events.QR_REFRESHED, (e) => {
qrImg.src = e.detail.qrUrl;
qrImg.style.opacity = '1';
statusEl.textContent = 'Scan the new QR code';
});
auth.on(MK2Events.QR_EXPIRED, () => {
statusEl.textContent = 'QR expired. Generating a new one...';
});
auth.on(MK2Events.QR_SUCCESS, (e) => {
statusEl.textContent = 'Success! Redirecting...';
});
// Start QR auth -- events handle everything
document.getElementById('qr-btn').onclick = () => {
auth.qr.startAuth();
};
Example 3: Multiple Components Listening
A key strength of events: multiple independent components can react to the same event without knowing about each other.
// Component 1: Status indicator
class StatusIndicator {
constructor(auth) {
this.el = document.getElementById('status-indicator');
auth.on(MK2Events.AUTH_SUCCESS, () => {
this.el.textContent = 'Authenticated';
this.el.className = 'success';
});
auth.on(MK2Events.AUTH_ERROR, (e) => {
if (e.detail.code === 'user_cancelled') return;
this.el.textContent = e.detail.error.message;
this.el.className = 'error';
});
}
}
// Component 2: Loading spinner
class LoadingSpinner {
constructor(auth) {
this.el = document.getElementById('spinner');
auth.on(MK2Events.PASSKEY_START, () => {
this.el.style.display = 'block';
});
// Hide on any terminal state
auth.on(MK2Events.PASSKEY_SUCCESS, () => this.hide());
auth.on(MK2Events.PASSKEY_ERROR, () => this.hide());
}
hide() { this.el.style.display = 'none'; }
}
// Component 3: Analytics tracker
class AnalyticsTracker {
constructor(auth) {
auth.on(MK2Events.AUTH_SUCCESS, (e) => {
analytics.track('auth_success', { user: e.detail.user?.external_id });
});
auth.on(MK2Events.AUTH_ERROR, (e) => {
if (e.detail.code === 'user_cancelled') return; // Don't track dismissals
analytics.track('auth_error', { code: e.detail.code });
});
}
}
// Initialize -- each component subscribes independently
const auth = new MasterKey2({ apiBaseUrl: '...' });
new StatusIndicator(auth);
new LoadingSpinner(auth);
new AnalyticsTracker(auth);
// One trigger, all three components respond
document.getElementById('login-btn').onclick = () => auth.passkey.authenticate();
Example 4: React Integration
import { useEffect, useState, useRef } from 'react';
import MasterKey2, { MK2Events } from './masterkey2.js';
function useAuth(apiBaseUrl) {
const authRef = useRef(null);
if (!authRef.current) {
authRef.current = new MasterKey2({ apiBaseUrl });
}
return authRef.current;
}
function LoginPage() {
const auth = useAuth('https://auth.example.com');
const [status, setStatus] = useState('');
const [error, setError] = useState('');
const [qrUrl, setQrUrl] = useState('');
useEffect(() => {
const onSuccess = (e) => {
window.location.href = e.detail.redirectUrl || '/';
};
const onError = (e) => {
if (e.detail.code === 'user_cancelled') {
setStatus('');
return;
}
setError(e.detail.error.message);
};
const onQR = (e) => {
setQrUrl(e.detail.qrUrl);
setStatus('Scan the QR code');
};
const onQRStatus = (e) => {
if (e.detail.status === 'scanned') {
setStatus('QR scanned! Completing...');
}
};
auth.on(MK2Events.AUTH_SUCCESS, onSuccess);
auth.on(MK2Events.AUTH_ERROR, onError);
auth.on(MK2Events.QR_GENERATED, onQR);
auth.on(MK2Events.QR_STATUS_CHANGE, onQRStatus);
// Cleanup on unmount
return () => {
auth.off(MK2Events.AUTH_SUCCESS, onSuccess);
auth.off(MK2Events.AUTH_ERROR, onError);
auth.off(MK2Events.QR_GENERATED, onQR);
auth.off(MK2Events.QR_STATUS_CHANGE, onQRStatus);
};
}, [auth]);
return (
<div>
<button onClick={() => auth.passkey.authenticate()}>Sign in with Passkey</button>
<button onClick={() => auth.qr.startAuth()}>Sign in with QR Code</button>
{qrUrl && <img src={qrUrl} alt="QR Code" />}
{status && <p>{status}</p>}
{error && <p className="error">{error}</p>}
</div>
);
}
Example 5: Vue 3 Composition API
<template>
<div>
<button @click="loginWithPasskey">Sign in with Passkey</button>
<button @click="loginWithQR">Sign in with QR Code</button>
<img v-if="qrUrl" :src="qrUrl" alt="QR Code" />
<p v-if="status">{{ status }}</p>
<p v-if="error" class="error">{{ error }}</p>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import MasterKey2, { MK2Events } from './masterkey2.js';
const status = ref('');
const error = ref('');
const qrUrl = ref('');
const auth = new MasterKey2({
apiBaseUrl: 'https://auth.example.com'
});
const handlers = {
success: (e) => { window.location.href = e.detail.redirectUrl || '/'; },
error: (e) => {
if (e.detail.code === 'user_cancelled') { status.value = ''; return; }
error.value = e.detail.error.message;
},
qr: (e) => { qrUrl.value = e.detail.qrUrl; status.value = 'Scan the QR code'; },
qrStatus: (e) => {
if (e.detail.status === 'scanned') status.value = 'QR scanned! Completing...';
},
};
onMounted(() => {
auth.on(MK2Events.AUTH_SUCCESS, handlers.success);
auth.on(MK2Events.AUTH_ERROR, handlers.error);
auth.on(MK2Events.QR_GENERATED, handlers.qr);
auth.on(MK2Events.QR_STATUS_CHANGE, handlers.qrStatus);
});
onUnmounted(() => {
auth.off(MK2Events.AUTH_SUCCESS, handlers.success);
auth.off(MK2Events.AUTH_ERROR, handlers.error);
auth.off(MK2Events.QR_GENERATED, handlers.qr);
auth.off(MK2Events.QR_STATUS_CHANGE, handlers.qrStatus);
});
const loginWithPasskey = () => auth.passkey.authenticate();
const loginWithQR = () => auth.qr.startAuth();
</script>
Example 6: Svelte
<script>
import { onMount, onDestroy } from 'svelte';
import MasterKey2, { MK2Events } from './masterkey2.js';
let status = '';
let error = '';
let qrUrl = '';
const auth = new MasterKey2({
apiBaseUrl: 'https://auth.example.com'
});
const onSuccess = (e) => { window.location.href = e.detail.redirectUrl || '/'; };
const onError = (e) => {
if (e.detail.code === 'user_cancelled') { status = ''; return; }
error = e.detail.error.message;
};
const onQR = (e) => { qrUrl = e.detail.qrUrl; status = 'Scan the QR code'; };
const onQRStatus = (e) => {
if (e.detail.status === 'scanned') status = 'QR scanned! Completing...';
};
onMount(() => {
auth.on(MK2Events.AUTH_SUCCESS, onSuccess);
auth.on(MK2Events.AUTH_ERROR, onError);
auth.on(MK2Events.QR_GENERATED, onQR);
auth.on(MK2Events.QR_STATUS_CHANGE, onQRStatus);
});
onDestroy(() => {
auth.off(MK2Events.AUTH_SUCCESS, onSuccess);
auth.off(MK2Events.AUTH_ERROR, onError);
auth.off(MK2Events.QR_GENERATED, onQR);
auth.off(MK2Events.QR_STATUS_CHANGE, onQRStatus);
});
</script>
<button on:click={() => auth.passkey.authenticate()}>Sign in with Passkey</button>
<button on:click={() => auth.qr.startAuth()}>Sign in with QR Code</button>
{#if qrUrl}<img src={qrUrl} alt="QR Code" />{/if}
{#if status}<p>{status}</p>{/if}
{#if error}<p class="error">{error}</p>{/if}
Best Practices
1. Use Global Events for Common Actions
// One handler for all auth methods
auth.on(MK2Events.AUTH_SUCCESS, (e) => {
window.location.href = e.detail.redirectUrl || '/';
});
// Don't duplicate for each method:
// auth.on(MK2Events.PASSKEY_SUCCESS, ...) // redundant
// auth.on(MK2Events.QR_SUCCESS, ...) // redundant
2. Always Handle user_cancelled
Users dismissing the WebAuthn dialog is normal, not an error. Don’t show error UI for it:
auth.on(MK2Events.AUTH_ERROR, (e) => {
if (e.detail.code === 'user_cancelled') {
resetUI(); // Silently reset
return;
}
showError(e.detail.error.message);
});
3. Clean Up Listeners in SPAs
Prevent memory leaks by removing listeners when components unmount:
// React
useEffect(() => {
const handler = (e) => { /* ... */ };
auth.on(MK2Events.AUTH_SUCCESS, handler);
return () => auth.off(MK2Events.AUTH_SUCCESS, handler);
}, []);
// Vue
onMounted(() => auth.on(MK2Events.AUTH_SUCCESS, handler));
onUnmounted(() => auth.off(MK2Events.AUTH_SUCCESS, handler));
// Svelte
onMount(() => auth.on(MK2Events.AUTH_SUCCESS, handler));
onDestroy(() => auth.off(MK2Events.AUTH_SUCCESS, handler));
4. Use once() for One-Shot Analytics
auth.once(MK2Events.AUTH_SUCCESS, (e) => {
analytics.track('first_login', {
method: 'passkey',
user: e.detail.user?.external_id
});
});
5. Use Specific Events for Custom Behavior
Global events handle the common case. Specific events let you add targeted behavior:
// Specific: play a sound when QR is scanned
auth.on(MK2Events.QR_STATUS_CHANGE, (e) => {
if (e.detail.status === 'scanned') {
playSound('beep');
}
});
// Global: redirect on any auth success
auth.on(MK2Events.AUTH_SUCCESS, (e) => {
window.location.href = e.detail.redirectUrl || '/';
});
6. Combine with Composite Components
Events from the JS SDK and DOM events from composite components are independent. You can use both:
<masterkey2-authenticate api-base-url="https://auth.example.com"></masterkey2-authenticate>
<script type="module">
import MasterKey2, { MK2Events } from './masterkey2.js';
// Component handles UI rendering and browser detection
// JS events add cross-cutting concerns
const auth = new MasterKey2({ apiBaseUrl: 'https://auth.example.com' });
auth.on(MK2Events.AUTH_SUCCESS, (e) => {
analytics.track('login', { user: e.detail.user?.external_id });
});
// DOM event from the component for redirect
document.querySelector('masterkey2-authenticate')
.addEventListener('success', (e) => {
window.location.href = e.detail.redirectUrl || '/';
});
</script>
Debugging Events
Enable Debug Mode
const auth = new MasterKey2({
apiBaseUrl: 'https://auth.example.com',
debug: true // All events logged as [MasterKey2 SDK] Event: mk2:... { ... }
});
Log All Events
Object.values(MK2Events).forEach(eventName => {
auth.on(eventName, (e) => {
console.log(`[${eventName}]`, e.detail);
});
});
Event Monitor Component
Build a live event log for development:
class EventMonitor {
constructor(auth, containerEl) {
Object.values(MK2Events).forEach(eventName => {
auth.on(eventName, (e) => {
const entry = document.createElement('div');
entry.textContent = `${new Date().toISOString()} ${eventName}`;
entry.title = JSON.stringify(e.detail, null, 2);
containerEl.prepend(entry);
});
});
}
}
// Usage:
new EventMonitor(auth, document.getElementById('event-log'));
Choosing an Integration Approach
| Callbacks | Web Components | Events | |
|---|---|---|---|
| Coupling | Medium | Low | Very Low |
| Setup | Pass options to method | Add HTML element | Register listeners once |
| Multiple listeners | No | Via DOM events | Yes |
| Framework fit | Any | Server-rendered, vanilla JS | React, Vue, Svelte, Angular |
| Browser detection | Manual | Automatic (composite) | Manual |
| Best for | Simple integrations | Drop-in UI | Complex apps, analytics |
All three approaches work together and fire simultaneously — choose what fits your architecture.
Real-World References
- bv-masterkey2 admin login (
templates/pages/admin/login.html) — same-origin integration using<masterkey2-authenticate>with a DOMsuccessevent listener that callsPOST /admin/passkey-loginto create an admin session. - bv-masterkey2-web (
bv-masterkey2-web/src/components/WebAuthnLogin.svelte) — cross-origin Svelte wrapper using DOM events from the composite component with differentiated error handling perMK2ErrorCode.