Building UI Application Plugins

This guide walks you through developing, building, and deploying a UI application plugin for Vertesia.

UI applications are React-based plugins that extend the Vertesia Studio interface with custom user experiences focused on specific use cases and business processes.

Vertesia provides a collection of reusable React components (via @vertesia/ui) to facilitate UI development, along with built-in authentication handling, allowing you to focus on your application's business logic rather than infrastructure and tooling concerns.

LLM Reference

For AI assistants, comprehensive component documentation is available at:

Prerequisites

Creating Your Plugin

Initialize the Project

Use the @vertesia/create-plugin package to scaffold your plugin:

npm init @vertesia/plugin

You will be prompted for:

  • Package manager: npm or pnpm
  • Plugin name: Must match the name from your app manifest (kebab-case)
  • Plugin version: Semantic version (e.g., 1.0.0)
  • Description: Optional description
  • Isolation strategy: shadow (recommended) or css

Project Structure

my-plugin/
├── package.json
├── vite.config.ts
├── tsconfig.json
├── vercel.json          # Deployment config
├── manifest.json        # App manifest for registration
├── src/
│   ├── plugin.tsx       # Plugin entry point (minimal)
│   ├── app.tsx          # Main app component
│   ├── routes.tsx       # Route definitions
│   ├── index.css        # Tailwind CSS entry
│   ├── main.tsx         # Standalone app entry (for dev)
│   ├── assets.ts        # Asset URL resolution
│   ├── env.ts           # Dev environment config
│   ├── context/         # React contexts
│   ├── pages/           # Page components
│   ├── components/      # Shared components
│   └── hooks/           # Custom hooks
└── dist/
    └── lib/
        ├── plugin.js    # Built plugin bundle
        └── plugin.css   # Built CSS

Key Files

plugin.tsx (Entry Point)

Keep this minimal - just export the component with PortalContainerProvider:

import { PortalContainerProvider } from '@vertesia/ui/core';
import { App } from './app';

export default function MyPlugin({ slot }: { slot: string }) {
    if (slot === 'page') {
        return (
            
                
            
        );
    }
    console.warn('No component found for slot', slot);
    return null;
}

app.tsx (Main App)

Import CSS here, set up providers and router:

import './index.css';
import { NestedRouterProvider } from '@vertesia/ui/router';
import { MyContextProvider } from './context/MyContext';
import { routes } from './routes';

export function App() {
    return (
        
            
        
    );
}

routes.tsx

Define routes as an array:

import { HomePage } from './pages/HomePage';
import { DetailPage } from './pages/DetailPage';

export const routes = [
    { path: '/', Component: HomePage },
    { path: '/items/:id', Component: DetailPage },
    { path: '*', Component: () => 
Not found
}, ];

package.json

{
  "name": "@vertesia/plugin-my-plugin",
  "version": "0.1.0",
  "type": "module",
  "plugin": {
    "title": "My Plugin",
    "publisher": "vertesia",
    "external": false,
    "status": "beta"
  },
  "scripts": {
    "dev": "vite dev",
    "build": "vite build --mode lib",
    "build:app": "vite build --mode app"
  },
  "peerDependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "dependencies": {
    "@vertesia/common": "^0.80.0",
    "@vertesia/ui": "^0.80.0",
    "dayjs": "^1.11.10",
    "lucide-react": "^0.511.0"
  },
  "devDependencies": {
    "@tailwindcss/vite": "^4.1.18",
    "@vitejs/plugin-basic-ssl": "^2.1.0",
    "@vitejs/plugin-react": "^5.1.2",
    "tailwindcss": "^4.1.18",
    "typescript": "^5.9.3",
    "vite": "^7.3.0"
  }
}

vite.config.ts

import tailwindcss from '@tailwindcss/vite';
import basicSsl from '@vitejs/plugin-basic-ssl';
import react from '@vitejs/plugin-react';
import { defineConfig, type ConfigEnv, type UserConfig } from 'vite';

// Dependencies provided by host app - don't bundle these
const EXTERNALS = [
    'react',
    'react-dom',
    'react/jsx-runtime',
    'react-dom/client',
    '@vertesia/ui',
    /^@vertesia\/ui\/.*/,
    '@vertesia/common',
    '@vertesia/client',
    'lucide-react',
    'dayjs',
    /^dayjs\/.*/,
];

export default defineConfig((env) => {
    if (env.mode === 'lib') {
        return defineLibConfig(env);
    }
    return defineAppConfig();
});

function defineLibConfig({ command }: ConfigEnv): UserConfig {
    if (command !== 'build') {
        throw new Error("Use 'vite build --mode lib' for library builds");
    }
    return {
        plugins: [tailwindcss(), react()],
        build: {
            outDir: 'dist/lib',
            cssCodeSplit: false,
            lib: {
                entry: './src/plugin.tsx',
                formats: ['es'],
                fileName: 'plugin',
            },
            minify: true,
            sourcemap: true,
            rollupOptions: {
                external: EXTERNALS,
                output: { assetFileNames: 'plugin.[ext]' },
            }
        }
    };
}

function defineAppConfig(): UserConfig {
    return {
        plugins: [tailwindcss(), react(), basicSsl()],
        server: {
            proxy: {
                '/__/auth': {
                    target: 'https://dengenlabs.firebaseapp.com',
                    changeOrigin: true,
                }
            }
        },
        resolve: { dedupe: ['react', 'react-dom'] }
    };
}

Using Vertesia UI Components

Import from @vertesia/ui/*:

Important API Note: Input passes value directly to onChange, while Textarea uses standard React events:

// Input - simplified API (value directly)

 setName(value)} />

// Textarea - standard React (event object)