FlowIndex
Wallet

Getting Started

Set up passkey-based wallet authentication in your application.

LLM IndexLLM Full

This guide walks through integrating the FlowIndex passkey wallet into a React application. By the end, your users will be able to register passkeys, provision Flow accounts, and sign transactions with biometric authentication.

Prerequisites

  • Node.js 18+ or Bun
  • React 18+ application
  • A running Supabase instance with GoTrue (for auth tokens) and the passkey-auth edge function deployed
  • A domain served over HTTPS (WebAuthn requires a secure context)

Install Packages

# Using bun
bun add @flowindex/auth-core @flowindex/auth-ui @flowindex/flow-passkey

# Using npm
npm install @flowindex/auth-core @flowindex/auth-ui @flowindex/flow-passkey

Peer dependencies for @flowindex/auth-ui (install if not already present):

bun add react react-dom framer-motion lucide-react input-otp

Set Up the AuthProvider

Wrap your application with AuthProvider from @flowindex/auth-ui. This provides authentication state, passkey operations, and token management to all child components.

import { AuthProvider } from '@flowindex/auth-ui';

function App() {
  return (
    <AuthProvider
      config={{
        gotrueUrl: 'https://your-supabase-domain/auth/v1',
        passkeyAuthUrl: 'https://your-supabase-domain/functions/v1/passkey-auth',
        rpId: 'yourdomain.com',
        rpName: 'Your App Name',
        cookieDomain: '.yourdomain.com',
        enableLogoutDetection: true,
      }}
    >
      <YourApp />
    </AuthProvider>
  );
}

Configuration Options

OptionTypeDescription
gotrueUrlstringBase URL of your GoTrue auth endpoint
passkeyAuthUrlstringURL of the passkey-auth edge function
rpIdstringWebAuthn relying party ID (your domain, without protocol)
rpNamestringHuman-readable name shown in passkey prompts
cookieDomainstringCookie domain for cross-subdomain session sharing (e.g., .yourdomain.com)
enableLogoutDetectionbooleanDetect logout from other tabs/subdomains via cookie polling
enableRolesbooleanParse role/team claims from JWT tokens (default: true)
callbackPathstringOAuth callback path or full URL (default: /developer/callback)

Add the Login Modal

The LoginModal component provides a complete sign-in UI with support for GitHub, Google, email magic links, and passkey login.

import { useState } from 'react';
import { LoginModal, useAuth } from '@flowindex/auth-ui';

function Header() {
  const { user, signOut } = useAuth();
  const [showLogin, setShowLogin] = useState(false);

  return (
    <header>
      {user ? (
        <div>
          <span>{user.email}</span>
          <button onClick={signOut}>Sign out</button>
        </div>
      ) : (
        <button onClick={() => setShowLogin(true)}>Sign in</button>
      )}
      <LoginModal open={showLogin} onClose={() => setShowLogin(false)} />
    </header>
  );
}

Register a Passkey

After the user signs in (via any method), they can register a passkey. This creates a WebAuthn credential and stores the public key server-side.

import { usePasskeyAuth } from '@flowindex/auth-ui';

function RegisterPasskey() {
  const passkey = usePasskeyAuth();

  async function handleRegister() {
    const { credentialId, publicKeySec1Hex } = await passkey.register('My Wallet');
    console.log('Passkey registered:', credentialId);
    console.log('Public key (SEC1):', publicKeySec1Hex);
  }

  if (!passkey.hasSupport) {
    return <p>Passkeys are not supported on this device.</p>;
  }

  return <button onClick={handleRegister}>Register Passkey</button>;
}

Provision a Flow Account

Once a passkey is registered, provision an on-chain Flow account. This submits the passkey's P-256 public key to Flow's account creation service, which creates a new account with that key as its sole signer.

import { usePasskeyAuth } from '@flowindex/auth-ui';

