@pinwid/client-widget

Production-ready, framework-neutral Pinwid browser widget for React, Vue, Angular, Vite, plain JavaScript, and HTML websites.
559
Install
npm install @pinwid/client-widget
Latest Version:1.0.16
License:MIT
Last Updated:Oct 22, 2026
Links: npm
Maintainer: khikmat

@pinwid/client-widget

PinWid is a browser feedback widget for bug reports, screenshots, browser diagnostics, optional session replay, and realtime feedback tracking. It is framework-neutral: the same SDK works with React, Next.js, Vue 3, Angular, Vite, plain JavaScript, and static HTML. The current production API is https://api.pinwid.com/api/v1.

This README is the complete integration guide: create the keys, allow the host origin, install the SDK, mount it safely, configure privacy, test it locally, and ship it to production.

Installation

npm install @pinwid/client-widget
pnpm add @pinwid/client-widget
yarn add @pinwid/client-widget

Node.js 18.18+ is required for local builds. The browser bundle requires a modern browser with ShadowRoot, fetch, and ResizeObserver support. The published ESM entry bundles the widget's private React runtime, so Vue and Angular applications do not need to install React separately. The host application's own framework remains isolated from the widget by its ShadowRoot.

Credentials and security

Every widget instance requires a public project key and an environment key. These identify the destination environment and are safe to expose in browser code. Never put a server API token, database credential, webhook secret, or other private secret in frontend configuration.

Create or copy these values in the PinWid dashboard from the target project/environment:

  • Project public key — starts with bp_pub_.
  • Environment public key — the public key of the environment receiving feedback.

In the same environment, configure Allowed origins. Add exact origins such as https://app.example.com, http://localhost:5173, or http://127.0.0.1:5173. Do not include a path, query string, hash, or the API URL. localhost and 127.0.0.1 are different origins.

The apiUrl is expected to be visible in browser requests. A browser must know where to send feedback, so https://api.pinwid.com/api/v1 is not a secret and does not need to be hidden. Security comes from the public project/environment keys, the dashboard's Allowed origins, backend validation, rate limits, and never exposing private server credentials. Do not put BACKEND_INTERNAL_URL, database URLs, JWT secrets, webhook secrets, or npm tokens in this configuration.

Image upload optimization

Screenshots and image attachments are optimized in the browser before upload:

  • images larger than 2560px on their longest side are resized proportionally;
  • WebP is used when the browser supports it and produces a smaller file;
  • PNG remains the lossless fallback, and an original file is retained when an optimization attempt would make it larger;
  • screenshots use a higher quality setting than ordinary attachments;
  • Cloudinary delivery URLs use f_auto,q_auto,w_1920,c_limit, so dashboards and previews receive a responsive, compressed format while the optimized Cloudinary asset remains addressable by its storage key.

This keeps text and UI details readable while reducing upload size and dashboard bandwidth. If a browser cannot decode or encode an image, the widget safely uploads the original file instead of blocking the feedback report.

Basic usage

import { init } from "@pinwid/client-widget";

const widget = init({
  publicKey: "bp_pub_...",
  environmentKey: "env_...",
  apiUrl: "https://api.pinwid.com/api/v1",
  position: "bottom-right",
  primaryColor: "#2563eb",
  metadata: { app: "storefront", release: "2026.10.0" },
});

await widget.ready;

In React, initialize once and clean up on unmount:

import { useEffect } from "react";
import { destroy, init } from "@pinwid/client-widget";

export function PinwidWidget() {
  useEffect(() => {
    init({
      publicKey: import.meta.env.VITE_PINWID_PUBLIC_KEY,
      environmentKey: import.meta.env.VITE_PINWID_ENVIRONMENT_KEY,
      apiUrl: import.meta.env.VITE_PINWID_API_URL,
    });
    return () => destroy();
  }, []);
  return null;
}

For Next.js, place the integration in a client component ("use client") or initialize from a client-only effect. Do not initialize during server rendering. init() replaces a previous instance; call destroy() when the host application is unmounted or the user signs out.

Example Next.js client component:

"use client";

import { useEffect } from "react";
import { destroy, init } from "@pinwid/client-widget";

export function PinwidWidget() {
  useEffect(() => {
    const client = init({
      publicKey: process.env.NEXT_PUBLIC_PINWID_PUBLIC_KEY!,
      environmentKey: process.env.NEXT_PUBLIC_PINWID_ENVIRONMENT_KEY!,
      apiUrl: process.env.NEXT_PUBLIC_PINWID_API_URL ?? "https://api.pinwid.com/api/v1",
    });
    void client.ready;
    return () => destroy();
  }, []);

  return null;
}

