Skip to content
Swingback, home

BlogDevelopers

Add a feedback widget to Next.js, with users signed in

Put a feedback widget in a Next.js app with your users already signed in: a route handler signs a short-lived token, and a client component hands it over.

By Elie Baier ·

A feedback widget in a Next.js app takes three parts: a route handler that signs a short-lived token for the user your app already signed in, a client component that puts the widget's frame on the page, and a message from one to the other. Your users then post, vote and comment as themselves, without a second account, and you know who asked for what. Here is each part for the App Router, with Swingback's widget; widgets that take a signed identity work the same way.

Why sign your users in#

A widget can take anonymous feedback, but a request without a person behind it is hard to use:

  • You can't weigh it. "Export to CSV" from a team on your largest plan isn't the same request as from a trial that ended last month.
  • You can't tell them it shipped. The person who asked is the one who most wants to know.
  • You get a second login. Asking people to create an account on the feedback tool loses most of them at the first click.

Your app knows who is signed in. A token signed by your server tells the widget, and nothing else needs to change.

How the parts fit#

  1. The page shows the widget in an iframe, from your portal's /widget address.
  2. When the frame is ready, it posts a ready message to your page.
  3. Your page asks your server for a token, and your server signs one for the session's user.
  4. Your page posts the token to the frame, which answers identified, or error with a reason.

The secret that signs tokens stays on your server. The browser only ever sees tokens, which expire within minutes.

1. Sign a token on the server#

Keep the identity secret, its id and your workspace's id in your server's environment, never in a NEXT_PUBLIC_ variable: those end up in the browser's JavaScript. Signing a token takes a dozen lines of Node.js and no library; Signing in your users has the function, identityToken(user), with every claim it can carry.

Then a route handler hands a token to the signed-in user, and to nobody else:

// app/api/feedback-token/route.ts
import { identityToken } from "@/lib/identity-token"; // the function from the docs
import { currentUser } from "@/lib/auth"; // however your app reads its session

export async function GET() {
  const user = await currentUser();
  if (!user) return Response.json({ error: "signed_out" }, { status: 401 });
  return Response.json({ token: identityToken(user) }, { headers: { "Cache-Control": "no-store" } });
}

Route handlers aren't cached in recent versions of Next.js, and no-store keeps any cache in between from handing one user's token to another.

2. Put the widget on the page#

A client component holds the frame and listens to it. The portal's address is public, so a NEXT_PUBLIC_ variable is fine for it:

// components/feedback-widget.tsx
"use client";

import { useEffect, useRef } from "react";

const portal = process.env.NEXT_PUBLIC_SWINGBACK_PORTAL ?? ""; // such as https://feedback.example.com

export function FeedbackWidget() {
  const frame = useRef<HTMLIFrameElement>(null);

  useEffect(() => {
    async function onMessage(event: MessageEvent) {
      const target = frame.current?.contentWindow;
      // Only the widget's own frame, from the portal's origin.
      if (!target || event.origin !== portal || event.source !== target) return;
      const message = event.data;
      if (message?.source !== "swingback") return;

      if (message.type === "ready") {
        const response = await fetch("/api/feedback-token");
        if (!response.ok) return; // signed out: the widget offers its own way in
        const { token } = await response.json();
        target.postMessage({ source: "swingback", type: "identify", token }, portal);
      } else if (message.type === "error") {
        console.warn("Feedback widget:", message.code);
      }
    }
    window.addEventListener("message", onMessage);
    return () => window.removeEventListener("message", onMessage);
  }, []);

  return (
    <iframe ref={frame} src={`${portal}/widget`} title="Feedback" style={{ width: 400, height: 640, border: 0 }} />
  );
}

Two details matter:

  • Check where a message comes from. Any page can post messages to yours. The handler answers only the widget's own frame, at the portal's origin, and posts the token to that origin alone, so a token never reaches another site.
  • Listen before the frame is ready. The effect runs as soon as the component is on the page, while the frame is still loading, so it hears ready. A listener added later, after a click for instance, would miss it.

Render <FeedbackWidget /> where you want feedback: a page of its own, a panel your app opens, or a dialog. A token works until it expires, so a reload signs the same user in again.

3. Sign them out with your app#

When your user signs out, tell the frame before your app forgets them, so that the next person on that browser isn't writing as them:

frame.contentWindow?.postMessage({ source: "swingback", type: "logout" }, portal);

The frame answers signed-out.

Before you ship it#

  • Allow your app's origin only. Under Settings → Widget, list the sites that may embed the widget, such as https://app.example.com. Browsers then refuse to show it anywhere else, and it ignores messages from other sites.
  • Let your own CSP allow the frame. If your app sends a Content Security Policy, its frame-src needs the portal's origin.
  • Keep tokens short. Ten minutes is plenty: the page asks for a new one each time it loads.
  • Send what helps you rank requests. The token can carry traits, such as a plan, and the companies a user belongs to with what each is worth to you: see Customers and companies.

The widget's docs have the messages and their errors, and the same page as plain HTML for an app that isn't built with Next.js.

Start hearing what your customers want

Set up your portal in minutes. The Free plan has no time limit, and no card is needed.