function ProvisionAccount() {
  const passkey = usePasskeyAuth();
  const account = passkey.selectedAccount;

  async function handleProvision() {
    if (!account) return;

    // Start provisioning on both mainnet and testnet
    const result = await passkey.provisionAccounts(account.credentialId);

    // Poll for each network's transaction to seal
    for (const [network, info] of Object.entries(result.networks)) {
      if (info.txId) {
        const address = await passkey.pollProvisionTx(
          info.txId,
          network as 'mainnet' | 'testnet',
        );
        await passkey.saveProvisionedAddress(account.credentialId, network, address);
        console.log(`${network} account created: 0x${address}`);
      }
    }

    // Refresh state to pick up new addresses
    await passkey.refreshState();
  }

  return (
    <div>
      {account?.flowAddress ? (
        <p>Flow address: 0x{account.flowAddress}</p>
      ) : (
        <button onClick={handleProvision}>Create Flow Account</button>
      )}
    </div>
  );
}

Sign Flow Transactions

Use getFlowAuthz to create an FCL-compatible authorization function, then pass it to fcl.mutate or fcl.send.

import * as fcl from '@onflow/fcl';
import { usePasskeyAuth } from '@flowindex/auth-ui';

function SendTransaction() {
  const passkey = usePasskeyAuth();

  async function handleSend() {
    const account = passkey.selectedAccount;
    if (!account?.flowAddress) return;

    const authz = passkey.getFlowAuthz(account.flowAddress, 0);

    const txId = await fcl.mutate({
      cadence: `
        transaction {
          prepare(signer: &Account) {
            log(signer.address)
          }
        }
      `,
      proposer: authz,
      payer: authz,
      authorizations: [authz],
    });

    console.log('Transaction submitted:', txId);
    const result = await fcl.tx(txId).onceSealed();
    console.log('Transaction sealed:', result.status);
  }

  return <button onClick={handleSend}>Send Transaction</button>;
}

Sign Without FCL

If you are not using FCL, you can sign raw messages directly using the sign method on the passkey state:

import { usePasskeyAuth } from '@flowindex/auth-ui';

function SignMessage() {
  const passkey = usePasskeyAuth();

  async function handleSign() {
    const messageHex = '48656c6c6f'; // "Hello" in hex
    const { signature, extensionData } = await passkey.sign(messageHex);
    console.log('Signature:', signature);
    console.log('Extension data:', extensionData);
  }

  return <button onClick={handleSign}>Sign Message</button>;
}

Using the Low-Level SDK Directly

For non-React applications or custom integrations, use @flowindex/flow-passkey and @flowindex/auth-core directly.

Create a Passkey Client

import { createPasskeyAuthClient } from '@flowindex/auth-core';

const passkeyClient = createPasskeyAuthClient({
  passkeyAuthUrl: 'https://your-supabase-domain/functions/v1/passkey-auth',
  rpId: 'yourdomain.com',
  rpName: 'Your App Name',
});

Register and Login

// Register a new passkey (requires an authenticated session token)
const { credentialId, publicKeySec1Hex } = await passkeyClient.register(accessToken);

// Login with a passkey (no session required -- returns a token hash)
const { tokenHash, email } = await passkeyClient.login();

Sign a Transaction

import { signFlowTransaction, createPasskeyAuthz } from '@flowindex/flow-passkey';

// Direct signing
const { signature, extensionData } = await signFlowTransaction({
  messageHex: '...', // RLP-encoded transaction with domain tag
  credentialId: 'base64url-credential-id',
  rpId: 'yourdomain.com',
});

// FCL authorization function
const authz = createPasskeyAuthz({
  address: '0x1234abcd',
  keyIndex: 0,
  credentialId: 'base64url-credential-id',
  rpId: 'yourdomain.com',
});

Browser Support

Passkeys require WebAuthn support. The wallet checks for window.PublicKeyCredential at runtime and exposes passkey.hasSupport so you can show appropriate fallback UI.

BrowserMinimum Version
Chrome67+
Safari14+
Firefox60+
Edge18+

Discoverable credentials (resident keys) and cross-device passkey sync require more recent versions (Chrome 108+, Safari 16+, etc.).

On this page