@goplausible/liquid-client
TypeScript browser client library for Liquid Auth — passwordless FIDO2/WebAuthn authentication with Algorand wallet binding and WebRTC peer-to-peer signaling.
Improved by GoPlausible, originated by the Algorand Foundation's Liquid Auth Client reference design. Redesigned from the ground up with a lighter footprint: native WebSocket instead of Socket.io, zero transport dependencies, and a simple { event, data } JSON protocol that works directly with Cloudflare Durable Objects.
| Project | Package / App | Description |
|---|---|---|
| Liquid Auth Cloud | @goplausible/liquid-auth-cloud |
Cloudflare Workers auth server — edge-deployed, serverless, zero idle cost |
| Liquid Auth Android | Android wallet app | FIDO2 + WebRTC with native OkHttp WebSocket |
| Rocca Wallet | React Native / Expo app | Cross-platform wallet — FIDO2 passkeys + WebRTC, links to @goplausible/liquid-client via file: dependency |
| WebRTC Payment SDK | @goplausible/webrtc-payment-sdk |
Used in — micropayment-gated WebRTC streams on Algorand |
- No Socket.io — uses the browser's native
WebSocketAPI directly, eliminating engine.io transport negotiation and ~200KB of dependencies - Cloud-native protocol —
{ event, data }JSON envelope designed for Cloudflare Durable Objects (WalletRoom Hibernation API) - Room routing via URL —
SignalClientconnects to/ws?requestId=xxx, so both peers land in the same Durable Object instance without any room-join handshake - Lighter build — ships as pure ESM, tree-shakable, with no runtime dependencies beyond
eventemitter3,qr-code-styling, anduuid - React Native compatible — QR code styling uses dynamic import (lazy loading) so the library works in React Native environments where
qr-code-stylingis not available - Pure base32 address encoding — Algorand address codec with no
algokit-utilsdependency
The package exposes granular subpath exports for tree-shaking:
| Subpath | Purpose |
|---|---|
@goplausible/liquid-client |
Main entry — SignalClient, encoding, etc. |
@goplausible/liquid-client/signal |
SignalClient only |
@goplausible/liquid-client/encoding |
Base64URL / Base32 / address utilities |
@goplausible/liquid-client/assertion |
FIDO2 assertion (sign-in) helpers |
@goplausible/liquid-client/assertion/encoder |
Assertion response encoding |
@goplausible/liquid-client/attestation |
FIDO2 attestation (registration) helpers |
npm install @goplausible/liquid-client --saveimport { SignalClient, encoding, attestation, assertion } from "@goplausible/liquid-client";
const client = new SignalClient("https://liquidauth.goplausible.xyz");const testAccount = algosdk.generateAccount();
await client.attestation(
async (challenge: Uint8Array) => ({
type: "algorand",
address: testAccount.addr,
signature: encoding.toBase64URL(nacl.sign.detached(challenge, testAccount.sk)),
requestId: "019097ff-bb8d-7f68-9062-89543625aca5",
device: "Demo Web Wallet",
}),
);await client.assertion(credentialId);const requestId = SignalClient.generateRequestId();
client.peer(requestId, "offer").then((dataChannel: RTCDataChannel) => {
dataChannel.onmessage = (event) => console.log(event.data);
});
const qrBlob = await client.qrCode();client.peer(requestId, "answer").then((dataChannel: RTCDataChannel) => {
dataChannel.onmessage = (event) => console.log(event.data);
});All messages use a JSON envelope:
{ event: string, data: any }| Event | Description |
|---|---|
link |
Register interest in a requestId / auth completed notification |
offer-description |
WebRTC SDP offer |
offer-candidate |
WebRTC ICE candidate (offer side) |
answer-description |
WebRTC SDP answer |
answer-candidate |
WebRTC ICE candidate (answer side) |
class SignalClient extends EventEmitter {
readonly url: string;
ws: WebSocket | null;
type: "offer" | "answer" | null;
authenticated: boolean;
requestId?: string;
peerClient?: RTCPeerConnection;
connect(requestId?: string): void;
attestation(onChallenge, options?, debug?): Promise<User>;
assertion(credId, debug?): Promise<User | null>;
peer(requestId, type, config?): Promise<RTCDataChannel>;
link(requestId): Promise<LinkMessage>;
signal(type): Promise<RTCSessionDescriptionInit>;
qrCode(): Promise<string>;
deepLink(requestId?): string;
close(disconnect?): void;
}# From the monorepo root
npm run build:vendor # Build the library with Vite
npm run lint:client # Lint with oxlint
npm run fmt:client # Format with oxfmtMIT