Framework guide
Comments for Next.js App Router
The widget is a Client Component. The author token is a Route Handler. The secret never reaches the browser.
Install
pnpm add @bootstrapware/comments
Client island
"use client";
import { Comments } from "@bootstrapware/comments";
import "@bootstrapware/comments/styles.css";
export function TaskDiscussion({
user,
authorToken,
renewAuthorToken,
}: {
user: { id: string; name?: string };
authorToken: string;
renewAuthorToken: () => Promise<string>;
}) {
return (
<Comments
appId={process.env.NEXT_PUBLIC_COMMENTS_APP_ID}
publishableKey={process.env.NEXT_PUBLIC_BSW_COMMENTS_PUBLISHABLE_KEY}
user={user}
scope={{ tenantKey: "acme", resourceType: "task", resourceId: "task_1842" }}
authorToken={authorToken}
renewAuthorToken={renewAuthorToken}
/>
);
}NEXT_PUBLIC_COMMENTS_APP_ID is the published id, for example cma_demo in the repo example or the cma_ id from get_comment_install_snippet. NEXT_PUBLIC_BSW_COMMENTS_PUBLISHABLE_KEY is the test or live publishable key. Neither value is a secret.
Server-only token route
getSessionUser loads the cookie session. resourceAccess reads your membership table. For the example task, Ada, Kai, and Noor are members. Boa is a member of tenant other with the same resource id task_1842 and must get 404 on the Acme task. Permissions come from that record. Ada may include moderate. Kai may not. Ignore authorId and permissions on the request body.
// app/api/comments/author-token/route.ts
import { NextResponse } from "next/server";
import { getSessionUser, resourceAccess } from "@/lib/session";
const COMMENTS_APP_ID = process.env.COMMENTS_APP_ID ?? "cma_demo";
export async function POST(request: Request) {
const user = await getSessionUser();
if (!user) return NextResponse.json({ error: "Sign in required." }, { status: 401 });
const body = await request.json();
const resource = {
tenantKey: String(body.tenantKey ?? ""),
resourceType: String(body.resourceType ?? ""),
resourceId: String(body.resourceId ?? ""),
};
const access = await resourceAccess(user.id, resource);
if (!access) {
return NextResponse.json({ error: "The requested resource was not found." }, { status: 404 });
}
const requested = Array.isArray(body.mentionIds)
? body.mentionIds.filter((id) => typeof id === "string")
: [];
const mentions = requested.filter((id) => access.mentionIds.includes(id));
const response = await fetch("https://comments.bootstrapware.co/api/v1/author-tokens", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BSW_COMMENTS_SECRET}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
authorId: user.id,
appId: COMMENTS_APP_ID,
tenantKey: resource.tenantKey,
resourceType: resource.resourceType,
resourceId: resource.resourceId,
permissions: access.permissions,
mentions,
expiresInSec: 300,
}),
});
const json = await response.json();
if (!response.ok) return NextResponse.json(json, { status: response.status });
return NextResponse.json({ authorToken: json.data.authorToken });
}Set BSW_COMMENTS_SECRET to the full dashboard secret, bsw_test_sec_… or bsw_live_sec_…. Leave the rest of the key out of git. The browser's renewAuthorToken function POSTs this route and returns authorToken only.
BYO Route Handlers in examples/comments-nextjs follow the same session rule and persist to a JSON file. See persisted BYO.
Related: Identity and tokens · React · API keys