reflet-sdk
v0.3.3
Published
Official Reflet SDK - Collect user feedback, feature requests, and bug reports from your app
Maintainers
Readme
reflet-sdk
Official SDK for integrating Reflet feedback collection into your application. Collect user feedback, feature requests, and bug reports directly from your app.
Installation
npm install reflet-sdk
# or
yarn add reflet-sdk
# or
pnpm add reflet-sdk
# or
bun add reflet-sdkQuick Start
1. Get Your API Keys
Go to your Reflet dashboard → Settings → API Keys & Widget to generate your keys:
- Public Key (
fb_pub_...) - Safe to use in frontend code - Secret Key (
fb_sec_...) - Keep secure, use only on your server
2. Basic Usage (Any JavaScript/TypeScript Environment)
The core SDK works in any JavaScript environment - browser, Node.js, Deno, Bun, or any framework.
import { Reflet } from 'reflet-sdk';
// Initialize the client
const reflet = new Reflet({
publicKey: 'fb_pub_your_key_here',
user: {
id: 'user_123', // Required: unique user ID from your system
email: '[email protected]', // Optional: for notifications
name: 'Jane Doe', // Optional: display name
},
});
// List feedback
const { items } = await reflet.list({ status: 'open' });
// Get single feedback with comments
const feedback = await reflet.get('feedback_id');
// Create new feedback
const { feedbackId } = await reflet.create({
title: 'Dark mode support',
description: 'Would love to have a dark theme option!',
});
// Vote on feedback (toggles vote on/off)
await reflet.vote('feedback_id');
// Add a comment
await reflet.comment({
feedbackId: 'feedback_id',
body: 'This would be amazing!',
});3. React Integration
For React applications, use the provided hooks for a better developer experience.
import { RefletProvider, useFeedbackList, useVote, useCreateFeedback } from 'reflet-sdk/react';
// 1. Wrap your app with the provider (do this once at the root)
function App() {
// Get the current user from your authentication system
const currentUser = getCurrentUser(); // Replace with your auth logic
return (
<RefletProvider
publicKey="fb_pub_your_key_here"
user={currentUser ? {
id: currentUser.id,
email: currentUser.email,
name: currentUser.name,
} : undefined}
>
<YourApp />
</RefletProvider>
);
}
// 2. Use hooks anywhere in your app
function FeedbackPage() {
const { data, isLoading, error } = useFeedbackList({ sortBy: 'votes' });
const { mutate: vote } = useVote();
const { mutate: createFeedback } = useCreateFeedback();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{data?.items.map((item) => (
<div key={item.id}>
<h3>{item.title}</h3>
<p>{item.description}</p>
<button onClick={() => vote({ feedbackId: item.id })}>
{item.hasVoted ? 'Voted' : 'Vote'} ({item.voteCount})
</button>
</div>
))}
</div>
);
}4. Floating Feedback Widget
One component adds a floating button to your app. Opening it screenshots the current viewport, lets the user draw on that screenshot, and lets them point at an element so the report carries the React component behind it.
npx reflet-cli initOr mount it yourself, once, as the last child of your app shell:
import { RefletFeedback } from 'reflet-sdk/feedback';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<RefletFeedback publicKey={process.env.NEXT_PUBLIC_REFLET_PUBLIC_KEY} />
</body>
</html>
);
}The entry ships its own "use client" directive, so a Next.js layout can stay a
Server Component. With Vite, read the key from
import.meta.env.VITE_REFLET_PUBLIC_KEY and render the widget next to <App />.
Every report carries the URL, page title, browser, OS, device, viewport, locale and timezone, plus the last 30 console errors and warnings. Pointing at an element adds a selector that resolves back to it, the React component stack, and the source file and line when the build exposes them (development and preview builds do; production builds strip React's debug data).
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| publicKey | string | — | Your fb_pub_… key. Optional inside RefletProvider. |
| user | RefletUser | — | Who is reporting. Hides the email field. |
| position | "bottom-right" \| "bottom-left" \| "top-right" \| "top-left" | "bottom-right" | Which corner the button sits in. |
| enabled | boolean | true | Render the widget. Gate it to staff or beta users. |
| dismissForDays | number | — | Let a reporter hide the launcher in this browser for this many days. |
| captureOnOpen | boolean | true | Screenshot as soon as the panel opens. |
| captureConsole | boolean | true | Record console errors and warnings. |
| hotkey | string \| null | null | Shortcut toggling the panel, e.g. "mod+shift+f". |
| theme | "auto" \| "light" \| "dark" | "auto" | Colour scheme. |
| primaryColor | string | — | Any CSS color for the button and accents. |
| offset | number | 20 | Pixels between the button and the viewport edge. |
| metadata | Record<string, string> | — | Extra fields merged into every report. |
| categories | ("bug" \| "idea" \| "question")[] | all three | Category chips to show. |
| labels | Partial<FeedbackWidgetLabels> | — | Override any string for i18n. |
| onSubmit | (result: { feedbackId: string }) => void | — | Fires after a successful send. |
The panel renders in a shadow root, so your CSS cannot reach it and its CSS
cannot reach your app. Screenshots are rendered from the DOM rather than through
getDisplayMedia, so users are never asked for a screen-share permission —
the trade-off is that cross-origin images without CORS headers, iframes and
canvas content may come out blank.
5. Server-Side User Signing (required for voting and commenting)
user is a client-asserted identity: the browser can claim to be anyone, so
the API accepts it only to attribute a report or a survey answer. Voting,
commenting and subscribing require a token signed on your server with
signUser — anything else is rejected with a 401.
On your server (works with any backend: Express, Fastify, Hono, serverless functions, etc.):
import { signUser } from 'reflet-sdk/server';
// This function uses Web Crypto API - works in Node.js 18+,
// Deno, Bun, Cloudflare Workers, Vercel Edge, etc.
export async function generateUserToken(user: YourUserType) {
const { token, expiresAt } = await signUser(
{
id: user.id, // Required: unique identifier
email: user.email, // Optional
name: user.name, // Optional
avatar: user.avatar, // Optional
},
process.env.REFLET_SECRET_KEY! // Your secret key from Reflet dashboard
);
return { token, expiresAt };
}On the client (pass the token from your server):
import { Reflet } from 'reflet-sdk';
const reflet = new Reflet({
publicKey: 'fb_pub_your_key_here',
userToken: tokenFromServer, // JWT received from your server
});Important: What NOT To Do
Security
// ❌ WRONG: Never expose your secret key in client-side code
const reflet = new Reflet({
publicKey: 'fb_pub_xxx',
secretKey: 'fb_sec_xxx', // NEVER DO THIS - secret keys are for servers only
});
// ❌ WRONG: Never hardcode secret keys
const token = await signUser(user, 'fb_sec_hardcoded_key');
// ✅ CORRECT: Use environment variables on the server
const token = await signUser(user, process.env.REFLET_SECRET_KEY!);User Identification
// ❌ WRONG: Don't use unstable or guessable user IDs
const reflet = new Reflet({
publicKey: 'fb_pub_xxx',
user: { id: '[email protected]' }, // Email as ID - can change, easily guessed
});
// ❌ WRONG: Don't use sequential IDs
const reflet = new Reflet({
publicKey: 'fb_pub_xxx',
user: { id: '1' }, // Sequential - easily enumerable
});
// ✅ CORRECT: Use your system's stable, unique user identifier
const reflet = new Reflet({
publicKey: 'fb_pub_xxx',
user: { id: 'usr_a1b2c3d4e5' }, // UUID, CUID, or database ID
});Client Initialization
// ❌ WRONG: Don't create multiple client instances
function handleVote(id: string) {
const reflet = new Reflet({ publicKey: 'fb_pub_xxx' }); // New instance every call
await reflet.vote(id);
}
// ✅ CORRECT: Create one instance and reuse it
const reflet = new Reflet({ publicKey: 'fb_pub_xxx', user: currentUser });
function handleVote(id: string) {
await reflet.vote(id);
}React Provider
// ❌ WRONG: Don't use hooks outside the provider
function App() {
return (
<div>
<FeedbackList /> {/* Error: No RefletProvider */}
<RefletProvider publicKey="fb_pub_xxx">
<OtherStuff />
</RefletProvider>
</div>
);
}
// ✅ CORRECT: Wrap at the root, use hooks inside
function App() {
return (
<RefletProvider publicKey="fb_pub_xxx" user={currentUser}>
<FeedbackList />
<OtherStuff />
</RefletProvider>
);
}Token Handling
// ❌ WRONG: Don't sign tokens on the client
import { signUser } from 'reflet-sdk/server';
// This exposes your secret key in client bundle!
// ❌ WRONG: Don't mix user and userToken
const reflet = new Reflet({
publicKey: 'fb_pub_xxx',
user: { id: '123' },
userToken: 'jwt_token', // Pick one, not both
});
// ✅ CORRECT: Use user for simple cases (client-side identification)
const reflet = new Reflet({
publicKey: 'fb_pub_xxx',
user: { id: '123', email: '[email protected]' },
});
// ✅ CORRECT: Use userToken for production (server-signed)
const reflet = new Reflet({
publicKey: 'fb_pub_xxx',
userToken: tokenFromYourServer,
});API Reference
Reflet Client
const reflet = new Reflet(config: RefletConfig);Configuration
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| publicKey | string | Yes | Your organization's public API key (fb_pub_...) |
| user | RefletUser | No | Client-asserted identity — attributes reports only, never grants voting or commenting |
| userToken | string | No | Signed JWT from your server (signUser), required for voting, commenting and subscribing |
| baseUrl | string | No | Custom API endpoint (defaults to Reflet production API) |
Note: Provide either
userORuserToken, not both. With neither, the user can only read feedback and submit anonymous reports. Withuseralone, reports are attributed but voting, commenting and subscribing stay unavailable.
Methods
| Method | Description | Requires User |
|--------|-------------|---------------|
| list(params?) | List feedback with optional filtering | No |
| get(feedbackId) | Get single feedback with full details | No |
| getConfig() | Get organization configuration | No |
| getRoadmap() | Get roadmap data | No |
| getChangelog(limit?) | Get changelog entries | No |
| create(params) | Submit new feedback | Yes |
| vote(feedbackId, type?) | Vote on feedback (toggle) | Yes |
| comment(params) | Add a comment | Yes |
| getComments(feedbackId) | Get comments for a feedback | No |
| subscribe(feedbackId) | Subscribe to updates | Yes |
| unsubscribe(feedbackId) | Unsubscribe from updates | Yes |
React Hooks
All hooks must be used inside a <RefletProvider>.
| Hook | Description | Requires User |
|------|-------------|---------------|
| useFeedbackList(options?) | Fetch feedback list with filters | No |
| useFeedback(feedbackId) | Fetch single feedback item | No |
| useComments(feedbackId) | Fetch comments for feedback | No |
| useOrganizationConfig() | Fetch organization configuration | No |
| useRoadmap() | Fetch roadmap data | No |
| useChangelog(limit?) | Fetch changelog entries | No |
| useVote() | Vote mutation hook | Yes |
| useCreateFeedback() | Create feedback mutation hook | Yes |
| useAddComment() | Add comment mutation hook | Yes |
| useSubscription() | Subscribe/unsubscribe hooks | Yes |
Server Utilities
Works in any JavaScript runtime with Web Crypto API support: Node.js 18+, Deno, Bun, Cloudflare Workers, Vercel Edge, AWS Lambda, etc.
import { signUser, verifyUser } from 'reflet-sdk/server';
// Sign a user token (async - uses Web Crypto API)
const { token, expiresAt } = await signUser(
{ id: 'user_123', email: '[email protected]', name: 'Jane' },
secretKey,
expiresInSeconds // Optional, defaults to 24 hours
);
// Verify a token (optional - for server-side validation)
const user = await verifyUser(token, secretKey);
// Returns user data if valid, null if invalid/expiredTypes
interface RefletUser {
id: string; // Required: unique identifier from your system
email?: string; // Optional: used for notifications
name?: string; // Optional: display name
avatar?: string; // Optional: avatar URL
}
interface FeedbackItem {
id: string;
title: string;
description: string;
status: FeedbackStatus;
voteCount: number;
commentCount: number;
hasVoted: boolean; // Whether current user voted
author?: FeedbackAuthor;
tags: FeedbackTag[];
createdAt: string;
}
interface FeedbackDetail extends FeedbackItem {
isSubscribed: boolean; // Whether current user is subscribed
comments: Comment[];
}
type FeedbackStatus =
| 'open'
| 'under_review'
| 'planned'
| 'in_progress'
| 'completed'
| 'closed';Error Handling
The SDK provides typed errors for precise error handling:
import { RefletError, RefletAuthError, RefletNotFoundError } from 'reflet-sdk';
try {
await reflet.vote('feedback_id');
} catch (error) {
if (error instanceof RefletNotFoundError) {
// Feedback item doesn't exist
console.log('Feedback not found');
} else if (error instanceof RefletAuthError) {
// User not identified or invalid token
console.log('Please sign in to vote');
} else if (error instanceof RefletError) {
// Other API errors (rate limit, server error, etc.)
console.log('Error:', error.message, 'Status:', error.status);
}
}Common Error Scenarios
| Error | Cause | Solution |
|-------|-------|----------|
| RefletAuthError | No signed token provided | Pass a userToken from signUser; user alone cannot vote or comment |
| RefletAuthError | Invalid or expired token | Generate a new token on your server |
| RefletNotFoundError | Invalid feedback ID | Check the ID exists |
| RefletError (403) | Organization is private | Use a secret key or make organization public |
| RefletError (429) | Rate limited | Reduce request frequency |
Framework Integration Examples
Generic Pattern (Any Framework)
// 1. Create a singleton client instance
// feedback-client.ts
import { Reflet } from 'reflet-sdk';
let refletInstance: Reflet | null = null;
export function getRefletClient(user?: { id: string; email?: string; name?: string }) {
if (!refletInstance || user) {
refletInstance = new Reflet({
publicKey: process.env.REFLET_PUBLIC_KEY || 'fb_pub_xxx',
user,
});
}
return refletInstance;
}
// 2. Use it anywhere
const reflet = getRefletClient(currentUser);
const feedback = await reflet.list();Server-Side Token Generation (Any Backend)
// api/reflet-token.ts (or your preferred API route pattern)
import { signUser } from 'reflet-sdk/server';
export async function handleTokenRequest(authenticatedUser: YourUserType) {
// Verify user is authenticated with YOUR auth system first
if (!authenticatedUser) {
throw new Error('Unauthorized');
}
const { token, expiresAt } = await signUser(
{
id: authenticatedUser.id,
email: authenticatedUser.email,
name: authenticatedUser.displayName,
},
process.env.REFLET_SECRET_KEY!
);
return { token, expiresAt };
}React with Any State Management
// Works with Redux, Zustand, Jotai, MobX, or plain React state
import { RefletProvider } from 'reflet-sdk/react';
function App() {
// Get user from your state management solution
const user = useYourAuthHook(); // useSelector, useStore, useAtom, etc.
return (
<RefletProvider
publicKey="fb_pub_xxx"
user={user ? { id: user.id, email: user.email, name: user.name } : undefined}
>
<YourApp />
</RefletProvider>
);
}Vanilla JavaScript (No Framework)
<script type="module">
import { Reflet } from 'https://esm.sh/reflet-sdk';
const reflet = new Reflet({
publicKey: 'fb_pub_xxx',
user: { id: 'user_123' },
});
// Load and display feedback
const { items } = await reflet.list();
document.getElementById('feedback-list').innerHTML = items
.map(item => `
<div>
<h3>${item.title}</h3>
<p>${item.description}</p>
<button onclick="vote('${item.id}')">${item.voteCount} votes</button>
</div>
`)
.join('');
window.vote = async (id) => {
await reflet.vote(id);
location.reload(); // Simple refresh, or update DOM manually
};
</script>Troubleshooting
"Authentication required" error
Cause: Trying to vote, comment, or create feedback without user identification.
Solution: Provide a server-signed userToken when initializing the client:
const reflet = new Reflet({
publicKey: 'fb_pub_xxx',
user: { id: 'your_user_id' }, // Add this
});"Organization not found" or 404 errors
Cause: Invalid public key or organization doesn't exist.
Solution:
- Verify your public key in Reflet dashboard → Settings → API Keys
- Ensure the organization exists and is active
"Network error" or connection issues
Cause: Cannot reach Reflet API.
Solution:
- Check your internet connection
- Verify no firewall/proxy is blocking requests
- The SDK connects to
https://harmless-clam-802.convex.siteby default
Token expired errors
Cause: Server-signed token has expired (default: 24 hours).
Solution: Generate and send a fresh token from your server:
// On your server
const { token } = await signUser(user, secretKey, 86400); // 24 hours
// Refresh before expiry or on 401 errorsLicense
MIT
