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