Next.js

Where each key lives in an App Router project, and how to keep the secret server-side.

Browser or server Embed key in components, integration key in route handlers dev · https://api.dev.oprag.ai

Next.js makes the key boundary easy to get right and easy to get wrong: anything a client component reads must be NEXT_PUBLIC_, and anything else must never be imported from one.

Widget in a client component

app/components/chat.tsx TSX
"use client";

import { useEffect } from "react";
import { mountWidget, type WidgetHandle } from "@oprag/sdk/widget";

export function Chat() {
  useEffect(() => {
    let handle: WidgetHandle | undefined;
    let cancelled = false;

    void mountWidget({
      projectId: process.env.NEXT_PUBLIC_OPRAG_PROJECT_ID!,
      embedKey: process.env.NEXT_PUBLIC_OPRAG_EMBED_KEY!,
      apiUrl: process.env.NEXT_PUBLIC_OPRAG_API_URL!,
    }).then((widget) => {
      if (cancelled) return widget.destroy();
      handle = widget;
    });

    return () => {
      cancelled = true;
      handle?.destroy();
    };
  }, []);

  return null;
}

Integration key in a route handler

app/api/ask/route.ts TypeScript
import { createClient, OpragError } from "@oprag/sdk";

// This module is never bundled for the browser, so the secret stays put.
const oprag = createClient({
  apiUrl: process.env.OPRAG_API_URL!,
  apiKey: process.env.OPRAG_SECRET_KEY!,
});

export async function POST(request: Request) {
  const { question, sessionId, visitorId } = await request.json();

  try {
    const res = await oprag.chat.ask({ question, sessionId, visitorId });
    return Response.json(res);
  } catch (err) {
    if (err instanceof OpragError) {
      return Response.json({ error: err.message }, { status: err.status });
    }
    throw err;
  }
}

Fronting the widget with your own route handler is the backend-for-frontend recipe.

Ready to ship?

Get started free