Add a feedback widget to a Next.js app
A complete App Router guide for collecting feedback in-product, identifying users, customizing the widget, and verifying submissions.
This guide adds Upstep to a Next.js App Router application without turning your layout into a Client Component. The widget mounts once in the browser, while the rest of the app can remain server-rendered.
Before you begin
- ✓A Next.js 13+ application using the App Router
- ✓An Upstep project and its project API key
- ✓Node.js 18 or later
Create a project and choose the right key
Create an app in the Upstep dashboard, then copy its project API key from Settings. This key is intentionally used in the browser: it only identifies the feedback project. Do not put database, Stripe, auth, or other server-only credentials in the same environment variable.
- Use one Upstep project per customer-facing product or environment.
- For staging, create a separate Upstep project so test feedback never appears in production.
- Add the value to .env.local and do not commit it.
NEXT_PUBLIC_UPSTEP_KEY=upstep_your_project_keyInstall the browser SDK
The JavaScript SDK supplies the launcher, feedback form, voting board, and API client. It does not require a server-side route in your Next.js app.
npm install @upstep/jsMount the widget in one Client Component
The SDK reads browser APIs, so initialize it in a file marked use client. Keep this component tiny and render it once from the root layout. The rest of your layout, including data fetching and authentication, can stay as Server Components.
// app/components/feedback-widget.tsx
"use client";
import { useEffect } from "react";
import Upstep from "@upstep/js";
export function FeedbackWidget() {
useEffect(() => {
Upstep.init({
apiKey: process.env.NEXT_PUBLIC_UPSTEP_KEY!,
theme: "auto",
position: "right",
});
}, []);
return null;
}Render it from the root layout
Place the component immediately inside body so it survives client-side navigation. Upstep only needs mounting once; adding it to individual pages creates duplicate launchers after navigation.
// app/layout.tsx
import { FeedbackWidget } from "./components/feedback-widget";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<FeedbackWidget />
</body>
</html>
);
}Identify signed-in users after login
Identification is optional, but recommended. A stable internal user ID helps Upstep deduplicate votes and lets you connect useful feedback to a known account. Do not send an email address unless it is also the user ID you intentionally use in your product.
- Render this only after your client-side session has resolved.
- Call Upstep.identify(undefined) when a user signs out.
- Use an opaque database ID rather than a mutable display name.
"use client";
import { useEffect } from "react";
import Upstep from "@upstep/js";
export function IdentifyUpstepUser({ userId }: { userId?: string }) {
useEffect(() => {
Upstep.identify(userId);
}, [userId]);
return null;
}Use your own feedback button instead of the floating launcher
If your product already has a Help menu, command palette, or settings page, hide the default launcher and open the same widget from your own button. This makes feedback feel native to the product while preserving the full Upstep form and voting board.
// feedback-widget.tsx
Upstep.init({
apiKey: process.env.NEXT_PUBLIC_UPSTEP_KEY!,
launcher: false,
});
// Any Client Component
import Upstep from "@upstep/js";
<button onClick={() => Upstep.open()}>Send feedback</button>Test the complete feedback path
Run the application locally, open the widget, submit a feature request, vote for it, and confirm that the item appears in the matching Upstep project. Then test in an incognito browser to verify the anonymous flow separately from the identified-user flow.
Check before moving on
- □The launcher appears exactly once after navigation.
- □A submitted item arrives in the correct project.
- □The user can vote once without a confusing error.
- □The widget follows light and dark mode as expected.
- □Staging traffic does not enter the production board.
Troubleshooting
The widget appears twice in development.+
Make sure the initialization component is rendered once in the root layout. Do not mount it in both a page and a layout.
Nothing appears after deployment.+
Confirm NEXT_PUBLIC_UPSTEP_KEY exists in the deployment environment, then redeploy. Variables prefixed with NEXT_PUBLIC_ are compiled into the client bundle.
Votes are not tied to accounts.+
Call Upstep.identify with the same stable application user ID after your session resolves.