Skip to content

Eligibility SDK Integration Guide for Next.js (SSR)

This guide explains how to integrate the Eligibility SDK into a Next.js (App Router) application using a secure proxy, server-side authentication, and wallet signature verification. The SDK is designed to be flexible and works in both client-only and SSR setups.

Eligibility SDK Design Philosophy

The SDK is designed with a clear separation of responsibility:

SDK Responsibilities

  • Provides a fully client-side UI for eligibility workflows.
  • Calls backend endpoints defined by the SDK configuration.
  • Supports secure proxying through a developer-supplied proxyUrl.
  • Injects X-App-Auth header if getAuthToken() is provided.
  • Warns in development mode when an API key is exposed on the frontend.

Developer Responsibilities

Developers are responsible for securing their backend proxy:

  • Validate requests using session cookies, tokens, or signed headers.
  • Rate limit incoming requests by IP/session to prevent abuse.
  • Only allow forwarding to trusted backend domains and paths.
  • Inject the x-api-key server-side (never expose on the frontend).
  • Enforce CORS and domain origin checks.

SDK Configuration Overview

type EligibilitySDKConfig = {
  network: "staging" | "testnet" | "mainnet";
  proxyUrl?: string;
  apiKey?: string;
  getAuthToken?: () => string | undefined;
  customHeaders?: Record<string, string>;
  getCustomHeaders?: () => Promise<Record<string, string>>;
  includeCredentials?: boolean;
};
  • proxyUrl: Forwards SDK requests through a backend controlled by the developer.
  • apiKey: Use only in secure server environments, not client-side.
  • getAuthToken: If defined, SDK will attach this as X-App-Auth header.

Developer Guidance: Secure Proxy Implementation

While the SDK supports proxyUrl, it is up to the developer to:

  • Read the x-api-backend header and validate the requested domain.
  • Inject x-api-key in the proxy request using a secure environment variable.
  • Verify requests using cookies or tokens.
  • Apply rate limiting to protect from abuse.
  • Restrict CORS to trusted frontend origins.

Example Proxy Route (App Router)

/app/server/createSecureProxy

import { NextRequest, NextResponse } from "next/server";

interface CreateSecureProxyOptions {
  apiKey: string;
  allowedDomains: string[];
  rateLimit?: {
    windowMs: number;
    limit: number;
  };
}

const rateMap = new Map<string, { count: number; lastReset: number }>();
const blockedIPs = new Set<string>(["192.168.0.1"]); // example of blocked IPs

export function createSecureProxy({
  apiKey,
  allowedDomains,
  rateLimit,
}: CreateSecureProxyOptions) {
  return async function handler(req: NextRequest) {
    if (req.method === "OPTIONS") {
      return new NextResponse(null, { status: 204 });
    }

    // Validate x-api-backend
    const targetUrl = req.headers.get("x-api-backend");
    if (!targetUrl) {
      return NextResponse.json(
        { error: "Missing x-api-backend" },
        { status: 400 }
      );
    }

    try {
      const url = new URL(targetUrl);
      const allowed = allowedDomains.some((domain) =>
        url.hostname.endsWith(domain)
      );
      if (!allowed || url.protocol !== "https:") {
        throw new Error("Blocked domain");
      }
    } catch (err) {
      console.error("Invalid x-api-backend:", err);
      return NextResponse.json(
        { error: "Invalid x-api-backend" },
        { status: 400 }
      );
    }

    // Rate limit + block list
    const ip =
      req.headers.get("x-forwarded-for")?.split(",")[0].trim() || "unknown";
    if (blockedIPs.has(ip)) {
      return NextResponse.json({ error: "Forbidden IP" }, { status: 403 });
    }

    if (rateLimit) {
      const now = Date.now();
      const record = rateMap.get(ip) || { count: 0, lastReset: now };
      if (now - record.lastReset > rateLimit.windowMs) {
        record.count = 0;
        record.lastReset = now;
      }
      record.count++;
      rateMap.set(ip, record);
      if (record.count > rateLimit.limit) {
        return NextResponse.json(
          { error: "Too Many Requests" },
          { status: 429 }
        );
      }
    }

    // Forward the request
    const headers = new Headers(req.headers);
    headers.set("x-api-key", apiKey); // inject API key

    const fetchOptions: RequestInit = {
      method: req.method,
      headers,
    };

    if (req.method !== "GET" && req.method !== "HEAD") {
      fetchOptions.body = await req.text();
    }

    const response = await fetch(targetUrl, fetchOptions);
    const contentType = response.headers.get("content-type") || "";

    let nextRes: NextResponse;

    if (contentType.includes("application/json")) {
      const json = await response.json();
      nextRes = NextResponse.json(json, { status: response.status });
    } else {
      const text = await response.text();
      nextRes = new NextResponse(text, { status: response.status });
      nextRes.headers.set("Content-Type", contentType);
    }

    return nextRes;
  };
}

