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
'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.
'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
'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
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.