For Next.js, browser-visible environment variables must use the NEXT_PUBLIC_ prefix:

NEXT_PUBLIC_PINWID_API_URL=https://api.pinwid.com/api/v1
NEXT_PUBLIC_PINWID_PUBLIC_KEY=bp_pub_...
NEXT_PUBLIC_PINWID_ENVIRONMENT_KEY=env_...

Vue 3

The ESM entry can be imported directly from a Vue 3 component. It owns the widget lifecycle and is safe to use from onMounted; cleanup is required when the component is removed.

<script setup lang="ts">
import { onBeforeUnmount, onMounted } from "vue";
import { destroy, init } from "@pinwid/client-widget";

onMounted(() => {
  const client = init({
    publicKey: import.meta.env.VITE_PINWID_PUBLIC_KEY,
    environmentKey: import.meta.env.VITE_PINWID_ENVIRONMENT_KEY,
    apiUrl: import.meta.env.VITE_PINWID_API_URL ?? "https://api.pinwid.com/api/v1",
  });

  void client.ready.catch(console.error);
});

onBeforeUnmount(() => destroy());
</script>

For Vue SSR/Nuxt, put this code in a client-only component/plugin and never call init() during server rendering.

Angular

Initialize from a browser lifecycle hook and destroy the singleton when the host component is destroyed. The package bundles its private React runtime, so no React dependency is required in the Angular application.

import { AfterViewInit, Component, OnDestroy } from "@angular/core";
import { destroy, init } from "@pinwid/client-widget";

@Component({
  selector: "app-root",
  standalone: true,
  template: "<main>App</main>",
})
export class AppComponent implements AfterViewInit, OnDestroy {
  ngAfterViewInit(): void {
    const client = init({
      publicKey: "bp_pub_...",
      environmentKey: "env_...",
      apiUrl: "https://api.pinwid.com/api/v1",
    });

    void client.ready.catch(console.error);
  }

  ngOnDestroy(): void {
    destroy();
  }
}

For Angular SSR, guard initialization with isPlatformBrowser or move it into a browser-only component. Store environment-specific public values in Angular's environment.ts; never put server secrets there.

Framework lifecycle rules

  • Call init() once per host page/application. Calling it again replaces the previous widget instance.
  • Call destroy() when the host application, route-level shell, or signed-in session is removed.
  • Do not initialize during SSR/server rendering.
  • Public keys and the API URL may be present in frontend code; private API tokens and server credentials must not be.
  • The widget mounts in an isolated ShadowRoot, so host-framework CSS does not overwrite its controls and the widget does not require a framework component tree.

Configuration

Option Type Default Description
publicKey string required Public project key.
environmentKey string required Public environment key.
environmentId string deprecated Backwards-compatible alias for environmentKey.
apiUrl string https://api.pinwid.com/api/v1 API URL including the /api/v1 prefix.
position "bottom-right" | "bottom-left" "bottom-right" Launcher position.
primaryColor string "#635bff" Widget primary color.
accentColor string "#c026d3" Secondary gradient/accent color.
brandName string "Pinwid" Optional white-label name shown in the widget header.
brandLogoUrl string unset HTTP(S) URL or same-origin path for the widget logo.
hidePoweredBy boolean false Hide the PinWid attribution. Workspace/project branding from the dashboard takes precedence.
language "uz" | "en" | "ru" "uz" Widget language.
dateFormat "DD.MM.YYYY" | "MM/DD/YYYY" | "YYYY-MM-DD" "DD.MM.YYYY" Date format used by tracking views.
captureConsole boolean true Include captured console errors in reports.
captureNetwork boolean true Include captured failed requests in reports.
captureSessionAudio boolean false Opt in to microphone capture for Session Replay. Disabled by default.
maxAttachments number 5 Maximum attachments; clamped to 0..10.
metadata Record<string, unknown> {} Context attached to new feedback.
sentryDsn string unset Optional public Sentry DSN. Enables browser error capture and event correlation.
sentryRelease string unset Release label attached to Sentry events.
sentryEnvironment string unset Environment label attached to Sentry events.
sentryEventId string | null unset Existing Sentry event ID supplied by the host app.
enabled boolean true Disable the widget without removing integration code.

The API URL must include the backend API prefix, for example http://localhost:5000/api/v1 locally.

Plain JavaScript

import { init } from "@pinwid/client-widget";

const widget = init({
  publicKey: "bp_pub_...",
  environmentKey: "env_...",
  apiUrl: "https://api.pinwid.com/api/v1",
  sentryDsn: "https://public-key@o0.ingest.sentry.io/0",
  sentryRelease: "web@1.0.0",
  sentryEnvironment: "production",
});