/app/api/sdk/route.ts

import { createSecureProxy } from "@/app/server/createSecureProxy";

const handler = createSecureProxy({
  apiKey: process.env.AVERER_API_KEY!,
  allowedDomains: ["idp.staging.redbelly.network", "idp.averer.co"],
  rateLimit: { windowMs: 60000, limit: 10 },
});

export const GET = handler;
export const POST = handler;

/app/api/session/route.ts

import { SignJWT } from "jose";
import { NextRequest, NextResponse } from "next/server";
import { createPublicClient, http } from "viem";
import { redbellyTestnet } from "viem/chains";
import { parseSiweMessage } from "viem/siwe";

const JWT_SECRET = process.env.JWT_SECRET!;
const secret = new TextEncoder().encode(JWT_SECRET);

const client = createPublicClient({
  chain: redbellyTestnet,
  transport: http(),
});

export async function POST(req: NextRequest) {
  const { message, signature } = await req.json();

  try {
    const valid = await client.verifySiweMessage({
      message,
      signature,
    });
    if (!valid) throw new Error("SIWE message verification failed");

    const fields = parseSiweMessage(message);

    // Use jose to create the JWT
    const jwt = await new SignJWT({ sub: fields.address })
      .setProtectedHeader({ alg: "HS256" })
      .setIssuedAt()
      .setExpirationTime("1h")
      .sign(secret);

    const res = NextResponse.json({ success: true });
    res.cookies.set({
      name: "session-token",
      value: jwt,
      httpOnly: true,
      secure: true,
      path: "/",
      sameSite: "strict",
      maxAge: 60 * 60,
    });

    return res;
  } catch (error) {
    console.error("JWT creation or SIWE verification failed:", error);
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
}

middleware.ts

import { jwtVerify } from "jose";
import { NextRequest, NextResponse } from "next/server";

const secret = new TextEncoder().encode(process.env.JWT_SECRET!);

export async function middleware(request: NextRequest) {
  const token = request.cookies.get("session-token")?.value;
  const pathname = request.nextUrl.pathname;

  // Apply only to specific routes
  if (!pathname.startsWith("/api/sdk")) {
    return NextResponse.next();
  }

  if (!token) {
    return NextResponse.json(
      { error: "Unauthorized: No token" },
      { status: 401 }
    );
  }

  try {
    const { payload } = await jwtVerify(token, secret);
    console.log("JWT payload:", payload);
    // Optionally, you can add more checks on the payload here
    return NextResponse.next();
  } catch (err) {
    console.error("JWT verification failed:", err);
    return NextResponse.json(
      { error: "Invalid session token" },
      { status: 401 }
    );
  }
}

export const config = {
  matcher: ["/api/sdk"],
};

SDK Usage Example in Next.js

const EligibilitySDKProvider = dynamic(
  () =>
    import("@redbellynetwork/eligibility-sdk").then(
      (mod) => mod.EligibilitySDKProvider
    ),
  { ssr: false }
);
<EligibilitySDKProvider
  config={{
    network: "staging",
    proxyUrl: "/api/sdk",
  }}
>
  <EligibilityWidget
    onSuccess={onSuccessHandler}
    config={{
      queryHandler: queryHandler,
      authStatusHandler: authStatusHandler,
    }}
  />
</EligibilitySDKProvider>