Skip to main content
Version: 2.6

Building Reaktor Apps with Web UIs

Reaktor apps can include web-based user interfaces served directly from the Operaide platform. This guide covers how to build, configure, and deploy UIs for your Reaktor applications.

Overview

What Are Reaktor App UIs?

Reaktor app UIs are static web applications (HTML, CSS, JavaScript) that are:

  • Packaged alongside your Reaktor in the public/ directory
  • Served via the /app-ui/<package-name>@<version>/ route
  • Authenticated using your Operaide login session
  • Able to call your Aktor endpoints directly

URL Structure

The URL format uses your app's package.json metadata:

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

Where:

  • <name> is the name field from package.json
  • <version> is the version field from package.json

Example: Given this package.json:

{
"name": "operaide-app-op-demo-hello-world-with-ui",
"version": "0.5.5"
}

Your app is accessible at:

https://your-operaide-instance.com/app-ui/operaide-app-op-demo-hello-world-with-ui@0.5.5/
https://your-operaide-instance.com/app-ui/operaide-app-op-demo-hello-world-with-ui@0.5.5/styles.css
https://your-operaide-instance.com/app-ui/operaide-app-op-demo-hello-world-with-ui@0.5.5/app.js

All files are served from your app's public/ directory.

Authentication

Apps use your existing Operaide login session - no separate credentials needed!

How it works:

  1. You access the app URL
  2. If not logged in → redirects to Operaide login page
  3. After login → automatically returns to your app
  4. Your app loads with full authentication

For API calls, your app code uses the same session token:

// Get your session token using the Operaide helper function
// This helper is automatically injected into all HTML pages
const loginToken = window.getOperaideAuthToken();

// Use it to call Aktor endpoints
const response = await fetch('/api/v1/aktor/my-aktor', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Operaide-Token': loginToken // Your session token
},
body: JSON.stringify({ data: 'example' })
});

No API keys to manage! Just use your Operaide login session via the window.getOperaideAuthToken() helper.

File Organization

Directory Structure

your-reaktor-app/
├── src/
│ └── YourReaktor.reaktor.ts
├── public/
│ ├── index.html # Entry point
│ ├── app.js # Application logic
│ ├── styles.css # Custom styles
│ └── .operaide-external-resources.json # CSP config (optional)
├── package.json
└── ...

File Splitting

You can split your application across multiple files:

index.html - Main HTML structure

<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="./styles.css">
</head>
<body>
<!-- Your UI here -->
<script src="./app.js"></script>
</body>
</html>

styles.css - CSS styling

body {
font-family: sans-serif;
margin: 0;
padding: 20px;
}

app.js - JavaScript logic

// Your application code
console.log('App loaded');

Relative URLs

Use relative paths to reference files in the same directory:

<link rel="stylesheet" href="./styles.css">
<script src="./app.js"></script>
<img src="./logo.png">

Important: Always access your app with a trailing slash (/app-ui/<package-name>@<version>/) to ensure relative URLs resolve correctly.

External Resources (CDN)

Why Are External Resources Blocked?

By default, Reaktor apps use a strict Content Security Policy (CSP) that only allows loading resources from the same origin. This prevents:

  • Cross-site scripting (XSS) attacks
  • Data exfiltration
  • Malicious script injection

To load resources from external CDNs (Bootstrap, Google Fonts, etc.), you must explicitly configure them.

Content Security Policy Basics

CSP is a security feature that controls which origins can serve resources to your app:

What CSP Controls:

  • <script src="..."> - JavaScript from CDNs
  • <link rel="stylesheet" href="..."> - CSS from CDNs
  • <img src="..."> - Images from CDNs
  • <link rel="font" href="..."> - Fonts from CDNs

What CSP Does NOT Control:

  • fetch() or XMLHttpRequest to external APIs (controlled by CORS)

Creating the Configuration File

Create public/.operaide-external-resources.json:

{
"allowedOrigins": [
"https://cdn.jsdelivr.net",
"https://unpkg.com",
"https://fonts.googleapis.com"
]
}

Important Notes:

  • File must be named .operaide-external-resources.json
  • Must be in the public/ directory
  • Will NOT be served via HTTP (dot files are blocked for security)
  • JSON format with allowedOrigins array

