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 thenamefield frompackage.json<version>is theversionfield frompackage.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:
- You access the app URL
- If not logged in → redirects to Operaide login page
- After login → automatically returns to your app
- 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()orXMLHttpRequestto 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
allowedOriginsarray
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.comhttps://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 Fontshttps://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:
- Check that
.operaide-external-resources.jsonexists inpublic/ - Verify the origin is in the
allowedOriginsarray - 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:
- ✅ Config file is named exactly
.operaide-external-resources.json - ✅ Config file is in
public/directory - ✅ JSON is valid (no syntax errors)
- ✅ Origins are in
allowedOriginsarray (not a different field name) - ✅ App has been republished with
npm run deploy - ✅ 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
allowedOriginslist 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
- Reaktor Deployment - How to deploy your Reaktor apps
- Building Reaktors and Aktors - Creating the backend logic
- App Store: op-demo-hello-world-with-ui - Complete working example