Skip to main content
Version: 3.0

App UI

App UI lets you ship a custom user interface as part of your Operaide App — served directly by the platform, protected by login, and wired to your Reaktors.

Every App can include a public/ directory. Operaide serves its contents as a static web page at a stable URL, automatically scoped to the App Instance. No separate hosting, no extra authentication setup — the platform handles both.

Use Cases

App UI is the right choice whenever your users need to interact with the App directly, rather than through an external system calling your Reaktors via API:

  • Data dashboards — visualise Reaktor output in charts, tables, or KPI widgets
  • Form interfaces — collect structured user input, validate it, and pass it to a Reaktor for processing
  • Document management — upload files, track processing status, preview results
  • Custom workflows — guide users through multi-step processes that span multiple Reaktor calls

If your use case is pure backend integration (system-to-system API calls), you do not need an App UI. Add one when a human needs a screen to work with.

How It Works

Place your static files in a public/ directory inside your App package:

my-app/
├── src/
│ └── ...
└── public/
├── index.html
├── app.js
└── styles.css

Operaide serves this directory at:

/app-ui/<package-name>@<version>/

The page is protected by the platform's login — unauthenticated users are redirected automatically.

Calling Reaktors from the UI

The platform injects two browser globals into every App UI page:

  • window.getOperaideAuthToken() — returns the current user's authentication token
  • window.OperaideAppInstance.apiBasePath — the base URL for this App Instance's REST API

Use them to call your Reaktors from JavaScript:

async function callReaktor(reaktorName, input = {}) {
const token = window.getOperaideAuthToken();
const basePath = window.OperaideAppInstance?.apiBasePath;
const url = `${basePath}/reaktors/${reaktorName}`;

const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Operaide-Token': token,
},
body: JSON.stringify(input),
});
return response.json();
}

Because the token and base path are injected at runtime, the same build works across all instances without any hardcoded URLs.

Using React

For richer UIs, use React with a bundler such as Vite or esbuild. Build your React app into the public/ directory — the output is standard HTML, CSS, and JavaScript, which Operaide serves like any other static file.

The callReaktor helper above works the same way inside React components:

import { useState } from 'react';

export function SummaryButton({ documentId }: { documentId: string }) {
const [summary, setSummary] = useState<string | null>(null);

async function handleClick() {
const result = await callReaktor('summarise', { documentId });
setSummary(result.summary);
}

return (
<div>
<button onClick={handleClick}>Summarise</button>
{summary && <p>{summary}</p>}
</div>
);
}

Controlling Access with Roles

Operaide can restrict which users are allowed to call a Reaktor — and you can mirror those restrictions in your App UI to give users clear feedback before they try.

Define Roles and Permissions

In your .reaktor.ts file, declare the roles your app needs and attach permissions to them:

import { registerRole, registerPermission, registerReaktorDefinition } from '@operaide/aktor';

const roleEditor = registerRole('roleEditor', {
description: 'Can use the editor feature',
});

const permToRunEditor = registerPermission('permToRunEditor', {
description: 'Permission to call the editor Reaktor',
roles: [roleEditor],
});

A role can also imply other roles. For example, if your app has a basic tier and an advanced tier, you can make the advanced role a superset of the basic one:

const roleAdvanced = registerRole('roleAdvanced', {
description: 'Access to advanced features',
implies: [roleEditor], // roleAdvanced users also have roleEditor
});

Attach the permission to a Reaktor using requiredPermissions to enforce it server-side:

registerReaktorDefinition({
reaktorDefinitionId: 'my-editor',
requiredPermissions: [permToRunEditor],
// ...
});

The platform rejects any call that does not come from a user with the required role, regardless of what the UI does.

Read Permissions in the UI

When the platform serves your App UI page, it injects a window.Operaide object. It contains one boolean property for each permission you registered, named after the permission key:

// Available in your page's JavaScript after the platform injects it
console.log(window.Operaide.permToRunEditor); // true or false

Use these booleans at page load to update the UI accordingly:

document.addEventListener('DOMContentLoaded', function () {
const button = document.getElementById('editorButton');

if (!window.Operaide.permToRunEditor) {
button.disabled = true;
button.textContent = 'Feature not available — contact your administrator';
}
});
UI checks are cosmetic

window.Operaide is for user experience only. The platform always enforces requiredPermissions on the server — a user cannot bypass the check by modifying the page. Disable or hide controls so users understand their access level, but never rely on this alone for security.

Assigning Roles

An administrator assigns roles to users in the platform's User Management settings for the App Instance. A user who does not have the required role sees the disabled UI, and receives an error if they call the Reaktor directly.

External CDN Resources

By default, the platform's Content Security Policy (CSP) blocks resources from external origins. To load CSS or JavaScript from a CDN, list the allowed origins in public/.operaide-external-resources.json:

{
"allowedOrigins": [
"https://cdn.jsdelivr.net"
]
}

The server reads this file at runtime to generate the correct CSP headers. The file itself is not served via HTTP.

Starter Template

The fastest way to start with App UI is the Hello World with UI template in the App Store. It includes a working public/ directory with authentication and a Reaktor call already wired up.