Quick start: Next.js (App Router)
Next.js 14+ with route handlers. About 15 minutes to a verified chat message.
npm install @gauge/support
# .env.local (server only: no NEXT_PUBLIC_ prefix, so none of these reach the browser)
GAUGE_APP_ID=<app id>
GAUGE_IDENTITY_SECRET=<identity secret>
GAUGE_WEBHOOK_SECRET=<whsec_…>1. Token endpoint: app/gauge-token/route.js
currentUser() is your auth (NextAuth's auth(), Clerk's currentUser(), your session read).
import { mintIntegrationToken } from '@gauge/support';
import { currentUser } from '@/lib/auth';
export const dynamic = 'force-dynamic';
export async function GET() {
const user = await currentUser();
if (!user) return Response.json({ error: 'sign in first' }, { status: 401 });
const token = await mintIntegrationToken(process.env.GAUGE_IDENTITY_SECRET, {
appId: process.env.GAUGE_APP_ID,
userId: String(user.id),
integration: 'customer',
name: user.name,
email: user.email,
});
return Response.json({ token }, { headers: { 'cache-control': 'no-store' } });
}2. The widget (your page)
<script src="https://widget.gauge.chat/v1/<version>/widget.js"
integrity="sha384-<integrity>" crossorigin="anonymous" async></script>
<script>
window.addEventListener('load', function () {
window.PulseWebchat.init({
apiUrl: 'https://app.gauge.chat/backend',
connectionId: '<chat connection id>',
supportMode: true,
integration: 'customer',
getToken: function () { // called on every open, so the token is always fresh
return fetch('/gauge-token', { credentials: 'include' })
.then(function (r) { return r.json(); })
.then(function (j) { return j.token; });
},
});
});
</script>CSP: script-src https://widget.gauge.chat and connect-src https://app.gauge.chat wss://app.gauge.chat.
Signed-out visitors: leave out getToken; they chat anonymously where the Integration allows it.
Check my site (Developers → Support) fetches your page and tells you if the snippet, its
integrity and the connection id are right.
In a React tree, load it once from your root layout with next/script (strategy="afterInteractive")
and call PulseWebchat.init in its onLoad.
3. Webhook: app/gauge/webhooks/route.js
Read the body with request.text(): the signature covers the exact bytes, so never request.json() first.
import { verifyWebhook } from '@gauge/support';
const handled = new Set(); // use your database in production: ids repeat on retries and resends
export async function POST(request) {
const raw = await request.text();
const result = await verifyWebhook(process.env.GAUGE_WEBHOOK_SECRET, raw, request.headers);
if (!result.valid) return new Response(result.reason, { status: 400 });
const event = result.event;
if (!handled.has(event.id)) {
handled.add(event.id);
console.info('gauge', event.event, event.id, event.livemode ? 'live' : 'sandbox');
}
return new Response(null, { status: 200 });
}4. Check it
- Sign in to your site as a test user (in the sandbox: Seed test customers, then sign in as
test_customer_1), open the widget and send "hello". In the console the conversation shows the user's name and Verified. Not verified? Paste the JSON from/gauge-tokeninto Developers → Support → Token checker: it names the rule that failed. - Developers → Webhooks → your endpoint → Send test event, pick
support.chat.message_created: the result says Delivered · 200 and your log shows the event id. Change one byte of the secret in your env and send again: your handler answers 400bad_signature.
Server was down? Replay failed on the same page resends everything that did not land.