Quickstart

Create a Studio project, install the SDK and send the first report from an Expo, React Native or web app.

Create a project

Sign in to PanelUI Studio, create a workspace and a project for your app. Project → Settings shows its publishable key (pk_…).

Install the SDK

npx expo install panelui-studio

Each install keeps a token that proves it is itself, so the SDK stores it in secure storage when it can. In an Expo app, add that:

npx expo install expo-secure-store

Without it the SDK falls back to AsyncStorage, then to memory — and memory means a new install is registered on every launch.

The same code runs on Expo's web target. There the SDK uses its browser build, which keeps the token in localStorage.

npm install panelui-studio

Web support needs panelui-studio 0.3.0 or later. Next.js, Vite and other web bundlers pick the SDK's browser build on their own. It keeps each install's token in localStorage, so there's nothing else to install. Where the browser blocks storage, the token lives in memory and a new install is registered on every page load.

Create the client

lib/studio.ts
import { createStudio } from 'panelui-studio';

export const studio = createStudio({
  publishableKey: process.env.EXPO_PUBLIC_STUDIO_KEY!,
});

The publishable key is public by design, so EXPO_PUBLIC_ is the right place for it: all it allows is submitting to this one project.

lib/studio.ts
import { createStudio } from 'panelui-studio';

export const studio = createStudio({
  publishableKey: process.env.NEXT_PUBLIC_STUDIO_KEY!,
  device: { appVersion: '1.4.0' },
});

The publishable key is public by design, so the framework's public prefix (NEXT_PUBLIC_ or VITE_) is the right place for it: all it allows is submitting to this one project.

A browser has no app version to read, so pass your own as device.appVersion. Leave it out if your site doesn't have one.

Send a report

await studio.feedback.submit({
  message: 'The workout screen freezes.',
  rating: 2,
  screen: '/workout/start',
});

The first call registers the install, then sends. Platform, OS, device model, app version and locale are filled in for you. Open the inbox and it's there.

The first call registers the install, then sends. The platform (web), the OS and its version, the browser and the locale are filled in for you; the browser shows where the inbox shows a device model. Open the inbox and it's there.

A report open in the Studio inbox: Feedback #5, status New, a Demo tag, and the message with its sender and a two-star rating.
The report in the project's Feedback inbox, seconds after the app sent it.

Any UI can call submit — a form of your own, a settings screen, a shake gesture. If your app uses PanelUI, Usage with PanelUI puts it behind its Feedback dialog.

Any UI can call submit: a form, a dialog, a button in the footer. Feedback has a complete form with a screenshot.

With React context

panelui-studio/react hands one client to the whole tree.

app/_layout.tsx
import { Stack } from 'expo-router';
import { StudioProvider } from 'panelui-studio/react';

import { studio } from '@/lib/studio';

export default function RootLayout() {
  return (
    <StudioProvider client={studio}>
      <Stack />
    </StudioProvider>
  );
}

client={studio} shares the client from lib/studio.ts, so code outside React and the hooks use the same install; publishableKey="pk_…" instead creates one for the tree. Anything below can call useStudio() for the client, or use the hooks in Conversations.

panelui-studio/react is a client module. The root layout is a server component, and a client instance can't be passed from one, so share studio through a small client component:

app/providers.tsx
'use client';

import { StudioProvider } from 'panelui-studio/react';

import { studio } from '@/lib/studio';

export function Providers({ children }: { children: React.ReactNode }) {
  return <StudioProvider client={studio}>{children}</StudioProvider>;
}
app/layout.tsx
import { Providers } from './providers';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

If nothing outside React needs the client, the layout can render <StudioProvider publishableKey={process.env.NEXT_PUBLIC_STUDIO_KEY!}> directly instead. The hooks only run in client components.

Anything below can call useStudio() for the client, or use the hooks in Conversations.

Usage with PanelUI

Studio doesn't need PanelUI. If your app uses it, PanelUI's Feedback dialog is a ready-made place to collect a report:

import { useState } from 'react';
import { Button, Feedback } from 'panelui-native';

import { studio } from '@/lib/studio';

export function FeedbackButton() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <Button variant="outline" onPress={() => setOpen(true)}>
        Send feedback
      </Button>
      <Feedback open={open} onOpenChange={setOpen}>
        <Feedback.Content>
          <Feedback.Panel>
            <Feedback.Title>What should we fix first?</Feedback.Title>
            <Feedback.Close />
            <Feedback.Field placeholder="Tell us what got in your way" />
          </Feedback.Panel>
          <Feedback.Footer>
            <Feedback.Cancel />
            <Feedback.Submit
              onSubmit={async (message) => {
                await studio.feedback.submit({ message });
                setOpen(false);
              }}
            />
          </Feedback.Footer>
        </Feedback.Content>
      </Feedback>
    </>
  );
}

Feedback.Submit doesn't close the dialog by itself, so a failed send can still show its error. Close it once submit resolves. Feedback adds a rating, a screenshot and the error message.

With PanelUI and the React hooks together, put StudioProvider above PanelUIProvider:

app/_layout.tsx
<StudioProvider client={studio}>
  <PanelUIProvider>
    <Stack />
  </PanelUIProvider>
</StudioProvider>

PanelUI's dialogs, sheets and popovers render into a layer that PanelUIProvider owns. A StudioProvider inside it can't be seen from there, so a hook like useFeedbackForm() in a Feedback dialog would throw useStudio must be used inside <StudioProvider>.

On this page