Brinpage

BrinPage CPM — Install & Quick Start (Next.js App Router)

A minimal copy-paste setup that works with Next.js (App Router) and BrinPage CPM running locally. You don’t need to install or import the SDK inside your Next.js app: communication with CPM is HTTP.

Important: do not install @brinpage/cpm in your Next.js app. That pattern caused a “Module not found” error in the past. We use a lightweight HTTP client (lib/cpm.ts) that calls CPM at http://localhost:3027 or, if you provide a license, routes through BrinPage Cloud.

Requirements

  • Node 18+ (20+ recommended)
  • Next.js App Router (app/ or src/app/)
  • Able to run CPM locally at http://localhost:3027
  • Optional: BrinPage Cloud account + license key (used by the CPM process, not your Next app)

1) Create a Next.js project

bash
npx create-next-app@latest
# Name: brinpage-quickstart
# Accept defaults
cd brinpage-quickstart

You can use an existing App Router project; steps are identical.

2) Install the CLI and start CPM

You must install @brinpage/cpm so the brinpage CLI is available:

bash
# Inside your Next.js project
npm i @brinpage/cpm

To route/bill via Cloud, set these in the CPM process (not in your Next app):

.env
BRINPAGE_LICENSE_KEY=bp_xxxx
BRINPAGE_API_BASE=https://cloud.brinpage.com

Don’t have a license yet? Create a BrinPage Cloud account and generate your API key.

Get your API key
bash
# Start CPM (bin: "brinpage")
npx brinpage cpm

# CPM panel/API at:
# http://localhost:3027

Health check: open http://localhost:3027/api/health → should return { ok: true, ... }.

3) Add the CPM HTTP client in your Next app

Create file: lib/cpm.ts (project root → ./lib/cpm.ts) — full file below.

It automatically routes to http://localhost:3027 (local CPM). If BRINPAGE_LICENSE_KEY is present, it calls Cloud (/api/sdk/ask) with auth.

typescript
// lib/cpm.ts
export type ChatMessage = { role: 'system' | 'user' | 'assistant'; content: string };

const CLOUD = process.env.BRINPAGE_API_BASE || 'https://cloud.brinpage.com';
const LICENSE =
  process.env.BRINPAGE_LICENSE_KEY ||
  process.env.BRINPAGE_API_KEY ||
  process.env.BRINPAGE_KEY ||
  '';

const LOCAL = process.env.BRINPAGE_SDK_ORIGIN || process.env.IA_STUDIO_ORIGIN || 'http://localhost:3027';

function resolveAskUrl() {
  if (LICENSE) return `${CLOUD}/api/sdk/ask`;
  return `${LOCAL}/api/ask`;
}

function toQuestion(messages?: ChatMessage[]) {
  if (!messages?.length) return '';
  for (let i = messages.length - 1; i >= 0; i--) {
    const m = messages[i];
    if (m.role === 'user' && m.content?.trim()) return m.content.trim();
  }
  return messages.map(m => `${m.role}: ${m.content}`).join('\n').trim();
}

export async function ask(opts: {
  question?: string;
  messages?: ChatMessage[];
  provider?: string;
  model?: string;
  stream?: boolean;
  context?: Record<string, unknown>;
  extraPrompts?: string[];
  tags?: string[];        // <-- include tags so they reach CPM/Cloud
  debugEcho?: boolean;
}) {
  const q = (opts.question ?? toQuestion(opts.messages)).trim();
  if (!q) throw new Error("Missing 'question' input");

  const url = resolveAskUrl();
  const headers: Record<string, string> = {
    'content-type': 'application/json',
    'accept': 'application/json',
  };
  if (url.includes('/api/sdk/ask')) {
    if (!LICENSE) throw new Error('Missing BRINPAGE_LICENSE_KEY to call Cloud');
    headers['authorization'] = `Bearer ${LICENSE}`;
  }

  const res = await fetch(url, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      question: q,
      provider: opts.provider,
      model: opts.model,
      stream: Boolean(opts.stream),
      context: opts.context ?? {},
      extraPrompts: opts.extraPrompts,
      tags: opts.tags,          // <-- now forwarded
      debugEcho: opts.debugEcho,
    }),
    cache: 'no-store',
  });

  const rawText = await res.text();
  let data: any = null; try { data = rawText ? JSON.parse(rawText) : null; } catch {}

  if (!res.ok) {
    const msg = (data && (data.error || data.message)) || (rawText && rawText.slice(0, 400)) || `ask failed (${res.status})`;
    throw new Error(msg);
  }

  return {
    text: (data?.text ?? data?.answer ?? data?.message ?? data?.output ?? '').toString(),
    raw: data ?? rawText,
  };
}

This client calls your local CPM by default and always sends a question. If a license is set, it routes to Cloud.

4) Add a server route in Next to call CPM

  • Create: app/api/chat/route.ts
  • If using src/app/: src/app/api/chat/route.ts
