Fluxer WebAuthn Passkey Architecture: Cross-Platform Implementation Guide
Fluxer implements a three-layer WebAuthn architecture that unifies browser-based and desktop-native passkey flows through a shared abstraction layer, automatically selecting platform-specific providers for macOS, Windows, and Linux while maintaining identical UI code across web and Electron clients.
The fluxerapp/fluxer repository delivers a comprehensive passkey authentication system that bridges web and desktop environments. This architecture leverages SimpleWebAuthn for browser compatibility while introducing a native abstraction layer for Electron-based desktop clients, enabling seamless biometric authentication across platforms without duplicating UI logic.
Three-Layer Architecture Overview
Fluxer’s passkey implementation is organized into three distinct layers that communicate through standardized JSON interfaces:
- Client Web Layer: Handles browser-based registration and authentication using
@simplewebauthn/browserviaWebAuthnUtils.tsx - Client Desktop Layer: Provides OS-level WebAuthn access through Electron’s main process, supporting both generic native modules and macOS-specific addons
- Server Layer: Generates challenges, verifies responses, and manages credential storage through
AuthMfaService.tsx
This separation allows the same React components to execute passkey ceremonies regardless of whether the user accesses Fluxer through a browser or the desktop application.
Client-Side Web Implementation
The web client implementation in fluxer_app/src/utils/WebAuthnUtils.tsx serves as the primary entry point for all passkey operations. It abstracts platform detection and delegates to the appropriate underlying implementation.
Platform Detection and Support
The assertWebAuthnSupported() function checks for WebAuthn availability by detecting the runtime environment:
// fluxer_app/src/utils/WebAuthnUtils.tsx
export async function assertWebAuthnSupported(): Promise<void> {
if (Platform.isElectron) {
const electronApi = getElectronAPI();
const nativeSupported = electronApi && (await electronApi.passkeyIsSupported?.());
if (nativeSupported) return;
}
if (browserSupportsWebAuthn()) return;
throw new Error('WebAuthn is not supported in this environment.');
}
When running inside Electron, the function queries the native bridge via passkeyIsSupported before falling back to standard browser capability detection.
Unified Registration and Authentication
The performRegistration() and performAuthentication() functions provide a single API for both platforms:
export async function performRegistration(
options: PublicKeyCredentialCreationOptionsJSON,
): Promise<RegistrationResponseJSON> {
await assertWebAuthnSupported();
if (Platform.isElectron) {
const electronApi = getElectronAPI();
const nativeSupported = electronApi && (await electronApi.passkeyIsSupported?.());
if (nativeSupported && electronApi.passkeyRegister) {
return electronApi.passkeyRegister(options); // ← native bridge
}
}
return await startRegistration({ optionsJSON: options }); // ← browser fallback
}
The performAuthentication() function follows an identical pattern, calling passkeyAuthenticate on the Electron API when available, otherwise invoking startAuthentication from SimpleWebAuthn.
Electron Preload Bridge
The Electron preload script in fluxer_desktop/src/preload/index.tsx exposes the native passkey API to the renderer process through secure IPC channels:
export const electronApi = {
passkeyIsSupported: (): Promise<boolean> =>
ipcRenderer.invoke('passkey-is-supported'),
passkeyAuthenticate: (options: PublicKeyCredentialRequestOptionsJSON): Promise<AuthenticationResponseJSON> =>
ipcRenderer.invoke('passkey-authenticate', options),
passkeyRegister: (options: PublicKeyCredentialCreationOptionsJSON): Promise<RegistrationResponseJSON> =>
ipcRenderer.invoke('passkey-register', options),
};
This bridge forwards all calls to the main process, ensuring that privileged OS operations remain isolated from the renderer while maintaining type safety through shared TypeScript definitions.
Main Process Provider Strategy
The main process implementation in fluxer_desktop/src/main/IpcHandlers.tsx implements a PasskeyProvider interface that dynamically selects the appropriate native implementation based on the operating system and build configuration.
Provider Selection Logic
The createPasskeyProvider() function determines which implementation to instantiate:
function createPasskeyProvider(): PasskeyProvider {
const macAddon = loadMacWebAuthnAddon();
if (macAddon) {
return createMacPasskeyProvider(macAddon); // macOS-specific addon
}
return createNativePasskeyProvider(); // generic native module
}
On macOS, the system attempts to load the electron-webauthn-mac addon for tighter integration with Touch ID and the Secure Enclave. On Windows and Linux, or when the macOS addon is unavailable, it falls back to the generic @electron-webauthn/native module.
Generic Native Implementation
The createNativePasskeyProvider() function wraps the cross-platform native module:
function createNativePasskeyProvider(): PasskeyProvider {
return {
isSupported: nativeIsSupported,
authenticate: async (options) => {
const cred = await nativeGet(convertRequestOptions(options));
return buildAuthenticationResponse(cred);
},
register: async (options) => {
const cred = await nativeCreate(convertCreationOptions(options));
return buildRegistrationResponse(cred);
},
};
}
This provider uses the nativeGet and nativeCreate functions from @electron-webauthn/native to communicate with the OS-level WebAuthn APIs on Windows and Linux.
macOS-Specific Optimization
The createMacPasskeyProvider() function implements a fallback strategy for macOS environments where the signed addon cannot be loaded:
function createMacPasskeyProvider(addon: WebAuthnMacAddon): PasskeyProvider {
const fallbackProvider = createNativePasskeyProvider();
let useAddon = true;
async function callWithFallback<T>(addonOp: () => Promise<T>, nativeOp: () => Promise<T>): Promise<T> {
if (!useAddon) return nativeOp();
try {
return await addonOp();
} catch (e) {
if (isMissingApplicationIdentifierError(e)) {
useAddon = false;
return nativeOp();
}
throw e;
}
}
return {
isSupported: async () => (useAddon ? true : fallbackProvider.isSupported()),
authenticate: (options) => callWithFallback(
async () => {
const cred = await addon.getCredential(convertMacRequestOptions(options));
return buildAuthenticationResponseFromMac(cred);
},
() => fallbackProvider.authenticate(options),
),
register: (options) => callWithFallback(
async () => {
const cred = await addon.createCredential(convertMacCreationOptions(options));
return buildRegistrationResponseFromMac(cred);
},
() => fallbackProvider.register(options),
),
};
}
If the addon throws a missing application identifier error—common in unsigned macOS builds—the provider disables the addon and transparently falls back to the generic native implementation for subsequent operations.
Server-Side Verification
The server implementation in packages/api/src/auth/services/AuthMfaService.tsx handles challenge generation, response verification, and credential persistence using @simplewebauthn/server.
Registration Options Generation
The generateWebAuthnRegistrationOptions() method creates registration challenges and enforces credential limits:
async generateWebAuthnRegistrationOptions(userId: UserID) {
const user = await this.repository.findUniqueAssert(userId);
const existing = await this.repository.listWebAuthnCredentials(userId);
if (existing.length >= 10) throw new WebAuthnCredentialLimitReachedError();
const rpName = Config.auth.passkeys.rpName;
const rpID = Config.auth.passkeys.rpId;
const options = await generateRegistrationOptions({
rpName,
rpID,
userID: new TextEncoder().encode(user.id.toString()),
userName: user.username!,
userDisplayName: user.username!,
attestationType: 'none',
excludeCredentials: existing.map(c => ({
id: c.credentialId,
transports: c.transports ? Array.from(c.transports) as any : undefined,
})),
authenticatorSelection: {
residentKey: 'required',
requireResidentKey: true,
userVerification: 'required',
},
});
await this.saveWebAuthnChallenge(options.challenge, { context: 'registration', userId });
return options;
}
The service enforces a hard limit of 10 credentials per user and configures the relying party (RP) using values from Config.auth.passkeys.
Response Verification
The verifyWebAuthnRegistration() method validates client responses against stored challenges:
async verifyWebAuthnRegistration(userId, response, expectedChallenge, name) {
await this.consumeWebAuthnChallenge(expectedChallenge, 'registration', { userId });
const rpID = Config.auth.passkeys.rpId;
const expectedOrigin = Config.auth.passkeys.allowedOrigins;
const verification = await verifyRegistrationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID: rpID,
});
if (!verification.verified || !verification.registrationInfo) {
throw new InvalidWebAuthnCredentialError();
}
// Store credential, update authenticator types...
}
The server verifies the origin against Config.auth.passkeys.allowedOrigins and the RP ID against Config.auth.passkeys.rpId, ensuring that only legitimate clients can register credentials.
Cross-Platform Usage Examples
The unified API allows identical React components to function across web and desktop environments.
Registering a Passkey
import { performRegistration } from '@app/utils/WebAuthnUtils';
import { generateWebAuthnRegistrationOptions } from '@app/api/auth';
async function registerPasskey() {
const options = await generateWebAuthnRegistrationOptions();
const credential = await performRegistration(options);
await apiClient.post('/auth/webauthn/register', { credential });
}
Authenticating with a Passkey
import { performAuthentication } from '@app/utils/WebAuthnUtils';
import { generateWebAuthnAuthenticationOptions } from '@app/api/auth';
async function loginWithPasskey() {
const options = await generateWebAuthnAuthenticationOptions();
const assertion = await performAuthentication(options);
const { token } = await apiClient.post('/auth/webauthn/authenticate', { assertion });
}
Both snippets execute identically whether running in Chrome, Safari, or the Fluxer desktop application, as the platform-specific logic is encapsulated within WebAuthnUtils.tsx.
Summary
- Fluxer implements a three-layer architecture separating web client utilities, Electron native bridges, and server verification services.
- Platform detection occurs at runtime in
WebAuthnUtils.tsx, automatically routing to the native Electron bridge when available or falling back to browser-based SimpleWebAuthn. - The main process dynamically selects providers, using the
electron-webauthn-macaddon on signed macOS builds for Touch ID integration, while Windows and Linux use the generic@electron-webauthn/nativemodule. - Automatic fallback protects against addon failures; if the macOS addon fails to load due to unsigned bundles or missing identifiers, the system transparently switches to the generic native provider.
- Server-side enforcement includes a 10-credential limit per user, challenge tracking, and strict origin/RP ID validation through
AuthMfaService.tsx.
Frequently Asked Questions
How does Fluxer handle WebAuthn differently on macOS versus Windows?
On macOS, Fluxer attempts to load the electron-webauthn-mac addon for native Touch ID and Secure Enclave integration, while Windows and Linux rely on the generic @electron-webauthn/native module that interfaces with the standard OS WebAuthn APIs. Both implementations expose the same PasskeyProvider interface, ensuring consistent behavior across platforms.
What happens if the macOS addon fails to load?
The createMacPasskeyProvider() function implements a fallback mechanism that catches missing application identifier errors—common in unsigned development builds—and disables the addon. Subsequent operations automatically route to the generic native provider without requiring code changes or user intervention.
How many passkeys can a user register in Fluxer?
The server enforces a limit of 10 passkeys per user through the generateWebAuthnRegistrationOptions() method in AuthMfaService.tsx, which throws a WebAuthnCredentialLimitReachedError when attempting to exceed this threshold.
Can the same UI components work for both web and desktop clients?
Yes. The performRegistration() and performAuthentication() functions in WebAuthnUtils.tsx abstract platform differences, allowing identical React components to execute passkey ceremonies in both browser and Electron environments without conditional rendering or platform-specific logic.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →