Integrations
Next.js

Next.js Integration

This guide covers SSR-safe patterns for integrating Veridex with Next.js, a production payment app that uses dynamic imports, cross-origin passkey authentication, and relayer-based gasless transfers.

⚠️

The Veridex SDK uses browser APIs (WebAuthn, localStorage) and must only run in Client Components. Always add 'use client' to components using the SDK. For SSR-safe initialization, use dynamic imports as shown below.

Installation

npm install @veridex/sdk ethers

SSR-Safe SDK Initialization (Dynamic Import)

The sera/dashboard uses a dynamic import + singleton pattern to prevent the SDK from being loaded during server-side rendering:

// lib/veridex-client.ts
import { WalletManager, CrossOriginAuth } from '@veridex/sdk';
 
const SERA_HUB_CHAIN = 'base';
const SERA_NETWORK = 'testnet';
const RELAYER_URL = process.env.NEXT_PUBLIC_VERIDEX_RELAYER_URL
  || 'https://relay.veridex.network';
const RELAYER_API_KEY = process.env.NEXT_PUBLIC_RELAYER_API_KEY || '';
 
let sdkInstance: any = null;
 
/**
 * Lazy-load the SDK to avoid SSR issues.
 * The SDK uses browser APIs (WebAuthn, localStorage) that don't exist on the server.
 */
export async function getVeridexSDK(): Promise<any> {
  if (!sdkInstance) {
    const { createSDK } = await import('@veridex/sdk');
    sdkInstance = createSDK(SERA_HUB_CHAIN, {
      network: SERA_NETWORK,
      relayerUrl: RELAYER_URL,
      relayerApiKey: RELAYER_API_KEY,
    });
  }
  return sdkInstance;
}

The await import('@veridex/sdk') ensures the SDK bundle is only loaded in the browser, never during SSR. This is critical for Next.js App Router where server components are the default.

Passkey Registration & Authentication

Register a New Passkey

export async function registerPasskey(username: string, displayName?: string) {
  const sdk = await getVeridexSDK();
 
  // Register passkey via WebAuthn (prompts biometric)
  const credential = await sdk.passkey.register(
    username,
    displayName || username
  );
 
  // Save credential to localStorage for auto-login
  sdk.passkey.saveToLocalStorage();
 
  // Sync credential to relayer for cross-device recovery
  await sdk.passkey.saveCredentialToRelayer();
 
  // Compute the vault address (deterministic from passkey)
  const vaultAddress = sdk.getVaultAddress();
 
  return { credential, vaultAddress };
}

Authenticate with Existing Passkey

export async function authenticateWithPasskey() {
  const sdk = await getVeridexSDK();
 
  // Discoverable credential authentication (shows passkey picker)
  const { credential } = await sdk.passkey.authenticate();
 
  sdk.passkey.saveToLocalStorage();
 
  const vaultAddress = sdk.getVaultAddress();
 
  return { credential, vaultAddress };
}

Sign a Challenge

export async function signWithPasskey(challenge: string) {
  const sdk = await getVeridexSDK();
  const signature = await sdk.passkey.sign(challenge);
  return signature;
}

Restore Credential from Storage

export async function restoreSDKCredential(): Promise<boolean> {
  const sdk = await getVeridexSDK();
 
  // Try localStorage first
  const saved = sdk.passkey.loadFromLocalStorage();
  if (saved) {
    sdk.setCredential(saved);
    return true;
  }
 
  // Try fetching from relayer (cross-device recovery)
  try {
    const relayerCred = await sdk.passkey.loadCredentialFromRelayer();
    if (relayerCred) {
      sdk.setCredential(relayerCred);
      sdk.passkey.saveToLocalStorage();
      return true;
    }
  } catch (error) {
    console.warn('Could not restore from relayer:', error);
  }
 
  return false;
}

Cross-Origin Passkey Authentication

When your app runs on a different domain than the passkey's RP ID, use CrossOriginAuth:

import { CrossOriginAuth } from '@veridex/sdk';
 
export async function authenticateCrossOrigin() {
  const crossOriginAuth = new CrossOriginAuth({
    authPortalUrl: 'https://auth.veridex.network',
    relayerUrl: RELAYER_URL,
    rpId: 'veridex.network',
  });
 
  // Tries Related Origin Requests (ROR) first, falls back to auth portal popup
  const result = await crossOriginAuth.authenticate();
 
  if (result.credential) {
    const sdk = await getVeridexSDK();
    sdk.setCredential(result.credential);
    return result;
  }
 
  throw new Error('Cross-origin authentication failed');
}

Cross-origin auth uses the Related Origin Requests (opens in a new tab) spec where supported, with an auth portal popup as fallback. This enables passkey sharing across *.veridex.network subdomains.

Gasless Transfers via Relayer

The sera/dashboard uses a prepare → sign → execute flow for gasless payments:

Prepare a Transfer

