Install the widget
The widget is a small launcher that opens a feedback form: an issue, an idea or anything else, a message, an optional email and a screenshot. Pick the install that matches your site, replace your-project with the slug from your project settings, and every submission lands in your inbox.
HTML
Load the script once, then put data-papercuts on the element that should open the form.
<script src="https://papercuts.so/widget/loader.js" async referrerpolicy="no-referrer"></script>
<button type="button" data-papercuts="your-project">Feedback</button>React
@papercuts/react mounts the loader once per page and renders nothing of its own. React is a peer dependency.
import { Papercuts } from "@papercuts/react";
export function App() {
return (
<>
{/* your app */}
<Papercuts project="your-project" />
</>
);
}To open the form from your own menu, call usePapercuts(). Calls made before the loader has run wait for it.
import { usePapercuts } from "@papercuts/react";
export function HelpMenu() {
const { open } = usePapercuts();
return <button onClick={() => open()}>Send feedback</button>;
}Next.js
The component is a client component, so it goes straight into the root layout.
// app/layout.tsx
import { Papercuts } from "@papercuts/react";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Papercuts project="your-project" />
</body>
</html>
);
}Identify signed-in users
identify() attaches your signed-in user to the feedback they send. The proof is computed on your server, with the identify secret from your project settings, and never in the browser.
The hash is the HMAC-SHA256 over the id, email and name joined with a newline, keyed with the project's identify secret, as lowercase hex. An absent email or name is an empty string.
A field that contains a line break is never verified, so the join stays unambiguous. A submission marked Verified in your inbox vouches for the id, the email and the name together; a payload whose email or name differs from what was signed arrives unverified.
// On your server, never in the browser
import { createHmac } from "node:crypto";
const secret = process.env.PAPERCUTS_IDENTIFY_SECRET!;
const hash = createHmac("sha256", secret)
.update([user.id, user.email ?? "", user.name ?? ""].join("\n"))
.digest("hex");
// In the browser, once the loader has run
papercuts.identify({ id: user.id, email: user.email, name: user.name, hash });Set it up from the terminal
npx @papercuts/cli init detects your framework, inserts the snippet or the component in the right file, writes the project key to .env.local and opens the dashboard. It prints the diff first and never edits a file without your confirmation.
npx @papercuts/cli init