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 installed and authenticated (
vertesia auth login)
1. Scaffold Your Plugin
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
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:
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:
{
"name": "my-plugin",
"title": "My Plugin",
"description": "What this plugin does",
"publisher": "your-org",
"visibility": "private",
"status": "beta"
}
Register and install in one step:
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:
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 is the easiest way to deploy your plugin — its generous free tier is more than enough for development and small-scale production.
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:
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 stylescss-- 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.
The @vertesia/ui package provides ready-to-use components. Import from subpaths:
// 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:
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
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.
Use Tailwind CSS with Vertesia's semantic color classes:
<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:
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