Origin-Level vs URL-Level Control

CSP works at the origin level (protocol + domain), not URL level.

When you allow an origin:

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

This permits ALL resources from that origin:

  • https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css
  • https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js
  • https://cdn.jsdelivr.net/any-package@any-version/any-file.js

You cannot restrict to specific files or paths - CSP only controls origins.

Forgiving URL Format

The configuration accepts both origins and full URLs:

{
"allowedOrigins": [
"https://unpkg.com",
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
]
}

The server automatically extracts origins:

  • https://unpkg.comhttps://unpkg.com
  • https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.csshttps://cdn.jsdelivr.net

This makes configuration easier and serves as documentation of which resources you're using.

Security Considerations

Trust Model: When you allow an origin, you trust everything from that domain. Only allow trusted CDNs:

Recommended CDNs:

  • https://cdn.jsdelivr.net - jsDelivr (npm packages)
  • https://unpkg.com - unpkg (npm packages)
  • https://fonts.googleapis.com - Google Fonts
  • https://cdnjs.cloudflare.com - Cloudflare CDN

⚠️ Be Careful With:

  • User-controlled content platforms
  • Unverified third-party CDNs
  • Origins you don't fully trust

What Happens Without Configuration?

If .operaide-external-resources.json is missing or malformed:

  • External resources are blocked by CSP
  • Browser console shows CSP violation errors
  • Developers see issues immediately (fail-fast)

Example console error:

Refused to load the stylesheet 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css'
because it violates the following Content Security Policy directive: "style-src 'self' 'unsafe-inline'".

Calling Aktor Endpoints

Your UI can call Reaktor aktor endpoints using the standard API format.

Endpoint Format

POST /api/v1/aktor/{aktorName}

The aktorName comes from your Reaktor registration:

// In your .reaktor.ts file
registerReaktorDefinition({
reaktorDefinitionId: 'my-reaktor',
// ... other config
createReaktor(params) {
// ...
}
});

Getting Your Session Token

The Operaide platform provides a helper function to get your authentication token:

// Use the helper function (automatically injected into all HTML pages)
const loginToken = window.getOperaideAuthToken();

This abstracts away the implementation details and provides a consistent API for token access.

Making API Calls

Use the session token in the X-Operaide-Token header:

async function callMyAktor(inputData) {
// Get your session token using the helper
const loginToken = window.getOperaideAuthToken();

// Call the aktor endpoint
const response = await fetch('/api/v1/aktor/my-reaktor', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Operaide-Token': loginToken // Your session token
},
body: JSON.stringify({
input: inputData
})
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

return await response.json();
}

Error Handling

If your session expires, the API will return a 401 error. Handle it by redirecting to login:

async function callMyAktor(inputData) {
const loginToken = window.getOperaideAuthToken();

const response = await fetch('/api/v1/aktor/my-reaktor', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Operaide-Token': loginToken
},
body: JSON.stringify({ input: inputData })
});

if (response.status === 401) {
// Session expired - redirect to login
window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname);
return;
}

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

return await response.json();
}

Complete Example

Example: Bootstrap App with API Integration

Here's a complete example using Bootstrap from CDN and calling a hello-world aktor.

public/.operaide-external-resources.json

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

public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World API Test</title>

<!-- Bootstrap from CDN (requires CSP config) -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">

<!-- Custom styles -->
<link rel="stylesheet" href="./styles.css">
</head>
<body>
<div class="container py-5">
<div class="card">
<div class="card-body">
<h1>API Integration Test</h1>
<input type="text" class="form-control" id="input" placeholder="Enter text...">
<button class="btn btn-primary mt-3" onclick="testAPI()">Call API</button>
<div id="response" class="mt-3"></div>
</div>
</div>
</div>

<!-- Bootstrap JS from CDN -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>

<!-- Application logic -->
<script src="./app.js"></script>
</body>
</html>

public/styles.css

