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-studiopnpm expo install panelui-studioyarn expo install panelui-studiobunx expo install panelui-studioEach 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-storeWithout 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-studiopnpm add panelui-studioyarn add panelui-studiobun add panelui-studioWeb 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
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.
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.

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.
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:
'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>;
}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:
<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>.