vlayer docs

Fullstack vouch Github Prover Tutorial

This guide demonstrates how to use vlayer's vouch service in a fullstack Next.js application to verify GitHub contributions.

Prerequisites

  • Node.js 20+
  • PostgreSQL database (we recommend Neon for serverless Postgres)
  • vouch datasource ID and customer ID
  • vouch API key (VOUCH_API_KEY)

Installation

Clone the repository and install dependencies:

git clone https://github.com/vlayer-xyz/vouch-github-verifier.git
cd vouch-github-verifier
pnpm install

Environment Variables

Create a .env.local file for Next.js environment variables:

VOUCH_API_KEY=your-vouch-api-key
VOUCH_WEBHOOK_SECRET=your-webhook-secret   # from Vouch dashboard → Organization → Webhook Secret
NEXT_PUBLIC_WEBHOOK_URL=https://your-public-url.example.com  # optional, defaults to window.location.origin

Prisma does not read .env.local — create a separate .env file for the database URL:

POSTGRES_URL=postgresql://user:password@host/dbname?sslmode=require

Database Setup

Generate the Prisma client and run migrations:

npx prisma generate
npx prisma migrate dev --name init

This creates the web_proofs table in your database. You can inspect it with:

npx prisma studio

Start Development Server

pnpm dev

Visit http://localhost:3000 to see the application.

Implementation

1. Starting verification

The client generates a requestId, then POST to /api/start-verification to get the vouch redirect URL:

const startVerification = async () => {
  const requestId = window.crypto.randomUUID();
  localStorage.setItem('lastRequestId', requestId);

  const webhookBaseUrl = process.env.NEXT_PUBLIC_WEBHOOK_URL || window.location.origin;

  const res = await fetch('/api/start-verification', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      requestId,
      redirectBackUrl: `${window.location.origin}?requestId=${requestId}`,
      webhookUrl: `${webhookBaseUrl}/api/web-proof`,
      inputs: {
        github_owner: githubOwner,
        github_repo: githubRepo,
        github_username: githubUsername,
      },
    }),
  });

  const { verificationUrl } = await res.json();
  window.location.href = verificationUrl;
};

Parameter breakdown:

  • requestId — UUID linking this browser session to the webhook callback
  • redirectBackUrl — where vouch redirects after verification (includes requestId as query param)
  • webhookUrl — your HTTPS endpoint to receive the proof
  • inputs — custom data passed to the datasource (GitHub details in this case)

The webhookUrl must be publicly accessible via HTTPS. For local development, use a service like ngrok or deploy to Vercel.

2. Fetching the proof after redirect

When vouch redirects back, requestId is in the URL. The frontend uses it to retrieve the stored proof:

const searchParams = useSearchParams();
const requestId = searchParams?.get('requestId');

useEffect(() => {
  const fetchProof = async () => {
    if (!requestId) return;

    setLoading(true);
    try {
      const response = await fetch(`/api/web-proof/${requestId}`);
      if (response.ok) {
        const data = await response.json();
        setProof(data);
      } else {
        setError('Proof not found yet. It may still be processing.');
      }
    } catch (err) {
      setError('Failed to fetch proof');
    } finally {
      setLoading(false);
    }
  };

  fetchProof();
}, [requestId]);

Server: Start Verification Route

The /api/start-verification route initializes the Vouch SDK with server-side credentials and returns the redirect URL. This keeps VOUCH_API_KEY out of the browser. View the implementation at app/api/start-verification/route.ts.

import { Vouch } from '@getvouch/sdk';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const { requestId, redirectBackUrl, webhookUrl, inputs } = await request.json();

  const vouch = new Vouch({
    customerId: '1be03be8-5014-413c-835a-feddf4020da2',
    apiKey: process.env.VOUCH_API_KEY!,
  });

  const { verificationUrl } = await vouch.getDataSourceUrl({
    datasourceId: 'ee72bdf7-cf47-424a-9705-75a96e39153e',
    requestId,
    redirectBackUrl,
    webhookUrl,
    inputs,
  });

  return NextResponse.json({ verificationUrl });
}

Backend: Webhook Endpoint

The webhook endpoint receives proofs from vouch and stores them in the database. View the complete implementation at app/api/web-proof/route.ts.

import { timingSafeEqual, createHash } from 'crypto';
import { prisma } from '@/lib/prisma';
import { NextRequest, NextResponse } from 'next/server';

function verifyPsk(authHeader: string | null): boolean {
  const secret = process.env.VOUCH_WEBHOOK_SECRET;
  if (!secret || !authHeader) return false;

  const expected = `PSK ${secret}`;

  // Hash both sides so timingSafeEqual always operates on equal-length buffers
  const expectedHash = createHash('sha256').update(expected).digest();
  const actualHash   = createHash('sha256').update(authHeader).digest();

  return timingSafeEqual(expectedHash, actualHash);
}

export async function POST(request: NextRequest) {
  // Authenticate before touching the body
  if (!verifyPsk(request.headers.get('authorization'))) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const payload = await request.json();

  const proofId   = payload.proofId?.trim()   || null;
  const requestId = payload.requestId?.trim() || null;

  // Use proofId if present, fall back to requestId (v2.0 payloads omit proofId)
  const uniqueKey = proofId ?? requestId;

  if (!uniqueKey) {
    console.error('Webhook rejected: payload contains neither proofId nor requestId');
    return NextResponse.json({ error: 'Missing proof identifier' }, { status: 400 });
  }

  await prisma.webProof.upsert({
    where: { proofId: uniqueKey },
    update: { requestId, payload },
    create: { proofId: uniqueKey, requestId, payload },
  });

  return NextResponse.json({ success: true }, { status: 200 });
}

Backend: Proof Retrieval Route

The GET /api/web-proof/[requestId] route looks up the stored proof by requestId so the frontend can display it after redirect. View the implementation at app/api/web-proof/[requestId]/route.ts.

import { prisma } from '@/lib/prisma';
import { NextRequest, NextResponse } from 'next/server';

export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ requestId: string }> }
) {
  const { requestId } = await params;

  const proof = await prisma.webProof.findFirst({
    where: { requestId },
    orderBy: { createdAt: 'desc' },
  });

  if (!proof) {
    return NextResponse.json({ error: 'Proof not found' }, { status: 404 });
  }

  return NextResponse.json(proof);
}

Database Schema

The web_proofs table stores the full webhook payload alongside the requestId used to look it up:

model WebProof {
  id        String   @id @default(uuid())
  proofId   String   @unique @map("proof_id")
  requestId String?  @map("request_id")
  payload   Json     @default("{}")
  createdAt DateTime @default(now()) @map("created_at")

  @@map("web_proofs")
  @@index([proofId])
  @@index([requestId])
  @@index([createdAt(sort: Desc)])
}

Next Steps

Now that you understand the vouch integration, you can:

  1. Customize the UI - Build your own proof display components
  2. Add more datasources - vouch supports multiple data sources beyond GitHub
  3. Verify proofs server-side - Send presentationJson to the POST /api/v2.0/verify endpoint to cryptographically verify each proof
  4. Build custom workflows - Use proofs for access control, airdrops, or reputation systems
  5. Analytics and monitoring - Track verification patterns and user behavior