body {
background: linear-gradient(135deg, #e0e7ff 0%, #f3e8ff 100%);
min-height: 100vh;
}

.card {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

public/app.js

async function testAPI() {
const responseDiv = document.getElementById('response');
const input = document.getElementById('input').value;

responseDiv.innerHTML = '<div class="spinner-border" role="status"></div>';

try {
// Get your Operaide session token using the helper
const loginToken = window.getOperaideAuthToken();

// Call the aktor endpoint
const response = await fetch('/api/v1/aktor/hello-world', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Operaide-Token': loginToken
},
body: JSON.stringify({ world: input || 'world' })
});

if (response.status === 401) {
// Session expired
responseDiv.innerHTML = `
<div class="alert alert-warning">
Session expired. <a href="/login?redirect=${encodeURIComponent(window.location.pathname)}">Please log in</a>
</div>
`;
return;
}

if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}

const data = await response.json();
responseDiv.innerHTML = `
<div class="alert alert-success">
<strong>Success:</strong> ${data}
</div>
`;
} catch (error) {
responseDiv.innerHTML = `
<div class="alert alert-danger">
<strong>Error:</strong> ${error.message}
</div>
`;
}
}

Reference Example

The op-demo-hello-world-with-ui app in the App Store demonstrates all these features:

  • Bootstrap 5 from CDN
  • File splitting (HTML, CSS, JS)
  • External resources configuration
  • Session token usage
  • Calling aktor endpoints
  • Error handling

Install it from the App Store and download it to see the complete implementation.

Troubleshooting

CSP Violations in Console

Symptom:

Refused to load the stylesheet 'https://example.com/style.css' because it violates
the following Content Security Policy directive...

Solution:

  1. Check that .operaide-external-resources.json exists in public/
  2. Verify the origin is in the allowedOrigins array
  3. Republish your app (npm run deploy)

Session Expired Errors

Symptom: API calls return 401 Unauthorized.

Cause: Your Operaide login session has expired.

Solution: Redirect the user to login:

if (response.status === 401) {
window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname);
}

The user will be automatically redirected back to your app after logging in.

Missing Trailing Slash

Symptom: Relative URLs like ./app.js don't load correctly.

Cause: Accessing /app-ui/<package-name>@<version> without trailing slash causes the browser to resolve ./app.js incorrectly.

Solution: Always access your app with a trailing slash: /app-ui/<package-name>@<version>/

The server automatically redirects to add the trailing slash.

Dot Files Blocked

Symptom: Trying to access .operaide-external-resources.json via HTTP returns 403 Forbidden.

Explanation: This is intentional security. Dot files (.filename) cannot be served via HTTP to prevent accidental exposure of:

  • .operaide-external-resources.json (configuration)
  • .env (environment variables)
  • .git/ (source control data)

The server reads the config file internally but blocks HTTP access.

External Resources Still Blocked After Configuration

Checklist:

  1. ✅ Config file is named exactly .operaide-external-resources.json
  2. ✅ Config file is in public/ directory
  3. ✅ JSON is valid (no syntax errors)
  4. ✅ Origins are in allowedOrigins array (not a different field name)
  5. ✅ App has been republished with npm run deploy
  6. ✅ Page has been hard-refreshed (Ctrl+Shift+R or Cmd+Shift+R)

Token Not Found

Symptom: window.getOperaideAuthToken() returns null.

Cause: You're not logged in to Operaide.

Solution: The app should automatically redirect you to login. If not, manually go to /login.

Best Practices

Security

  • Only allow trusted CDN origins
  • Keep the allowedOrigins list minimal
  • Regularly review and audit external dependencies
  • Use specific package versions in CDN URLs (not @latest)
  • Always handle 401 errors by redirecting to login

Performance

  • Minimize the number of external resources
  • Use CDN versions with integrity hashes when possible
  • Consider bundling dependencies locally for critical resources
  • Leverage browser caching with appropriate CDN URLs

Development

  • Test with and without the config file to verify CSP works
  • Use browser DevTools to inspect CSP violations
  • Keep the demo app (op-demo-hello-world-with-ui) as a reference
  • Document which external resources your app uses
  • Test session expiration handling

File Organization

  • Keep HTML minimal (structure only)
  • Put styling in styles.css
  • Put logic in app.js
  • Use meaningful file names for additional resources
  • Add comments explaining the purpose of external resources

Error Handling

  • Always check for 401 (session expired) and redirect to login
  • Show user-friendly error messages
  • Log errors to console for debugging
  • Provide fallback UI when API calls fail

See Also