---
title: "Building a UI Plugin"
source: "https://docs.vertesiahq.com/apps/ui"
markdown: "https://docs.vertesiahq.com/llms/apps/ui.md"
---

# Building a UI Plugin

UI plugins extend Vertesia Studio with custom React pages. Each plugin is a unified project containing a **React UI** (frontend) and a **Hono tool server** (backend for custom tools, skills, and interactions), built and deployed as a single unit.

## Prerequisites

- Node.js 22+ and npm or pnpm
- [Vertesia CLI](https://www.npmjs.com/package/@vertesia/cli) installed and authenticated (`vertesia auth login`)

## 1. Scaffold Your Plugin

```bash
npm init @vertesia/plugin@latest
```

You will be prompted for the plugin name (kebab-case), version, description, and isolation strategy (`shadow` recommended). The generated project includes everything needed: React UI, Hono tool server, Vite + Rollup build configs, Vercel deployment config, and example tools/skills.

## 2. Develop Locally

```bash
cd my-plugin
npm install
npm run dev
```

The generated `dev` script runs Vite in app mode (`vite dev --mode app`), so local development reads `.env.app` and `.env.app.local`.

Open `https://localhost:5173`. You'll see:

- **`/app/*`** -- your plugin UI with hot module replacement
- **`/*`** -- the tool server admin UI (manage tools, skills, interactions)
- **`/api`** -- the tool server API endpoint

HTTPS is required for authentication. The dev server uses a self-signed certificate.

The generated project includes an `.env.app` file:

```bash
VITE_APP_NAME=my-plugin
```

This value must match the `name` field in your Vertesia app manifest. It is public Vite build-time configuration, so it is safe to commit. Use `.env.app.local` for local overrides. Generic Vite development env files such as `.env.local` are not required by the generated template.

## 3. Register Your App in Vertesia

Create a `manifest.json`:

```json
{
  "name": "my-plugin",
  "title": "My Plugin",
  "description": "What this plugin does",
  "publisher": "your-org",
  "visibility": "private",
  "status": "beta"
}
```

Register and install in one step:

```bash
vertesia apps create --install -f manifest.json
```

This creates the app manifest, installs it in your current project, and grants you access.

Verify that `.env.app` uses the same app name:

```bash
VITE_APP_NAME=my-plugin
```

Restart `npm run dev` and navigate to `/app/` to see your plugin running with full Vertesia authentication.

## 4. Deploy to Vercel

[Vercel](https://vercel.com) is the easiest way to deploy your plugin — its generous free tier is more than enough for development and small-scale production.

```bash
npm i -g vercel
vercel --prod
```

The template includes a `vercel.json` and `api/index.js` adapter that handles routing: the standalone app is served at `/app`, `/` redirects to `/app`, and API requests go through the serverless function.

Vercel builds with `vite build --mode app`, so it reads `.env.app` and `.env.app.local` if present. Do not rely on `.env.local` for deployed builds. If you override `VITE_APP_NAME` in Vercel project settings, keep it equal to the manifest `name`.

After deploying, update your app manifest with the production endpoint:

```bash
vertesia apps update my-plugin --manifest '{
  "endpoint": "https://my-plugin.vercel.app/api"
}'
```

The `endpoint` URL tells Vertesia where to find your plugin's tools, skills, interactions, and UI configuration. It replaces the older `ui.src` and `tool_collections` fields.

**Important**: Disable deployment protection in Vercel project settings for the plugin to be publicly accessible.

### Isolation Strategies

The manifest `ui.isolation` field controls how the plugin CSS interacts with the host app:

- **`shadow`** (default, recommended) -- Shadow DOM fully isolates plugin styles
- **`css`** -- lighter weight, but plugin styles may conflict with the host. Required if using Radix UI portals (modals with inputs)

## Using Vertesia UI Components

  For a complete catalog of available components with live examples, props
  tables, and usage snippets, see the [Components reference](/components/buttons).

The `@vertesia/ui` package provides ready-to-use components. Import from subpaths:

```tsx
// Core components and hooks
import { Button, Card, Input, Spinner, VModal, VTabs, useFetch, useToast } from '@vertesia/ui/core';

// Router
import { useNavigate, useParams, NavLink, NestedRouterProvider } from '@vertesia/ui/router';

// Session and auth
import { useUserSession } from '@vertesia/ui/session';

// Layout
import { FullHeightLayout } from '@vertesia/ui/layout';
```

### Fetching Data

Use the `useFetch` hook with the Vertesia client:

```tsx
import { useFetch, Spinner } from '@vertesia/ui/core';
import { useUserSession } from '@vertesia/ui/session';

function MyPage() {
    const { client } = useUserSession();

    const { data, error } = useFetch(
        () => client.store.collections.list(),
        []
    );

    if (error) return <div>Failed to load</div>;
    if (!data) return <Spinner />;

    return <div>{data.map(item => ...)}</div>;
}
```

### Using the Vertesia Client

```tsx
const { client } = useUserSession();

// Collections and objects
const collections = await client.store.collections.list();
await client.store.objects.create(
    { content: file, name: file.name },
    { collection_id: collectionId }
);

// Launch an agent
await client.runs.create({
    interaction: 'app:my_interaction',
    data: { /* payload */ },
    tags: ['my-tag'],
});
```

### Styling

  For a complete catalog of available styles with live examples, see the [Styling reference](/components/semantic).

Use Tailwind CSS with Vertesia's semantic color classes:

```tsx
<div className="text-success bg-success border-success" />
<div className="text-destructive bg-destructive" />
<div className="text-muted" />
```

For these classes to be generated by Tailwind, your plugin must pull the Vertesia design tokens into its main Tailwind entry CSS so the `@theme` directives are visible at compile time:

```css {{ title: 'src/styles/tailwind.css' }}
@import 'tailwindcss';

/*
 * Pull in the Vertesia design tokens so the Tailwind compilation knows
 * about the semantic palette (--color-primary, --color-foreground, etc.).
 * Without this, classes like `bg-primary` and `text-muted` never get
 * generated as utilities.
 *
 * Skip @vertesia/ui/css/base.css if you have your own base layer — it
 * body-applies selection styles that may conflict with your palette.
 */
@import '@vertesia/ui/css/color.css';
@import '@vertesia/ui/css/theme.css';
@import '@vertesia/ui/css/utilities.css';
@import '@vertesia/ui/css/custom-tooltips.css';
```

If the import lives inside a component file (e.g. `import '@vertesia/ui/css/index.css'` from a TSX module) instead of the Tailwind entry CSS, the CSS variables are bundled but Tailwind never sees the `@theme` block at compile time, so the semantic utility classes are silently missing.

## Next Steps

The generated project includes a comprehensive README with detailed guides for:

- **Creating resources** -- tools, skills, interactions, content types, templates
- **Tool server configuration** -- registering collections, settings schema, org restrictions
- **Build system** -- dual Rollup/Vite architecture, import hooks
- **Debugging with the platform** -- Cloudflare tunnel for local testing with real agents
- **Theme customization** -- overriding CSS custom properties in `index.css`