export async function prepareTransfer(params: {
  token: string;
  recipient: string;
  amount: string;
  chain?: number;
}) {
  const sdk = await getVeridexSDK();
 
  const prepared = await sdk.prepareTransfer({
    targetChain: params.chain || 10004, // Base Sepolia
    token: params.token,
    recipient: params.recipient,
    amount: BigInt(params.amount),
  });
 
  return prepared;
}

Execute via Relayer (Gasless)

export async function executeSignedTransfer(
  transferId: string,
  signature: string
) {
  const sdk = await getVeridexSDK();
 
  // Execute through relayer — user pays no gas
  const result = await sdk.transferViaRelayer({
    transferId,
    signature,
  });
 
  return result;
}

Poll for Transaction Completion

export async function pollForTransactionCompletion(
  sequenceOrHubTx: string,
  maxAttempts = 30,
  intervalMs = 2000
): Promise<{ status: string; txHash?: string }> {
  for (let i = 0; i < maxAttempts; i++) {
    try {
      const response = await fetch(
        `${RELAYER_URL}/api/v1/tx/status/${sequenceOrHubTx}`
      );
      const data = await response.json();
 
      if (data.status === 'confirmed' || data.status === 'failed') {
        return data;
      }
    } catch (error) {
      console.warn('Poll attempt failed:', error);
    }
 
    await new Promise(resolve => setTimeout(resolve, intervalMs));
  }
 
  return { status: 'timeout' };
}

Balance Queries

export async function getBalance(address: string, token?: string) {
  const sdk = await getVeridexSDK();
  const portfolio = await sdk.balance.getPortfolioBalance(address, token);
  return portfolio;
}

Compute Vault Addresses

Vault addresses are deterministic — the same passkey always produces the same address on every chain:

import { WalletManager } from '@veridex/sdk';
 
export function computeVaultAddress(
  keyHash: string,
  chainConfig: { vaultFactory: string; vaultImplementation: string }
) {
  return WalletManager.computeVaultAddress(
    keyHash,
    chainConfig.vaultFactory,
    chainConfig.vaultImplementation
  );
}

React Context (Wallet Provider)

The sera/dashboard wraps all SDK operations in a WalletContext that supports multiple connection methods:

// lib/wallet-context.tsx
'use client';
 
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import {
  getVeridexSDK,
  registerPasskey,
  authenticateWithPasskey,
  signWithPasskey,
  restoreSDKCredential,
  prepareTransfer,
  executeSignedTransfer,
  getBalance,
} from './veridex-client';
 
type ConnectionMethod = 'injected' | 'walletconnect' | 'passkey';
 
interface WalletContextType {
  address: string | null;
  isConnected: boolean;
  connectionMethod: ConnectionMethod | null;
  isLoading: boolean;
 
  // Passkey methods
  connectPasskey: () => Promise<void>;
  registerNewPasskey: (username: string) => Promise<void>;
  signMessage: (message: string) => Promise<string>;
 
  // Payment methods
  preparePayment: (params: any) => Promise<any>;
  confirmPayment: (transferId: string) => Promise<any>;
 
  // Balance
  balance: string | null;
  refreshBalance: () => Promise<void>;
 
  disconnect: () => void;
}
 
const WalletContext = createContext<WalletContextType | undefined>(undefined);
 
export function WalletProvider({ children }: { children: ReactNode }) {
  const [address, setAddress] = useState<string | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const [connectionMethod, setConnectionMethod] = useState<ConnectionMethod | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [balance, setBalance] = useState<string | null>(null);
 
  // Auto-restore session on mount
  useEffect(() => {
    const restore = async () => {
      try {
        const restored = await restoreSDKCredential();
        if (restored) {
          const sdk = await getVeridexSDK();
          const vault = sdk.getVaultAddress();
          setAddress(vault);
          setIsConnected(true);
          setConnectionMethod('passkey');
        }
      } catch (error) {
        console.warn('Session restore failed:', error);
      } finally {
        setIsLoading(false);
      }
    };
    restore();
  }, []);
 
  const connectPasskey = async () => {
    setIsLoading(true);
    try {
      const { vaultAddress } = await authenticateWithPasskey();
      setAddress(vaultAddress);
      setIsConnected(true);
      setConnectionMethod('passkey');
    } finally {
      setIsLoading(false);
    }
  };
 
  const registerNewPasskey = async (username: string) => {
    setIsLoading(true);
    try {
      const { vaultAddress } = await registerPasskey(username);
      setAddress(vaultAddress);
      setIsConnected(true);
      setConnectionMethod('passkey');
    } finally {
      setIsLoading(false);
    }
  };
 
  const signMessage = async (message: string) => {
    return await signWithPasskey(message);
  };
 
  const preparePayment = async (params: any) => {
    return await prepareTransfer(params);
  };
 
  const confirmPayment = async (transferId: string) => {
    // Sign with passkey, then execute via relayer
    const signature = await signWithPasskey(transferId);
    return await executeSignedTransfer(transferId, signature);
  };
 
  const refreshBalance = async () => {
    if (!address) return;
    try {
      const portfolio = await getBalance(address);
      setBalance(portfolio?.totalUsdValue?.toFixed(2) || '0.00');
    } catch (error) {
      console.error('Balance refresh failed:', error);
    }
  };
 
  const disconnect = () => {
    setAddress(null);
    setIsConnected(false);
    setConnectionMethod(null);
    setBalance(null);
    localStorage.removeItem('veridex_credentials');
  };
 
  return (
    <WalletContext.Provider value={{
      address, isConnected, connectionMethod, isLoading,
      connectPasskey, registerNewPasskey, signMessage,
      preparePayment, confirmPayment,
      balance, refreshBalance,
      disconnect,
    }}>
      {children}
    </WalletContext.Provider>
  );
}
 