widget.ready.catch(console.error);

Calling init again replaces the current instance. Call destroy() when the host application is unmounted.

For a controlled integration test, capture a handled error after initialization:

import { captureSentryException } from "@pinwid/client-widget";

captureSentryException(new Error("PinWid Sentry integration test"));

CDN and static HTML

Automatic initialization

<script
  src="https://cdn.jsdelivr.net/npm/@pinwid/client-widget@1.0.16/dist/pinwid.min.js"
  data-public-key="bp_pub_..."
  data-environment-key="env_..."
  data-api-url="https://api.pinwid.com/api/v1"
  data-sentry-dsn="https://public-key@o0.ingest.sentry.io/0"
  data-sentry-release="web@1.0.0"
  data-sentry-environment="production"
></script>

The browser bundle auto-initializes when both keys are present. Do not add a second window.Pinwid.init() call for this automatic example.

Manual initialization

<script src="https://cdn.jsdelivr.net/npm/@pinwid/client-widget@1.0.16/dist/pinwid.min.js"></script>
<script>
  const client = window.Pinwid.init({
    publicKey: "bp_pub_...",
    environmentKey: "env_...",
    apiUrl: "https://api.pinwid.com/api/v1",
  });
  client.ready.then(() => console.log("PinWid ready"));
</script>

Use either automatic initialization through the script attributes or manual initialization through window.Pinwid.init; do not use both for the same script unless you intentionally want to replace the first instance. The supported script attributes are data-public-key, data-environment-key, data-api-url, data-enabled, data-position, data-capture-console, data-capture-network, data-capture-session-audio, data-sentry-dsn, data-sentry-release, and data-sentry-environment. Audio remains disabled unless explicitly set to true.

Sentry integration is optional and does not add the Sentry SDK to the default widget bundle. A DSN is a public identifier and may be included in client code; never expose a Sentry auth token in the widget. When the host page already initializes Sentry, Pinwid reuses that SDK and does not create a duplicate client. Otherwise, the Sentry browser runtime is lazy-loaded from Sentry's official CDN only when sentryDsn is configured. If the site's Content Security Policy blocks that CDN, install and initialize Sentry in the host application and expose its SDK as window.Sentry.

Allowed origins

Configure exact origins for the public environment in the dashboard:

http://localhost:5173
http://127.0.0.1:5173
https://app.example.com

Do not include a path, query string, or hash. Backend CORS variables control server policy; they do not replace the environment's public-origin allowlist.

Realtime feedback tracking

After a report is created, the widget subscribes to its status stream. Status changes made in the dashboard or an external integration are reflected in the client without a page refresh. If realtime transport is unavailable, the last known state is kept and normal submission continues.

Session replay, audio, and screen capture

Replay is optional and starts only through widget controls. By default, rrweb masks every input, scans page text for credentials/PII, blocks high-risk controls, and does not record canvas, images, fonts, or cross-origin iframes. Microphone capture is disabled by default; set captureSessionAudio: true only after obtaining the appropriate user consent. Audio requires browser support, a secure context, a user gesture, and explicit permission. Permission denial never prevents ordinary feedback submission.

This protection is applied before replay events are uploaded. It is not a guarantee that arbitrary pixels selected by a user, a canvas, or an already-rendered screenshot contain no secrets; do not select sensitive areas for screen capture and mark private UI with data-pinwid-private.

Mark private content before enabling replay:

<div data-pinwid-private>Never record this section.</div>
<span data-pinwid-mask>Mask this text in replay.</span>

Use data-pinwid-private and data-pinwid-mask for privacy controls.

The legacy aliases data-bugpin-private, data-sensitive, data-private, data-bugpin-mask, and data-bugpin-ignore are also recognized. These controls apply to replay/recording; they cannot guarantee that a user-selected screenshot contains no secret pixels.

Privacy boundaries at a glance

