Shopify SDK
Embed Klero feedback, roadmap, and changelog inside your Shopify embedded app. The widgets render using Shopify Polaris web components so they feel native to the Shopify admin.
Requirements
- A Shopify embedded app using React Router (the Shopify app template)
- Polaris web components loaded in your app (included by
AppProviderautomatically) - React >= 18
Installation
npm install @kleroai/shopifyEnvironment variables
KLERO_SLUG=your-project-slug
KLERO_JWT_SECRET=your-jwt-secret # from Klero dashboard → Settings → JWT Secret
KLERO_BASE_URL= # optional: custom domainYour JWT secret is available in the Klero dashboard under Settings → JWT Secret.
Setup: root layout
Set up the nav links in your root app.tsx:
// app/routes/app.tsx
import { Outlet, useLoaderData, useRouteError } from 'react-router';
import { AppProvider } from '@shopify/shopify-app-react-router/react';
import { boundary } from '@shopify/shopify-app-react-router/server';
import { authenticate } from '../shopify.server';
export const loader = async ({ request }) => {
await authenticate.admin(request);
return { apiKey: process.env.SHOPIFY_API_KEY || '' };
};
export default function App() {
const { apiKey } = useLoaderData();
return (
<AppProvider embedded apiKey={apiKey}>
<s-app-nav>
<s-link href="/app/feedback">Feedback</s-link>
<s-link href="/app/roadmap">Roadmap</s-link>
<s-link href="/app/changelog">Changelog</s-link>
</s-app-nav>
<Outlet />
</AppProvider>
);
}
export function ErrorBoundary() {
return boundary.error(useRouteError());
}Feedback page
// app/routes/app.feedback.tsx
import type { LoaderFunctionArgs } from 'react-router';
import { useLoaderData } from 'react-router';
import { KleroFeedbackPage, signKleroJwtFromShopifySession } from '@kleroai/shopify';
import { authenticate } from '../shopify.server';
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { session } = await authenticate.admin(request);
return {
slug: process.env.KLERO_SLUG || '',
baseUrl: process.env.KLERO_BASE_URL || undefined,
customerToken: await signKleroJwtFromShopifySession(
process.env.KLERO_JWT_SECRET || '',
session
),
};
};
export default function FeedbackPage() {
const { slug, baseUrl, customerToken } = useLoaderData<typeof loader>();
return <KleroFeedbackPage slug={slug} baseUrl={baseUrl} customerToken={customerToken ?? undefined} />;
}Roadmap page
// app/routes/app.roadmap.tsx
import type { LoaderFunctionArgs } from 'react-router';
import { useLoaderData } from 'react-router';
import { KleroRoadmapPage, signKleroJwtFromShopifySession } from '@kleroai/shopify';
import { authenticate } from '../shopify.server';
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { session } = await authenticate.admin(request);
return {
slug: process.env.KLERO_SLUG || '',
baseUrl: process.env.KLERO_BASE_URL || undefined,
customerToken: await signKleroJwtFromShopifySession(
process.env.KLERO_JWT_SECRET || '',
session
),
};
};
export default function RoadmapPage() {
const { slug, baseUrl, customerToken } = useLoaderData<typeof loader>();
return <KleroRoadmapPage slug={slug} baseUrl={baseUrl} customerToken={customerToken ?? undefined} />;
}Changelog page
// app/routes/app.changelog.tsx
import type { LoaderFunctionArgs } from 'react-router';
import { useLoaderData } from 'react-router';
import { KleroChangelogPage, signKleroJwtFromShopifySession } from '@kleroai/shopify';
import { authenticate } from '../shopify.server';
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { session } = await authenticate.admin(request);
return {
slug: process.env.KLERO_SLUG || '',
baseUrl: process.env.KLERO_BASE_URL || undefined,
customerToken: await signKleroJwtFromShopifySession(
process.env.KLERO_JWT_SECRET || '',
session
),
};
};
export default function ChangelogPage() {
const { slug, baseUrl, customerToken } = useLoaderData<typeof loader>();
return <KleroChangelogPage slug={slug} baseUrl={baseUrl} customerToken={customerToken ?? undefined} />;
}Authentication
signKleroJwtFromShopifySession maps the logged-in Shopify merchant/staff to a Klero customer JWT. Votes and comments are attributed to them rather than an anonymous ID.
It returns null when the session has no associated user (e.g. an offline token). The widgets accept null / undefined and fall back to anonymous mode automatically.
Online tokens required
The default Shopify CLI app template uses offline tokens, which have no associated user. You must enable online tokens or signKleroJwtFromShopifySession will always return null and users will appear anonymous.
Add useOnlineTokens: true to your shopifyApp() config:
// app/shopify.server.ts
const shopify = shopifyApp({
// ...
useOnlineTokens: true,
});After adding this, reinstall the app or go through the OAuth flow again so Shopify creates online sessions.
For custom JWT payloads, use signKleroJwt directly:
import { signKleroJwt } from '@kleroai/shopify';
const token = await signKleroJwt(process.env.KLERO_JWT_SECRET, {
id: 'user-123',
email: '[email protected]',
name: 'Jane Smith',
});See the Authentication Guide for full details on JWT payload fields.
API reference
KleroFeedbackPage
| Prop | Type | Required | Description |
|---|---|---|---|
slug | string | Yes | Your Klero project slug |
baseUrl | string | No | Override API base URL (for custom domains) |
customerToken | string | No | JWT for the authenticated user |
KleroRoadmapPage
| Prop | Type | Required | Description |
|---|---|---|---|
slug | string | Yes | Your Klero project slug |
baseUrl | string | No | Override API base URL |
customerToken | string | No | JWT for the authenticated user |
roadmapSlug | string | No | Show a specific roadmap by slug |
KleroChangelogPage
| Prop | Type | Required | Description |
|---|---|---|---|
slug | string | Yes | Your Klero project slug |
baseUrl | string | No | Override API base URL |
customerToken | string | No | JWT for the authenticated user |
Low-level widget components
If you need to render inside your own <s-page> and manage state yourself, use KleroFeedbackWidget, KleroRoadmapWidget, and KleroChangelogWidget. These accept a pageEl prop (the <s-page> DOM element) for header button injection, plus onDetailChange and onViewChange callbacks.
Imperative API
For non-React usage, mount widgets imperatively:
import { KleroFeedback, KleroRoadmap, KleroChangelog } from '@kleroai/shopify';
const feedback = await KleroFeedback.init({
slug: 'my-project',
el: '#feedback-container',
customerToken: token,
});
// Clean up
feedback.destroy();init() is async — it loads the Klero UI bundle from the server on first call, then resolves. The same pattern applies to KleroRoadmap.init() and KleroChangelog.init().
signKleroJwtFromShopifySession(secret, session)
| Parameter | Type | Description |
|---|---|---|
secret | string | Your Klero JWT secret |
session | ShopifySession | null | Shopify admin session from authenticate.admin() |
Returns Promise<string \| null>.