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/orsrc/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
npx create-next-app@latest
# Name: brinpage-quickstart
# Accept defaults
cd brinpage-quickstartYou 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:
# 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):
BRINPAGE_LICENSE_KEY=bp_xxxx
BRINPAGE_API_BASE=https://cloud.brinpage.comDon’t have a license yet? Create a BrinPage Cloud account and generate your API key.
Get your API key# Start CPM (bin: "brinpage")
npx brinpage cpm
# CPM panel/API at:
# http://localhost:3027Health 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.
// 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
// 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)
7) Run & Verify (two terminals)
Terminal A — CPM:
npx brinpage cpm
# http://localhost:3027Terminal B — Next:
npm run dev
# http://localhost:3000- UI: open
http://localhost:3000and 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/cpmin your Next app. Uselib/cpm.ts(HTTP) instead. - 500 / “Missing 'question'”: restore
lib/cpm.tsandapp/api/chat/route.tsexactly from above — they always sendquestion. - 404 at /api/chat: wrong folder. Must be
app/api/chat/route.ts(orsrc/app/api/chat/route.ts), notpages/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 optionalBRINPAGE_API_BASE). - TypeScript: “Property 'tags' does not exist on type...”: update
lib/cpm.tsto include atags?: 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.:
BRINPAGE_LICENSE_KEY=bp_xxxx
BRINPAGE_API_BASE=https://cloud.brinpage.comDon’t have a license yet? Create a BrinPage Cloud account and generate your API key.
Get your API keyFinal project layout (after applying this guide)
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