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.com → https://unpkg.com
  • https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css → https://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​