export function useWallet() {
  const context = useContext(WalletContext);
  if (!context) throw new Error('useWallet must be used within WalletProvider');
  return context;
}

App Router Setup

Layout

// app/layout.tsx
import { WalletProvider } from '@/lib/wallet-context';
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <WalletProvider>{children}</WalletProvider>
      </body>
    </html>
  );
}

Page Component

// app/page.tsx
'use client';
 
import { useWallet } from '@/lib/wallet-context';
 
export default function Home() {
  const {
    isConnected, address, isLoading,
    connectPasskey, registerNewPasskey, disconnect,
    balance, refreshBalance,
  } = useWallet();
 
  if (isLoading) return <div>Loading...</div>;
 
  if (!isConnected) {
    return (
      <div className="space-y-4">
        <button onClick={connectPasskey}>Sign In with Passkey</button>
        <button onClick={() => registerNewPasskey('user@example.com')}>
          Create New Wallet
        </button>
      </div>
    );
  }
 
  return (
    <div>
      <p>Connected: {address?.slice(0, 6)}...{address?.slice(-4)}</p>
      <p>Balance: ${balance || '...'}</p>
      <button onClick={refreshBalance}>Refresh</button>
      <button onClick={disconnect}>Disconnect</button>
    </div>
  );
}

API Routes (Relayer Proxy)

Proxy relayer requests through your Next.js API routes to keep API keys server-side:

// app/api/relayer/[...path]/route.ts
import { NextRequest, NextResponse } from 'next/server';
 
const RELAYER_URL = process.env.RELAYER_URL || 'https://relay.veridex.network';
const RELAYER_API_KEY = process.env.RELAYER_API_KEY || '';
 
export async function GET(
  request: NextRequest,
  { params }: { params: { path: string[] } }
) {
  const path = params.path.join('/');
  const response = await fetch(`${RELAYER_URL}/api/v1/${path}`, {
    headers: {
      'x-api-key': RELAYER_API_KEY,
      'Content-Type': 'application/json',
    },
  });
  const data = await response.json();
  return NextResponse.json(data, { status: response.status });
}
 
export async function POST(
  request: NextRequest,
  { params }: { params: { path: string[] } }
) {
  const path = params.path.join('/');
  const body = await request.json();
  const response = await fetch(`${RELAYER_URL}/api/v1/${path}`, {
    method: 'POST',
    headers: {
      'x-api-key': RELAYER_API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  });
  const data = await response.json();
  return NextResponse.json(data, { status: response.status });
}

Environment Variables

# .env.local (client-side — exposed to browser)
NEXT_PUBLIC_VERIDEX_RELAYER_URL=https://relay.veridex.network
NEXT_PUBLIC_RELAYER_API_KEY=your-public-api-key
 
# .env.local (server-side — API routes only)
RELAYER_URL=https://relay.veridex.network
RELAYER_API_KEY=your-secret-api-key

Server Components Considerations

⚠️

Never import @veridex/sdk in Server Components. The SDK requires browser APIs. Use the dynamic import pattern shown above, or keep all SDK usage in 'use client' components.

For server-side data fetching, call the relayer API directly:

// app/vault/[address]/page.tsx (Server Component)
export default async function VaultPage({ params }: { params: { address: string } }) {
  const res = await fetch(
    `${process.env.RELAYER_URL}/api/v1/balances/${params.address}`,
    { headers: { 'x-api-key': process.env.RELAYER_API_KEY! } }
  );
  const balances = await res.json();
 
  return (
    <div>
      <h1>Vault: {params.address}</h1>
      <pre>{JSON.stringify(balances, null, 2)}</pre>
    </div>
  );
}

Complete Example

Clone the sera/dashboard for a complete Next.js integration with passkey payments:

git clone https://github.com/Veridex-Protocol/demo
cd demo/sera/dashboard
cp .env.example .env.local
npm install
npm run dev

Open http://localhost:3000 (opens in a new tab).

Next Steps