typescript
// app/api/chat/route.ts
import { NextResponse } from 'next/server';
import { ask, type ChatMessage } from '@/lib/cpm';

export const dynamic = 'force-dynamic';

function deriveHeuristics(input: string) {
  const q = (input || '').toLowerCase();

  const shouldPinApple =
    /(^|\W)(apple|squircle|esquina|esquinas)(\W|$)/i.test(input);

  const extraPrompts: string[] = shouldPinApple ? ['apple-context-check.md'] : [];

  const tags: string[] = [];
  if (/apple|ui|ux|squircle/i.test(input)) {
    tags.push('apple', 'ui', 'context');
  }
  if (/finstacking|finanzas|fintech/i.test(input)) {
    tags.push('finstacking', 'finance');
  }

  return { extraPrompts, tags };
}

export async function GET(req: Request) {
  try {
    const { searchParams } = new URL(req.url);
    const q = (searchParams.get('q') ?? '').trim() || 'Hello';

    const { extraPrompts, tags } = deriveHeuristics(q);

    const { text, raw } = await ask({
      question: q,
      provider: process.env.CPM_PROVIDER || 'openai',
      model: process.env.CPM_MODEL || 'gpt-4o-mini',
      ...(extraPrompts.length ? { extraPrompts } : {}),
      ...(tags.length ? { tags } : {}),
      debugEcho: true,
    });

    return NextResponse.json({ ok: true, text, raw });
  } catch (err: any) {
    return NextResponse.json({ ok: false, error: err?.message || 'unexpected error' }, { status: 500 });
  }
}

export async function POST(req: Request) {
  try {
    const body = (await req.json().catch(() => ({}))) as {
      question?: string; q?: string; messages?: ChatMessage[];
      provider?: string; model?: string; stream?: boolean; context?: Record<string, unknown>;
      tags?: string[]; extraPrompts?: string[];
    };

    const question =
      (typeof body?.question === 'string' && body.question) ||
      (typeof body?.q === 'string' && body.q) ||
      '';

    const derived = deriveHeuristics(question);
    const extraPrompts = Array.isArray(body?.extraPrompts) ? body!.extraPrompts : derived.extraPrompts;
    const tags = Array.isArray(body?.tags) ? body!.tags : derived.tags;

    const { text, raw } = await ask({
      question,
      messages: body?.messages,
      provider: body?.provider ?? process.env.CPM_PROVIDER ?? 'openai',
      model: body?.model ?? process.env.CPM_MODEL ?? 'gpt-4o-mini',
      stream: body?.stream,
      context: body?.context,
      ...(extraPrompts.length ? { extraPrompts } : {}),
      ...(tags.length ? { tags } : {}),
      debugEcho: true,
    });

    return NextResponse.json({ ok: true, text, raw });
  } catch (err: any) {
    return NextResponse.json({ ok: false, error: err?.message || 'unexpected error' }, { status: 500 });
  }
}

The route always returns JSON (even on errors) to avoid “Unexpected end of JSON input”.

5) Replace the demo UI with a minimal chat

Replace completely: app/page.tsx (or src/app/page.tsx)

Loading code…

7) Run & Verify (two terminals)

Terminal A — CPM:

bash
npx brinpage cpm
# http://localhost:3027

Terminal B — Next:

bash
npm run dev
# http://localhost:3000
  • UI: open http://localhost:3000 and send “Hello”.
  • Direct route: http://localhost:3000/api/chat?q=Hello → expect { ok: true, text: "...", raw: { ... } }.

Troubleshooting

  • “Module not found: @brinpage/cpm”: don’t install or import @brinpage/cpm in your Next app. Use lib/cpm.ts (HTTP) instead.
  • 500 / “Missing 'question'”: restore lib/cpm.ts and app/api/chat/route.ts exactly from above — they always send question.
  • 404 at /api/chat: wrong folder. Must be app/api/chat/route.ts (or src/app/api/chat/route.ts), not pages/api.
  • CPM unreachable: ensure it runs at http://localhost:3027. Try /api/health.
  • Cloud call 401/403: you tried Cloud without a license. Start CPM with BRINPAGE_LICENSE_KEY (and optional BRINPAGE_API_BASE).
  • TypeScript: “Property 'tags' does not exist on type...”: update lib/cpm.ts to include a tags?: string[] prop (see the version in this guide).

Where do I put my BrinPage Cloud license key?

In the CPM process (the terminal where you run brinpage cpm), e.g.:

.env
BRINPAGE_LICENSE_KEY=bp_xxxx 
BRINPAGE_API_BASE=https://cloud.brinpage.com

Don’t have a license yet? Create a BrinPage Cloud account and generate your API key.

Get your API key

Final project layout (after applying this guide)

bash
your-project/
  app/
    api/
      chat/
        route.ts        ← created (full file)
    page.tsx            ← replaced (full file)
  lib/
    cpm.ts              ← created (full file)
  next.config.ts        ← optional (full file or snippet)
  .env            
  package.json