Guides

Integrating with Next.js

A complete end-to-end Next.js (App Router) integration. By the end you will have a wallet-connected app with a reusable verification hook, a <VerifiedGate /> component, and a server route that gates sensitive data behind Althea + wallet signature.

1. Project setup

Start from a Next.js 14+ project with the App Router. Install the SDK and wallet adapter packages (see Installation).

2. Wrap the app with WalletProvider

app/providers.tsx
'use client'

import {
  ConnectionProvider,
  WalletProvider,
} from '@solana/wallet-adapter-react'
import { PhantomWalletAdapter } from '@solana/wallet-adapter-wallets'
import { useMemo, type ReactNode } from 'react'
import '@solana/wallet-adapter-react-ui/styles.css'
import {
  WalletModalProvider,
} from '@solana/wallet-adapter-react-ui'

const RPC = process.env.NEXT_PUBLIC_SOLANA_RPC!

export function Providers({ children }: { children: ReactNode }) {
  const wallets = useMemo(() => [new PhantomWalletAdapter()], [])
  return (
    <ConnectionProvider endpoint={RPC}>
      <WalletProvider wallets={wallets} autoConnect>
        <WalletModalProvider>{children}</WalletModalProvider>
      </WalletProvider>
    </ConnectionProvider>
  )
}

Mount <Providers> in your root app/layout.tsx around {children}.

3. The useAfricaZK hook

Wrap the five SDK calls in one client-side hook so your components only see status, pending, and a verify action.

hooks/useAfricaZK.ts
'use client'

import { useCallback, useEffect, useState } from 'react'
import { useWallet } from '@solana/wallet-adapter-react'
import {
  checkAttestation,
  verifyIdentity,
  generateProof,
  submitProof,
  type AttestationStatus,
} from '@africazk/identity'

export function useAfricaZK() {
  const wallet = useWallet()
  const [status, setStatus] = useState<AttestationStatus | null>(null)
  const [pending, setPending] = useState(false)
  const [error, setError] = useState<Error | null>(null)

  useEffect(() => {
    if (!wallet.publicKey) {
      setStatus(null)
      return
    }
    checkAttestation(wallet.publicKey.toString(), 'mainnet-beta').then(setStatus)
  }, [wallet.publicKey])

  const verify = useCallback(
    async (idType: 'NIN' | 'BVN', idNumber: string, dob: string) => {
      setPending(true)
      setError(null)
      try {
        const cred = await verifyIdentity({ idType, idNumber, dob })
        const proof = await generateProof(cred)
        await submitProof(proof, wallet, 'mainnet-beta')
        const fresh = await checkAttestation(
          wallet.publicKey!.toString(),
          'mainnet-beta'
        )
        setStatus(fresh)
      } catch (e) {
        setError(e as Error)
      } finally {
        setPending(false)
      }
    },
    [wallet]
  )

  return { status, pending, error, verify, wallet }
}

4. The VerifiedGate component

components/VerifiedGate.tsx
'use client'

import { type ReactNode } from 'react'
import Link from 'next/link'
import { useAfricaZK } from '@/hooks/useAfricaZK'

export function VerifiedGate({ children }: { children: ReactNode }) {
  const { status, wallet } = useAfricaZK()

  if (!wallet.publicKey) {
    return <p>Connect your wallet to continue.</p>
  }
  if (status === null) return <p>Checking attestation…</p>
  if (!status.verified) {
    return (
      <Link href="/verify" className="btn-primary">
        Verify with Althea 
      </Link>
    )
  }
  return <>{children}</>
}

5. Build a verification page

Build a /verify page that shows the wallet connect button, then a NIN/BVN form. Call verify() from the hook on submit. The hook handles all three SDK calls.

6. Protect server routes

Always combine attestation + signature server-side

An attestation only proves the wallet is verified — not that the request comes from that wallet. Require a signed message before trusting the wallet identifier in any server-side check.
app/api/dashboard/route.ts
// app/api/dashboard/route.ts
import { NextResponse } from 'next/server'
import { checkAttestation } from '@africazk/identity'
import { verifySignature } from '@/lib/verify-signature'

export async function POST(req: Request) {
  const body = await req.json()
  const { wallet, message, signature } = body

  // 1. Confirm the request actually comes from the wallet
  const ok = await verifySignature(wallet, message, signature)
  if (!ok) return NextResponse.json({ error: 'bad sig' }, { status: 401 })

  // 2. Confirm the wallet has a valid attestation
  const attestation = await checkAttestation(wallet, 'mainnet-beta')
  if (!attestation.verified) {
    return NextResponse.json({ error: 'not verified' }, { status: 403 })
  }

  return NextResponse.json({ data: 'top-secret verified-only data' })
}

Production checklist

  • Cache the attestation status per wallet in your app state — don't re-fetch on every render.
  • Provide clear, copyable error messages on every failure path.
  • Show a progress bar during proof generation (3–8 seconds).
  • Have a self-serve "Re-check status" button — useful when the user just submitted a proof on another tab.