Plugin development
Creating a project
Section titled “Creating a project”The fastest way to start a new plugin project is the @racona/cli CLI:
bunx @racona/cliThe wizard walks you through the setup:
- App ID — kebab-case identifier (e.g.
my-app) - Display Name — human-readable name shown in Racona
- Description — short description
- Author — your name and email
- Features — pick the features you need
- Install dependencies? — automatically runs
bun install
Available features
Section titled “Available features”| Feature | What it adds |
|---|---|
sidebar | Sidebar navigation (menu.json, AppLayout mode, multiple page components) |
database | SQL migrations, sdk.data.query() support, local dev database via Docker |
remote_functions | server/functions.ts, sdk.remote.call(), local dev server |
notifications | sdk.notifications.send() support |
i18n | locales/hu.json + locales/en.json, sdk.i18n.t() support |
datatable | DataTable component with insert form, row actions (duplicate/delete), full i18n |
Generated project structure
Section titled “Generated project structure”The structure depends on the selected features. Full example (all features enabled):
my-app/├── manifest.json # App metadata and permissions├── package.json├── vite.config.ts├── tsconfig.json├── menu.json # (if sidebar)├── build-all.js # (if sidebar)├── dev-server.ts # (if remote_functions)├── docker-compose.dev.yml # (if database)├── .env.example # (if database)├── src/│ ├── App.svelte│ ├── main.ts│ ├── plugin.ts│ └── components/ # (if sidebar)│ ├── Overview.svelte│ ├── Settings.svelte│ ├── Datatable.svelte # (if datatable)│ ├── Notifications.svelte # (if notifications)│ └── Remote.svelte # (if remote_functions)├── server/ # (if remote_functions)│ └── functions.ts├── migrations/ # (if database)│ ├── 001_init.sql│ └── dev/│ └── 000_auth_seed.sql├── locales/ # (if i18n)│ ├── hu.json│ └── en.json└── assets/ └── icon.svgDevelopment workflow
Section titled “Development workflow”The available scripts depend on the selected features.
Base scripts (every project)
Section titled “Base scripts (every project)”bun dev # Vite dev server (standalone, Mock SDK) — http://localhost:5174bun run build # Build IIFE bundle (dist/index.iife.js)bun run build:watch # Build in watch modebun run package # Create .elyospkg packageWhen remote_functions is enabled
Section titled “When remote_functions is enabled”bun run dev:server # Start dev server — http://localhost:5175dev:server starts a Bun HTTP server that:
- Serves static files from
dist/and the project root (with CORS headers) - Exposes a
POST /api/remote/:functionNameendpoint for calling functions fromserver/functions.ts
When database is also enabled
Section titled “When database is also enabled”bun db:up # Start Docker Postgres containerbun db:down # Stop Docker Postgres containerbun run dev:full # dev:server + dev in parallel (single terminal)dev:full starts both the Vite dev server (5174) and the dev server (5175) at the same time, so you don’t need two terminals.
First run with database
Section titled “First run with database”cp .env.example .env # Set environment variablesbun db:up # Start Postgres container (Docker required)bun run dev:full # Dev server + Vite together.env.example contains the default connection URL for the database started by Docker Compose:
DATABASE_URL=postgresql://postgres:postgres@localhost:5433/{plugin_id}_devPORT=5175DEV_USER_ID=dev-userStandalone development (Mock SDK)
Section titled “Standalone development (Mock SDK)”Your app can be developed without a running Racona instance. The @racona/sdk/dev package provides a Mock SDK that simulates all SDK services:
| SDK service | Mock behavior |
|---|---|
ui.toast() | Writes to console.log |
ui.dialog() | Uses window.confirm / window.prompt |
data.set/get/delete() | Uses localStorage (under devapp:{appId}: key prefix) |
data.query() | Returns an empty array |
remote.call() | Configurable mock handler |
i18n.t() | Reads from the provided translations map |
notifications.send() | Writes to console.log |
Starting the dev server
Section titled “Starting the dev server”bun devThe Vite dev server starts at http://localhost:5174. Hot reload automatically refreshes the browser on every save.
If remote_functions is also enabled, you need to run bun run dev:server in parallel alongside bun dev (or use bun run dev:full if database is also enabled).
Mock SDK initialization
Section titled “Mock SDK initialization”The Mock SDK is initialized automatically in src/main.ts:
import { MockWebOSSDK } from '@racona/sdk/dev';import App from './App.svelte';import { mount } from 'svelte';
// Only runs when NOT inside Raconaif (typeof window !== 'undefined' && !window.webOS) { MockWebOSSDK.initialize({ i18n: { locale: 'en', translations: { en: { title: 'My App', welcome: 'Welcome!' }, hu: { title: 'Alkalmazás', welcome: 'Üdvözöljük!' } } }, context: { pluginId: 'my-app', user: { id: 'dev-user', name: 'Developer', email: 'dev@localhost', roles: ['admin'], groups: [] }, permissions: ['database', 'notifications', 'remote_functions'] } });}
const target = document.getElementById('app');if (target) mount(App, { target });All initialize() configuration options:
| Option | Type | Description |
|---|---|---|
i18n.locale | string | Default locale (e.g. 'en') |
i18n.translations | Record<string, Record<string, string>> | Translation keys per locale |
context.pluginId | string | Simulated app ID |
context.user | UserInfo | Simulated logged-in user |
context.permissions | string[] | Simulated permissions |
data.initialData | Record<string, unknown> | Pre-populated localStorage data |
remote.handlers | Record<string, Function> | Mock server function handlers |
assets.baseUrl | string | Asset URL prefix |
When Racona loads the app in production, window.webOS already exists, so the if (!window.webOS) guard prevents the Mock SDK from running.
Mocking remote calls
Section titled “Mocking remote calls”To test server functions in standalone mode:
MockWebOSSDK.initialize({ remote: { handlers: { getServerTime: async () => ({ iso: new Date().toISOString(), locale: new Date().toLocaleString('en-US') }), calculate: async ({ a, b, operation }) => { if (operation === 'add') return { result: a + b }; throw new Error('Unsupported operation'); } } }});Dev server port configuration
Section titled “Dev server port configuration”dev:server defaults to port 5175 (the Vite dev server uses 5174). If you’re developing multiple apps at the same time, the port can be overridden via the PORT environment variable in .env or directly:
PORT=5176 bun run dev:serverEnter the corresponding URL in the Racona Dev Apps loader: http://localhost:5176.
Testing inside a running Racona
Section titled “Testing inside a running Racona”Standalone dev mode (Mock SDK) only tests the UI. To test real SDK calls, database access, or server functions, you need to load the app into a running Racona instance.
The idea: build the app, start a static HTTP server (dev:server), then load it into Racona by URL. There is no automatic hot reload — if you change the code, you need to rebuild and reopen the app window.
Step 1 — Start Racona core
Section titled “Step 1 — Start Racona core”In the elyos-core monorepo root:
# Enable dev app loading in .env.local:# DEV_MODE=true
bun app:devRacona is available at http://localhost:5173 by default. Log in with an admin account.
Step 2 — Build the app
Section titled “Step 2 — Build the app”In the app project directory:
bun run buildThis creates dist/index.iife.js — the file Racona loads.
Step 3 — Start the plugin dev server
Section titled “Step 3 — Start the plugin dev server”bun run dev:serverThis starts the dev-server.ts Bun HTTP server at http://localhost:5175. It serves files from dist/ and the project root with CORS headers.
If database is also enabled, the server automatically runs migrations on startup, and server/functions.ts functions are accessible via POST /api/remote/:functionName.
Step 4 — Load the app into Racona
Section titled “Step 4 — Load the app into Racona”- Open Racona in the browser
- Start menu → App Manager
- Click “Dev Apps” in the left sidebar
- A URL input field appears with
http://localhost:5175as the default value - Click “Load”
Racona fetches manifest.json from the dev server, then loads the IIFE bundle and registers the app as a Web Component.
Reloading after changes
Section titled “Reloading after changes”# 1. Rebuildbun run build
# 2. In Racona: close the app window, then reopen it# (no need to click "Load" again — the app is already in the list)Full dev workflow summary
Section titled “Full dev workflow summary”Basic (without remote_functions):
# Terminal 1 — Racona corecd elyos-core && bun app:dev
# Terminal 2 — App build + servercd my-appbun run build # Build IIFE bundlebun run dev:server # Start static server (http://localhost:5175)
# In Racona: App Manager → Dev Apps → Load → http://localhost:5175With database (database + remote_functions):
# Terminal 1 — Racona corecd elyos-core && bun app:dev
# Terminal 2 — App (first time)cd my-appcp .env.example .env # Set DATABASE_URL and PORTbun db:up # Start Postgres container
# Terminal 2 — App (every time)bun run build # Build IIFE bundlebun run dev:server # Dev server + migrations + remote endpoint (http://localhost:5175)
# In Racona: App Manager → Dev Apps → Load → http://localhost:5175Installing a plugin (.elyospkg)
Section titled “Installing a plugin (.elyospkg)”Once your app is ready, package it and install it into Racona.
Creating the package
Section titled “Creating the package”bun run build # Build IIFE bundlebun run package # Create .elyospkg fileThis creates a {id}-{version}.elyospkg file (e.g. my-app-1.0.0.elyospkg). The package is a ZIP archive containing:
manifest.jsondist/— build output (IIFE bundle)locales/— translations (if present)assets/— static files (if present)menu.json— sidebar configuration (if present)server/— server-side functions (if present)migrations/— database migrations (if present, without dev seed files)
Uploading to Racona
Section titled “Uploading to Racona”- Start menu → App Manager → Plugin Upload
- Drag and drop the
.elyospkgfile, or click the browse button - Racona validates the package and shows a preview
- Click Install
During installation, Racona:
- Extracts files to the plugin storage directory
- Registers the app in the app registry
- Imports translations (if
locales/is present) - Creates the plugin database schema (if
databasepermission is declared) - Registers email templates (if
notificationspermission is declared)
Manifest file
Section titled “Manifest file”manifest.json contains the app metadata. Required and optional fields:
{ "id": "my-app", "name": { "hu": "Alkalmazásom", "en": "My App" }, "version": "1.0.0", "description": { "hu": "Rövid leírás", "en": "Short description" }, "author": "Your Name <email@example.com>", "entry": "dist/index.iife.js", "icon": "assets/icon.svg", "iconStyle": "cover", "category": "utilities", "permissions": ["database", "notifications", "remote_functions"], "multiInstance": false, "defaultSize": { "width": 800, "height": 600 }, "minSize": { "width": 400, "height": 300 }, "maxSize": { "width": 1920, "height": 1080 }, "keywords": ["example", "demo"], "isPublic": false, "sortOrder": 100, "dependencies": { "svelte": "^5.0.0", "@lucide/svelte": "^1.0.0" }, "minWebOSVersion": "2.0.0", "locales": ["hu", "en"]}Permissions
Section titled “Permissions”| Permission | Description | SDK functions |
|---|---|---|
database | Database access | data.set(), data.get(), data.query() |
notifications | Send notifications | notifications.send() |
remote_functions | Server-side functions | remote.call() |
file_access | File access | (planned) |
user_data | User data | (planned) |
ID format rules
Section titled “ID format rules”- Lowercase letters, numbers and hyphens only (
kebab-case) - Minimum 3, maximum 50 characters
- Regex:
^[a-z0-9-]+$
"id": "my-app" // ✅ Valid"id": "MyApp" // ❌ Invalid"id": "my_app" // ❌ InvalidWebOS SDK API
Section titled “WebOS SDK API”The SDK is available via the window.webOS global object:
const sdk = window.webOS!;UI Service
Section titled “UI Service”// Toast notificationsdk.ui.toast('Message', 'success');// type: 'info' | 'success' | 'warning' | 'error'
// Dialogconst result = await sdk.ui.dialog({ title: 'Title', message: 'Message', type: 'confirm' // 'info' | 'confirm' | 'prompt'});Remote Service
Section titled “Remote Service”// Call a server functionconst result = await sdk.remote.call('functionName', { param: 'value' });
// With a generic return typeconst result = await sdk.remote.call<MyResult>('functionName', params);Data Service
Section titled “Data Service”// Key-value storageawait sdk.data.set('key', { value: 123 });const value = await sdk.data.get('key');await sdk.data.delete('key');
// SQL query (plugin's own schema only!)const rows = await sdk.data.query('SELECT * FROM my_table WHERE id = $1', [123]);
// Transactionawait sdk.data.transaction(async (tx) => { await tx.query('INSERT INTO ...'); await tx.query('UPDATE ...'); await tx.commit();});I18n Service
Section titled “I18n Service”// Translationconst text = sdk.i18n.t('key');
// With parametersconst text = sdk.i18n.t('welcome', { name: 'John' });
// Current localeconst locale = sdk.i18n.locale; // 'hu' | 'en'
// Switch localeawait sdk.i18n.setLocale('en');Notification Service
Section titled “Notification Service”await sdk.notifications.send({ userId: 'user-123', title: 'Title', message: 'Message', type: 'info' // 'info' | 'success' | 'warning' | 'error'});Context Service
Section titled “Context Service”const pluginId = sdk.context.pluginId;const user = sdk.context.user;const permissions = sdk.context.permissions;
// Window controlssdk.context.window.close();sdk.context.window.setTitle('New title');Asset Service
Section titled “Asset Service”const iconUrl = sdk.assets.getUrl('icon.svg');const imageUrl = sdk.assets.getUrl('images/logo.png');TypeScript and autocomplete
Section titled “TypeScript and autocomplete”@racona/sdk ships with full TypeScript type definitions. The window.webOS type is available automatically:
// Automatic type — no import neededconst sdk = window.webOS!;
sdk.ui.toast('Hello!', 'success'); // ✅ autocompletesdk.data.set('key', { value: 123 }); // ✅ type checkingsdk.remote.call<MyResult>('fn', params); // ✅ generic return typeExplicit type import when needed:
import type { WebOSSDKInterface, UserInfo } from '@racona/sdk/types';
const user: UserInfo = sdk.context.user;Svelte 5 runes in plugins
Section titled “Svelte 5 runes in plugins”Plugins use Svelte 5 runes-based reactivity. The runes: true compiler option is enabled in vite.config.ts:
<script lang="ts"> const sdk = window.webOS!;
let count = $state(0); let doubled = $derived(count * 2);
$effect(() => { sdk.ui.toast(`Count: ${count}`, 'info'); });</script>
<button onclick={() => count++}> {count} (doubled: {doubled})</button>Server-side functions
Section titled “Server-side functions”When remote_functions is enabled, server-side functions are defined in server/functions.ts:
import type { PluginFunctionContext } from '@racona/sdk/types';
export async function getItems( params: { page: number; pageSize: number }, context: PluginFunctionContext) { const { db, pluginId } = context; const schema = `app__${pluginId.replace(/-/g, '_')}`;
const rows = await db.query( `SELECT * FROM ${schema}.items LIMIT $1 OFFSET $2`, [params.pageSize, (params.page - 1) * params.pageSize] );
return { success: true, data: rows };}Calling from the client:
const result = await sdk.remote.call('getItems', { page: 1, pageSize: 20 });Database migrations
Section titled “Database migrations”When database is enabled, SQL files in the migrations/ directory define the plugin’s own database schema. Files run in alphabetical order (e.g. 001_init.sql, 002_add_column.sql).
-- migrations/001_init.sqlCREATE TABLE items ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, value JSONB, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW());Files in migrations/dev/ are for development only (e.g. seed data) and are excluded from the .elyospkg package.
Styling
Section titled “Styling”CSS injection
Section titled “CSS injection”Plugin CSS is automatically bundled into the JS output during the IIFE build via vite-plugin-css-injected-by-js. This plugin is already included in the vite.config.ts generated by @racona/cli — no manual setup needed.
Specificity conflicts
Section titled “Specificity conflicts”The core app’s Tailwind styles (base layer resets) can override plugin styles. Svelte scoped CSS generates button.svelte-xxxx selectors, but Tailwind’s button { ... } reset loads with higher specificity.
The fix: always scope your styles inside a container class:
<!-- ❌ Bad — core styles will override --><style> button { border: 1px solid #ccc; }</style>
<!-- ✅ Good — scoped inside a container class --><style> .my-plugin button { border: 1px solid #ccc; }</style>If core styles override an element, all: revert restores the browser’s native style:
<style> .my-plugin button { all: revert; cursor: pointer; border: 1px solid #ccc; border-radius: 0.25rem; padding: 0.5rem 1rem; }</style>Summary
Section titled “Summary”| Rule | Why |
|---|---|
Scope inside a container class (.my-plugin button) | Core Tailwind styles override bare tag selectors |
Use all: revert when needed | Restores the browser’s native style |
| Give the root container a unique class name | Avoids conflicts with other plugins’ styles |
Security rules
Section titled “Security rules”Forbidden
Section titled “Forbidden”eval()andFunction()constructorinnerHTMLanddocument.write()- fetch/XHR to external domains
- Dynamic import from external URLs
- Accessing other plugins’ schemas
- Accessing system schemas (
platform,auth,public)
Allowed dependencies
Section titled “Allowed dependencies”Only whitelisted packages may appear in the manifest dependencies field:
svelte(^5.x.x)@lucide/svelte/lucide-sveltephosphor-svelte@elyos/*and@elyos-dev/*(any version) — deprecated, use@racona/*instead@racona/*(any version)
Common errors
Section titled “Common errors”| Error | Solution |
|---|---|
"Invalid plugin ID format" | Use kebab-case: my-plugin |
"Permission denied" | Add the required permission to manifest.json |
"Module not found" | Run bun run build first |
"Plugin already exists" | A plugin with that ID is already installed — uninstall it first |
"Plugin is inactive" | The plugin is in inactive state — activate it in the App Manager |
| Dev app not showing up | Check that DEV_MODE=true is set in the Racona .env.local |