Data path Automatic protection What the host team must do
Session Replay Inputs are masked, private/block regions are excluded, page text is scanned for common credentials and personal data, and events are sanitized again before upload. Canvas, inline images, fonts and cross-origin iframes are disabled by default. When an upload is interrupted, retry chunks may remain temporarily in browser IndexedDB and are cleaned after about one hour. Mark application-specific private areas with data-pinwid-private and test a real replay before production.
Screenshots and screen capture The browser captures only the region selected by the user and the editor can export a blurred version. Replay masking does not rewrite pixels in an already captured image. Select only the required region, blur secrets in the editor, review the final preview, and never capture passwords, API keys, payment data or private dashboards.
Console diagnostics Console values are stringified and redacted by sensitive key and credential patterns before they are attached to feedback. Do not intentionally log secrets; disable captureConsole when the host application cannot guarantee safe logs.
Network diagnostics Only failed fetch/XHR metadata is collected: method, sanitized URL, status, duration and timestamp. Request/response bodies and headers are not collected; sensitive URL parameters are redacted. Disable captureNetwork if URLs can contain business-sensitive data that the built-in redaction rules do not cover.
Credentials Browser configuration contains only the public project key, environment key and visible API URL. Provider tokens, database credentials, webhook secrets and server signing keys must stay on the server. Keep private credentials out of frontend code, metadata, console logs and screenshots.

These controls reduce exposure; they do not replace consent, a lawful basis for collecting visitor data, access reviews, retention settings, or a final human review of customer-submitted pixels. Configure the workspace retention period and allowed origins before production use.

Lifecycle

import { destroy, init } from "@pinwid/client-widget";

const widget = init({
  publicKey: "bp_pub_...",
  environmentKey: "env_...",
  apiUrl: "https://api.pinwid.com/api/v1",
});
await widget.ready;
destroy();

destroy() removes listeners, timers, realtime subscriptions, replay observers, and the widget host. It is safe to call more than once.

Local playground

Install dependencies and copy the local environment template:

pnpm install
cp .env.local.example .env.local

On PowerShell use Copy-Item .env.local.example .env.local. Then set the local values in .env.local:

VITE_PINWID_PUBLIC_KEY=bp_pub_...
VITE_PINWID_ENVIRONMENT_KEY=env_...
VITE_PINWID_API_URL=http://localhost:5000/api/v1

Add http://localhost:5173 to Allowed origins and start the playground:

pnpm dev

Build and package verification

pnpm typecheck
pnpm build
pnpm pack:check

Run the browser end-to-end suite when Playwright browsers are installed:

pnpm test:e2e:install
pnpm test:e2e

Output: dist/pinwid-client.es.js (ESM), dist/pinwid-client.iife.js (library IIFE), dist/pinwid.min.js (browser/CDN bundle), and dist/types (TypeScript declarations).

Troubleshooting

Widget does not appear: verify both public keys, enabled !== false, the /api/v1 API prefix, and the allowed origin.

The launcher is mounted only after the API validates the project key, environment key, and current page origin. If one character is wrong, or the page origin is not in Allowed origins, no empty launcher is shown. After changing Vite environment variables, restart the dev server (they are embedded at startup).

Configuration errors: confirm that publicKey and environmentKey belong to the same project and environment.

CDN changes are missing: pin the CDN version. A CDN bundle cannot read the embedding site's .env; pass runtime values through data-* attributes or window.Pinwid.init.

Realtime status is missing: verify API connectivity and that the feedback belongs to the same public environment. A blocked WebSocket/SSE connection does not break submission.

Replay or capture fails: use HTTPS (or localhost), start from a user gesture, and grant browser permission.

Next.js SSR or hydration warnings appear: move initialization into a component with "use client" and useEffect; never call init() from a server component or server-only module.

Production checklist

  • Use public keys only in browser code.
  • Configure exact allowed origins.
  • Use HTTPS in production (localhost is acceptable for local development).
  • Pin the npm/CDN version and upgrade intentionally.
  • Test loading, submission, attachments, and realtime status updates.
  • Mark sensitive content with data-pinwid-private or data-pinwid-mask.
  • Verify the API URL points to the intended environment.
  • Never commit .env.local, server tokens, webhook secrets, or database credentials.

Compatibility

The package is published as @pinwid/client-widget. Use the Pinwid configuration names documented above in all new integrations. The ESM entry is framework-neutral; the pinwid.min.js entry is the no-build browser/CDN bundle for static HTML, Vue, Angular, and other hosts.

License

MIT

Related Packages

ff-widget

FeedForward Widget - Universal Web Component for feedback collection. Works with...

2,421
@discoverworthy/deploy

Deploy static sites to DiscoverWorthy hosting

3,281
@sanity-labs/logo-soup

A tiny framework-agnostic library that makes logos look good together

72,239 416
monobill-mintui

A modern framework-agnostic UI kit built as Web Components. Requires Tailwind CS...

22,987
snagset

Click-to-comment review for a live website. One script tag, or one import. React...

4,053 0

Version History

Version Released License
1.0.16 MIT
1.0.15 MIT
1.0.14 MIT
1.0.13 MIT
1.0.12 MIT
Pre-releases (1)
Version Released License
0.0.0-stage