@sylergydigital/issue-pin-sdk
v0.6.10
Published
Issue Pin — in-app visual feedback SDK
Readme
@sylergydigital/issue-pin-sdk
In-app visual feedback SDK for Issue Pin.
Installation
1. Install
npm install @sylergydigital/issue-pin-sdk2. Peer dependencies
Ensure these are installed in your app:
npm install react react-dom @supabase/supabase-js html2canvas-pro lucide-reactThe public GitHub repo at https://github.com/sylergydigital/issue-pin-sdk is a read-only mirror of the publishable SDK package.
The package is published publicly on npm, so no .npmrc or GitHub Packages token setup is required for installation.
npm package page: https://www.npmjs.com/package/@sylergydigital/issue-pin-sdk
Source changes should still be made in the private source repo and will sync across automatically.
react-router-dom is not required. The SDK tracks window.location for thread fetching and ?highlight_thread= without router context.
Usage
The SDK is disabled by default. Pass enabled={true} (or bind it to state) to activate the feedback UI.
import { useState } from "react";
import { IssuePin } from "@sylergydigital/issue-pin-sdk";
import { supabase } from "./lib/supabase"; // your app's Supabase client
function App() {
const [feedbackOn, setFeedbackOn] = useState(false);
return (
<>
<nav>
<button onClick={() => setFeedbackOn(f => !f)}>
💬 Feedback
</button>
</nav>
<Home />
{/* Recommended: pass supabaseClient for automatic user identity */}
<IssuePin
apiKey="ew_live_..."
enabled={feedbackOn}
supabaseClient={supabase}
/>
</>
);
}Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| apiKey | string | — | Required. Your workspace API key |
| enabled | boolean | false | Toggle feedback UI visibility without unmounting |
| supabaseClient | SupabaseClient | — | Recommended. Your app's Supabase client — auto-extracts user identity from the auth session and auto-federates users into the workspace |
| user.id | string | — | Manual user ID for attribution (overrides auto-detect) |
| user.email | string | — | Manual user email (overrides auto-detect) |
| user.displayName | string | — | Name shown on comments (overrides auto-detect) |
| allowPinOnPage | boolean | true | Allow creating new element-based pins from the launcher |
| allowScreenshot | boolean | true | Allow screenshot capture from the launcher |
| showHints | boolean | true | Show SDK-owned instructional hints and coachmarks, including the "Open Issue Pin" toast |
| scrollContainer | { key: string; ref: RefObject<HTMLElement> } | — | Store and render new element pins in a host-owned scroll container |
| resolveAnchor | (target: Element) => { key: string; selector?: string \| null } \| null | — | Resolve a stable logical anchor key for clicked elements |
| pinsVisible | boolean | true | Show historical thread pins independently from the rest of the SDK UI |
| buttonPosition | "bottom-right" \| "bottom-left" | "bottom-right" | Floating button position |
| renderLauncher | (props) => ReactNode | — | Replace the stock launcher while keeping SDK-owned state/actions |
| renderModalCaptureButton | (props) => ReactNode | — | Replace the default screenshot button injected into open dialogs |
| showModalCaptureButton | boolean | true | Control whether modal screenshot buttons are injected |
| mode | "view" \| "annotate" | — | Controlled annotate mode — use with onModeChange |
| onModeChange | (mode: FeedbackMode) => void | — | Called when annotate mode changes |
| feedbackActive | boolean | — | Deprecated. Use mode: annotate ↔ true, view ↔ false |
| onFeedbackActiveChange | (active: boolean) => void | — | Deprecated. Use onModeChange |
Migration (v0.x → coordinate-first pins)
x_position/y_positionremain the persisted source of truth.- Element pins now resolve in this order:
anchor_key→selector→ stored coordinates. - Without
scrollContainer, element pins stay document-relative. WithscrollContainer, new element pins are stored and rendered in that container's coordinate space. - Prefer
mode/onModeChangeinstead offeedbackActive/onFeedbackActiveChangefor clarity. - No
<BrowserRouter>wrapper is required for the SDK. The SDK listens tohistory.pushState/replaceStateandpopstateso thread fetching and?highlight_thread=stay in sync in SPAs without importing React Router.
TypeScript
import type { FeedbackMode } from "@sylergydigital/issue-pin-sdk";
// FeedbackMode = "view" | "annotate"Logical anchors in a scroll container
import { useRef } from "react";
import { IssuePin, useIssuePinAnchor } from "@sylergydigital/issue-pin-sdk";
function VisitRow({ visit }: { visit: { id: string; name: string } }) {
const ref = useIssuePinAnchor(`visit-row:${visit.id}`);
return <div ref={ref} data-visit-id={visit.id}>{visit.name}</div>;
}
function AppShell() {
const mainRef = useRef<HTMLDivElement>(null);
return (
<>
<div ref={mainRef} style={{ overflow: "auto", height: 600 }}>
<VisitRow visit={{ id: "123", name: "Annual review" }} />
</div>
<IssuePin
apiKey="ew_live_..."
enabled
scrollContainer={{ key: "dashboard-main", ref: mainRef }}
resolveAnchor={(target) => {
const row = target.closest("[data-visit-id]");
if (!row) return null;
return {
key: `visit-row:${row.getAttribute("data-visit-id")}`,
};
}}
/>
</>
);
}Layer stacking (z-index)
UI layers use a single internal scale exported as Z (pins → overlay → popover → launcher → screenshot modal → flash). If your app uses very high z-index values (e.g. 99999 modals), raise them above Z.launcher (or import Z and position relative to it):
import { Z } from "@sylergydigital/issue-pin-sdk";
// Example: host modal above the SDK launcher
<div style={{ zIndex: Z.launcher + 1 }}>...</div>Controlling visibility
Since enabled defaults to false, the SDK mounts its provider (preserving state and subscriptions) but renders no UI until activated. Common patterns:
Navbar toggle (recommended)
const [feedbackOn, setFeedbackOn] = useState(false);
<button onClick={() => setFeedbackOn(f => !f)}>Feedback</button>
<IssuePin apiKey="ew_live_..." enabled={feedbackOn} />Role-based
{["qa", "admin"].includes(currentUser.role) && (
<IssuePin apiKey="ew_live_..." enabled={feedbackOn} user={currentUser} />
)}Environment-based
{import.meta.env.VITE_APP_ENV !== "production" && (
<IssuePin apiKey="ew_live_..." enabled={feedbackOn} />
)}Advanced usage
Use the built-in customization props first. Composing low-level SDK internals directly is no longer the recommended way to customize launcher behavior.
Launcher action flags
<IssuePin
apiKey="ew_live_..."
supabaseClient={supabase}
enabled={feedbackOn}
allowPinOnPage={false}
/>If only one launcher action is enabled, the stock launcher triggers it directly instead of opening the menu.
Compose feature flags
<IssuePin
apiKey="ew_live_..."
enabled={feedbackEnabled}
allowPinOnPage={pinOnPageEnabled}
allowScreenshot={screenshotEnabled}
pinsVisible={pinsVisible}
showHints={false}
/>Hide historical pins without disabling feedback
<IssuePin
apiKey="ew_live_..."
supabaseClient={supabase}
enabled
pinsVisible={showHistoricalPins}
/>Custom launcher
<IssuePin
apiKey="ew_live_..."
supabaseClient={supabase}
enabled
renderLauncher={({ mode, canPinOnPage, canScreenshot, toggleMenu, enterPinMode, exitPinMode, startScreenshotCapture }) => (
<div style={{ position: "fixed", right: 24, bottom: 24 }}>
{mode === "annotate" ? (
<button onClick={exitPinMode}>Exit pin mode</button>
) : canPinOnPage ? (
<button onClick={enterPinMode}>Pin on page</button>
) : canScreenshot ? (
<button onClick={() => void startScreenshotCapture()}>Screenshot</button>
) : (
<button onClick={toggleMenu}>Feedback</button>
)}
</div>
)}
/>Custom modal screenshot button
<IssuePin
apiKey="ew_live_..."
supabaseClient={supabase}
enabled
renderModalCaptureButton={({ captureScreenshot }) => (
<button
onClick={() => void captureScreenshot()}
style={{ position: "absolute", top: 12, right: 12 }}
>
Capture
</button>
)}
/>Low-level composition (advanced only)
Import individual components only if you need full custom assembly:
import { FeedbackProvider, FeedbackButton, FeedbackOverlay, ThreadPins } from "@sylergydigital/issue-pin-sdk";
function App() {
return (
<FeedbackProvider apiKey="ew_live_...">
<YourApp />
<FeedbackOverlay />
<ThreadPins />
<FeedbackButton />
</FeedbackProvider>
);
}ThreadPins renders markers at stored document-percentage coordinates and updates on scroll/resize (no CSS selector resolution).
User identity & auto-federation
Supabase auth (recommended — zero config)
If your app uses Supabase for authentication, just pass your existing client. The SDK:
- Auto-detects identity from
auth.getSession()— no manual user props needed - Auto-federates the user into the Issue Pin workspace as a
commenter— they appear in the workspace member list and their comments are properly attributed - Uses the dashboard org
site_urlfor thread deep-links when a user clicks an in-app pin
import { supabase } from "./lib/supabase";
<IssuePin apiKey="ew_live_..." supabaseClient={supabase} enabled={feedbackOn} />No backend code or sync secrets are needed in the host app. On the first successful federation call, Issue Pin verifies the external Supabase JWT against its issuer JWKS and bootstraps the workspace trust mapping automatically.
How auto-federation works
When a user is detected via supabaseClient:
- The SDK calls the
sdk-federateendpoint with the API key + user identity - The endpoint validates the API key, resolves the workspace, verifies the external Supabase JWT against the issuer JWKS, and derives the external project ref from the token issuer
- If the workspace has no active Supabase issuer mapping yet, Issue Pin auto-creates the matching
identity_sourcesandworkspace_identity_sourcesrecords for that issuer - The endpoint upserts the user as a
client_federatedworkspace member withcommenterrole
sequenceDiagram
participant App as Host App
participant SDK as IssuePin SDK
participant EF as sdk-federate (Edge Fn)
participant DB as Database
App->>SDK: Mount with apiKey + supabaseClient
SDK->>SDK: auth.getSession() → extract id, email, name
SDK->>EF: POST { apiKey, externalId, email, displayName }
EF->>DB: SHA-256(apiKey) → resolve_api_key()
DB-->>EF: workspace_id
EF->>DB: verify JWT via issuer JWKS
EF->>DB: bootstrap issuer mapping if missing
EF->>DB: Upsert user_identities
EF->>DB: Upsert workspace_members (commenter role)
DB-->>EF: OK
EF-->>SDK: { success, user_id, workspace_id }
SDK-->>App: User can now post feedback
Note over SDK: Result cached — runs once per sessionThe first verified Supabase issuer becomes the workspace's default SDK federation issuer. If you later need to trust a different or additional issuer, add it explicitly in the Federation UI.
Other auth providers (manual)
For non-Supabase auth systems, pass identity props directly:
// NextAuth / Auth.js
const { data: session } = useSession();
<IssuePin apiKey="ew_live_..." user={{ email: session?.user?.email, displayName: session?.user?.name }} />// Clerk
const { user } = useUser();
<IssuePin apiKey="ew_live_..." user={{ id: user?.id, email: user?.primaryEmailAddress?.emailAddress, displayName: user?.fullName }} />If neither supabaseClient nor user props are provided, a console warning will remind you during development.
Server-to-server federation (advanced)
For bulk provisioning or non-Supabase apps, use the federate-user edge function with a sync_secret. See the Federation docs for details.
Security
The API key is a publishable key — safe to include in client-side code. It only grants scoped access to create threads and comments for the associated workspace, enforced server-side via Row Level Security.
Content Security Policy (CSP)
If your app sets a Content Security Policy header, add these directives so the IssuePin SDK can connect to Supabase, render screenshot previews, and apply inline styles:
connect-src https://*.supabase.co wss://*.supabase.co;
img-src data: blob:;
style-src 'unsafe-inline';Merge these with your existing CSP directives — for example, if you already have connect-src 'self', change it to connect-src 'self' https://*.supabase.co wss://*.supabase.co;.
Narrower alternative: Replace
*.supabase.cowith your project's specific URL (e.g.https://abcdef.supabase.co) for a tighter policy.
Why each directive is needed
| Directive | Sources | Reason |
|-----------|---------|--------|
| connect-src | https://*.supabase.co | REST API calls to read/write threads and comments |
| connect-src | wss://*.supabase.co | WebSocket connections for real-time thread updates |
| img-src | data: blob: | Screenshot preview thumbnails rendered as data URIs and blob URLs |
| style-src | 'unsafe-inline' | html2canvas-pro injects inline styles during screenshot capture |
Programmatic access
The SDK exports a typed constant for use in server-side middleware or build-time CSP generation:
import { CSP_REQUIREMENTS } from "@sylergydigital/issue-pin-sdk";
// CSP_REQUIREMENTS = {
// "connect-src": ["https://*.supabase.co", "wss://*.supabase.co"],
// "img-src": ["data:", "blob:"],
// "style-src": ["'unsafe-inline'"],
// }Runtime detection
When a CSP policy blocks an SDK operation, the browser console shows a warning like:
[IssuePin] CSP violation: connect-src blocked "https://abc.supabase.co/rest/v1/threads".
Add to your Content-Security-Policy: connect-src https://*.supabase.co wss://*.supabase.coThese warnings appear automatically — no debug flag needed.
AI Agent Prompt
Paste this into Claude Code, Cursor, or Codex to integrate the SDK automatically:
Add the Issue Pin feedback SDK to this React app.
Install: npm install @sylergydigital/issue-pin-sdk
Peer deps: npm install react react-dom @supabase/supabase-js html2canvas-pro lucide-react
Integration:
1. Import { IssuePin } from "@sylergydigital/issue-pin-sdk"
2. Add a boolean state: const [feedbackOn, setFeedbackOn] = useState(false)
3. Place <IssuePin apiKey={import.meta.env.VITE_ISSUE_PIN_API_KEY} enabled={feedbackOn} supabaseClient={supabase} /> in your app tree (e.g. layout root)
4. Add a toggle button: <button onClick={() => setFeedbackOn(f => !f)}>💬 Feedback</button>
5. Import your app's Supabase client: import { supabase } from "./lib/supabase"
Constraints:
- enabled defaults to false; SDK mounts but renders no UI until true
- apiKey is a publishable key (safe for client-side code)
- Pass supabaseClient for automatic user identity (recommended for Supabase auth apps)
- Alternative: pass user={{ email: "[email protected]", displayName: "Name" }} for non-Supabase auth
- Optional feature flags: allowPinOnPage, allowScreenshot, pinsVisible, showHints
- Use showHints={false} if the host app wants to suppress SDK instructional coachmarks such as the "Open Issue Pin" toast