Skip to content

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 AppProvider automatically)
  • React >= 18

Installation ​

bash
npm install @kleroai/shopify

Environment variables ​

env
KLERO_SLUG=your-project-slug
KLERO_JWT_SECRET=your-jwt-secret   # from Klero dashboard → Settings → JWT Secret
KLERO_BASE_URL=                    # optional: custom domain

Your 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:

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 ​

tsx
// 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 ​

tsx
// 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 ​

tsx
// 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:

ts
// 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:

ts
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 ​

PropTypeRequiredDescription
slugstringYesYour Klero project slug
baseUrlstringNoOverride API base URL (for custom domains)
customerTokenstringNoJWT for the authenticated user

KleroRoadmapPage ​

PropTypeRequiredDescription
slugstringYesYour Klero project slug
baseUrlstringNoOverride API base URL
customerTokenstringNoJWT for the authenticated user
roadmapSlugstringNoShow a specific roadmap by slug

KleroChangelogPage ​

PropTypeRequiredDescription
slugstringYesYour Klero project slug
baseUrlstringNoOverride API base URL
customerTokenstringNoJWT 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:

ts
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) ​

ParameterTypeDescription
secretstringYour Klero JWT secret
sessionShopifySession | nullShopify admin session from authenticate.admin()

Returns Promise<string \| null>.

Customer Feedback Management