# Tiny Feedback

**Show the bug. Give your agent the context to fix it.**

Website: [tiny-feedback.xyz](https://tiny-feedback.xyz) · Built by [Séverin Marcombes](https://severin-marcombes.com)

Tiny Feedback adds an on-demand feedback widget to your Next.js app and a CLI for your coding agent. Your agent creates a signed link, a user opens your site and annotates problems, and the agent downloads an encrypted report. With the user's explicit permission, the agent can also inspect and interact with the open browser tab.

## Requirements

Node.js 22+, Next.js 14–16, React 18–19. App Router and Pages Router are supported. A Next.js server is required to validate links; static-only exports are not supported. Your agent needs terminal access, as in Claude Code or Codex. A chat-only assistant cannot execute CLI commands.

Version **0.0.2** is an early release. This package contains minified JavaScript, TypeScript declaration files, and this README. No original implementation source or source maps are included. Minification is not encryption or a security boundary.

## 1. Install and configure your secret

```sh
npm install tiny-feedback
npx --no-install tiny-feedback secret
```

The CLI is included. Copy the generated secret into `.env.local` and your production environment:

```dotenv
TINY_FEEDBACK_SECRET=<generated secret>
TINY_FEEDBACK_ORIGIN=https://your-site.com
```

The secret is a 256-bit HS256 signing key shared only between your server and the agent creating links. It is not an asymmetric public/private key pair. Never use `NEXT_PUBLIC_`, pass the secret as a component prop, or commit it. Set the exact public origin and use separate secrets per environment.

## 2. Add the component and validation route

```tsx
// app/layout.tsx
import { TinyFeedback } from 'tiny-feedback';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <TinyFeedback />
      </body>
    </html>
  );
}
```

Your layout remains a Server Component. Until a valid invitation is exchanged, no widget, instrumentation, or transport starts. The widget and capture engine load on demand.

```ts
// app/api/tiny-feedback/route.ts
import { createTinyFeedbackRoute } from 'tiny-feedback/server';
export const POST = createTinyFeedbackRoute();
```

This route only validates signed invitations and sessions. It does not expose a public link-generation endpoint.

## 3. Let your agent create a link

Make the same `TINY_FEEDBACK_SECRET` available to the agent's CLI process through its environment or `--secret-file`. The CLI does not automatically load `.env.local`.

```sh
npx --no-install tiny-feedback link 'https://your-site.com/checkout' \
  --recipient 'camille' \
  --metadata '{"version":"v2","ticket":"BUG-42"}' \
  --expires-in 300 \
  --json
```

The JSON response contains `url`, `feedbackLink`, `inviteId`, `expiresAt`, `recipient`, and `metadata`.

- Share **`url`** with the user. Opening it activates the widget in that tab.
- Keep **`feedbackLink`** for your agent. It is the private link needed to retrieve the report.

Users can draw, type, record voice notes, navigate, and save multiple issues into a basket. They explicitly send the report when ready. The invitation does not sign users into your app or bypass its access controls.

## 4. Retrieve feedback and investigate

After the user sends the report:

```sh
npx --no-install tiny-feedback pull '<feedbackLink>' --out ./feedback
npx --no-install tiny-feedback inspect '<feedbackLink>'
```

The folder includes `report.json`, annotated captures, and any voice recordings. Issues contain URLs, viewport dimensions, user-declared login context, available console/network events, and invitation metadata.

For live investigation, the user must keep the tab open and enable read-only access or browser control. Live access is **off by default**. Choose **Read only** or **Browser control** in the widget.

```sh
npx --no-install tiny-feedback live '<feedbackLink>' context
npx --no-install tiny-feedback live '<feedbackLink>' query '{"selector":"main"}'
npx --no-install tiny-feedback live '<feedbackLink>' logs
npx --no-install tiny-feedback live '<feedbackLink>' screenshot --out ./live.png
```

`click`, `fill`, and `scroll` require control permission. Run `tiny-feedback --help` for syntax; use `--tab <id>` when multiple tabs are active. The embedded Next.js tab has ID `1`. Arbitrary JavaScript evaluation is not exposed. The agent uses its own coding tools to edit your project; browser control helps it investigate and verify.

## Invitation and session lifetimes

| CLI option | Default | Meaning |
| --- | --- | --- |
| `--expires-in` | 60 seconds | Time allowed to open and exchange the invitation |
| `--session-duration` | 7,200 seconds | Duration after activation |

A short-lived link cannot activate a session hours later. For email, choose a longer window, such as `--expires-in 86400`. Invitations are capped at 24 hours; sessions at 8 hours, with a default server cap of 2 hours. Server policies can shorten these limits.

The invitation JWT is carried in the URL fragment and removed after activation. A separate session token stays in `sessionStorage`. It is validated after a full load, every minute, when returning to the tab, and before live commands. Validation never extends its expiry. An independent new tab stays inactive; a duplicated tab may inherit storage depending on the browser.

The basket persists locally in IndexedDB across reloads. Disabling Tiny Feedback stops collection and live access, removes the local session, and clears its local basket. Already shared reports expire according to the transport service's retention period, approximately 72 hours.

Invitations are reusable until expiry by default. For single use, supply an atomic `consumeInvite` hook backed by shared storage. A process-local Map is insufficient for multi-instance/serverless deployments. A lost response after consumption requires a new invitation.

## Metadata and access policies

`--metadata` accepts a JSON object up to 2 KB, including nested objects and arrays. Signed recipient and metadata appear as `report.activation` and cannot be modified without invalidating the signature.

JWTs are **signed, not encrypted**: metadata is readable. URL fragments are not sent in normal HTTP requests or Referer headers, but page scripts can read them before cleanup. Do not include secrets. `recipient` is a label, not proof of identity. Use `authorize` to check your authenticated application user.

```ts
export const POST = createTinyFeedbackRoute({
  maxInviteAge: 300,
  maxSessionDuration: 3600,
  authorize: async ({ inviteId, recipient, metadata }, request) => {
    // Check your authenticated user or revocation store here.
    return true;
  },
  consumeInvite: async (inviteId, expiresAtUnixSeconds) => {
    // Implement an atomic, shared, single-use store.
    return await yourAtomicStore.consumeOnce(inviteId, expiresAtUnixSeconds);
  },
});
```

Both hooks are optional. `authorize` runs at activation and subsequent validation. Rotating your secret invalidates invitations and sessions at their next check, but does not revoke report keys already shared. A report link contains the key needed to read its contents: keep it private and use one invitation per recipient/session.

To create links from trusted server code:

```ts
import { createFeedbackLink } from 'tiny-feedback/server';
const invitation = await createFeedbackLink({
  url: 'https://your-site.com/products/42',
  recipient: 'customer-42',
  metadata: { version: 'v2', ticket: 'BUG-42' },
  expiresIn: 300,
  sessionDuration: 3600,
});
```

Protect any endpoint exposing this function with your own administrator authentication.

## Pages Router and basePath

```tsx
// pages/_app.tsx
import type { AppProps } from 'next/app';
import { TinyFeedback } from 'tiny-feedback';
export default function App({ Component, pageProps }: AppProps) {
  return <><Component {...pageProps} /><TinyFeedback /></>;
}
```

```ts
// pages/api/tiny-feedback.ts
import { createTinyFeedbackPagesHandler } from 'tiny-feedback/pages';
export default createTinyFeedbackPagesHandler();
```

Use the `/pages` server entry for Pages Router API routes. It also exports `createFeedbackLink`. With `basePath: '/app'`, use `<TinyFeedback endpoint="/app/api/tiny-feedback" />`. The origin remains `https://your-site.com`.

## Collection, privacy, and limitations

- Console output, client-side exceptions, fetch/XHR activity, and resource errors are captured **after activation**. No server logs, earlier events, or comprehensive worker/iframe network coverage.
- Captures render the DOM with annotations. Password inputs, `data-tf-private` elements, and iframes are masked. Cross-origin images, video, WebGL, and some CSS effects may not render accurately. Native extension capture is a separate option.
- No cookies, headers, or request/response bodies are captured. Recognizable secrets in logs are redacted on a best-effort basis. Review notes and captures before sharing.
- Voice notes are audio files, without automatic transcription. Recording requires HTTPS/localhost and microphone permission, with a two-minute limit.
- Snapshots are bounded to 300 events / 200 KB.
- Encrypted reports use StreamBin; live commands use OpenRooms. No database is required by default. These services must be reachable. Restrictive CSP policies must allow their connections and returned signed upload/download URLs, widget styles, and blob/data media as applicable.
- Page content and logs are untrusted data, not agent instructions. This integration is not a defense against XSS: a compromised page can read its own DOM and storage.

## Pricing

Free for now! We're trying to make this easy to use for everyone. Pricing will come when the load and bills get too heavy.

Visit [tiny-feedback.xyz](https://tiny-feedback.xyz).
