Runtime & View
The view endpoints serve apps to end users and provide a secure proxy for API access.
Architecture Overview
Apps run in a sandboxed environment with:
- Entry Point: HTML served with context injection
- Static Assets: CSS, JS, images served from library
- API Proxy: Whitelisted access to Informer APIs
- Server Route Dispatch: File-convention server-side handlers via
_server/prefix - View Tokens: Read-only tokens created for each view session
Security Model
Direct API Access Blocked: Apps cannot make direct fetch() calls to /api/ endpoints. This prevents malicious app code from bypassing the whitelist using session cookies.
Proxy Required: All API access must go through /api/apps/{id}/view/api/ which enforces:
- Whitelist validation (informer.yaml)
- Read-only restrictions
- App-specific permissions
Referer Check: The server blocks any /api/ request with a Referer header indicating an app view context (unless it's the proxy route itself).
GET /api/apps/{id}/view
Serve the app's entry point (typically index.html) with injected context.
Authentication: Required (session or token)
Permissions Required: Member role (run permission)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
Response:
Returns HTML content with injected JavaScript variables:
<!DOCTYPE html>
<html>
<head>
<title>Sales Dashboard</title>
<script>
// Injected context
window.__INFORMER__ = {
report: {
id: "d4f8a2b1-1234-5678-90ab-cdef12345678",
name: "Sales Dashboard"
},
theme: "light",
roles: ["viewer", "approver"],
viewToken: "abc123def456...",
user: {
username: "john.doe",
displayName: "John Doe"
},
datasets: {
"sales": {
"id": "dataset-uuid-1",
"name": "Sales Data"
}
}
};
</script>
</head>
<body>
<!-- App content -->
</body>
</html>
Context Injection:
The server injects a window.__INFORMER__ object containing:
report- App identity (idandname)theme- Current theme (lightordark)roles- Array of the user's resolved role IDs (see Roles)viewToken- Temporary read-only token for this sessionuser- Current user informationdatasets- Dataset references with metadataopenChat(opts)- Open the AI copilot with context (see Embedded Chat)showCopilot()- Activate the platform copilot button (hidden by default for apps)registerTool(def)- Register a tool the AI copilot can call at runtime
Entry Point:
The entry point is determined by the app's defn.entryPoint field (default: index.html).
Error Responses:
404 Not Found- App doesn't exist or user lacks access403 Forbidden- User lacks run permission
GET /api/apps/{id}/launch
Serve the app as a standalone, home-screen-installable document — the same runtime as /view, but a top-level page (not iframed) with web-clip <head> metadata so an iOS/Android "Add to Home Screen" tile opens the app chrome-less, inheriting its name and icon.
Authentication: Optional (mode: try). Authenticated requests render the app; unauthenticated requests redirect to /go/login and return to this same /launch URL after sign-in.
Permissions Required: Read access to the app.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID (ownerId:slug) |
path | string | Client-side route path (optional, may be empty) — the catch-all that lets an HTML5 history route survive a hard refresh of the tile, the same way /view/-/{path*} does. Ignored by the server; the SPA router picks it up. |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
theme | string | light or dark (optional) |
Response:
Returns the same HTML as /view plus web-clip meta (apple-mobile-web-app-capable, apple-mobile-web-app-title, theme-color, and — when the app ships a favicon.svg — an apple-touch-icon link to /launch/icon-180.png).
The same standalone-context fields are injected on window.__INFORMER__ for both /view and /launch — appDeepLink (informer://go/apps/{id}) and openInApp() (navigate to that deep link). What differs is the value of standalone:
standalone—trueon this/launchentry,falsewhen iframed via/view(or running inside GO). Apps gate their "open in app" affordance on this flag.
The server injects no "open in app" UI; the app decides if and where to surface it using the fields above.
Error Responses:
404 Not Found— App doesn't exist or user lacks access
GET /api/apps/{id}/launch/icon-180.png
Serve the home-screen tile icon: a 180×180 PNG rasterized from the app's library favicon.svg (iOS ignores SVG apple-touch-icons). The /launch page only emits the icon <link> when a favicon.svg exists.
Authentication: Optional (mode: try). Reads the access token from the ?token= the page appends to the icon URL so iOS can fetch it cookielessly.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
Response:
200 OK with Content-Type: image/png (Cache-Control: private, max-age=86400), or 404 Not Found when the app has no favicon, isn't readable, or the SVG can't be rasterized — on 404 the OS draws its own initials tile.
GET /api/apps/{id}/view/-/{path*}
Serve static assets (CSS, JS, images) from the app's library, or fall through to the host index.html when the path is an HTML5 history route rather than a real file.
Authentication: Required (session or token)
Permissions Required: Member role (run permission)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
path | string | Asset path (e.g., css/styles.css, images/logo.png) — may be empty |
Response:
If a file at path exists in the library, returns the file content with the appropriate Content-Type header.
If the file is not found, the server inspects the Accept header:
| Request shape | Behavior |
|---|---|
Accept includes text/html (top-level navigation, hard refresh, iframe load) | Falls through to the host index.html so the SPA router can pick the path up client-side |
Empty path (request to /view/-/) | Same — serves the host page |
Anything else (<script src>, <link href>, fetch() for JSON, etc.) | 404 Not Found |
This split lets HTML5 history routes survive a hard refresh while genuine missing-asset errors still surface as 404s. SPAs that use a client-side router (React Router, Vue Router, vanilla history.pushState) work without any extra server configuration.
URL Structure:
The /-/ prefix separates assets from API routes and is the value injected as the <base href> on every page render:
/view/-/css/styles.css— Static asset/view/-/orders/123— Client-side history route (falls through to host page on direct load)/view/api/datasets— API proxy
Example — assets:
<!-- In index.html, all of these resolve under <base href="/api/apps/.../view/-/"> -->
<link rel="stylesheet" href="css/styles.css">
<script src="js/app.js"></script>
<img src="images/logo.png">
Example — client-side routing:
// React Router, vanilla history API, etc. all work — the server fall-through
// means a hard refresh of /api/apps/.../view/-/orders/123 still returns the
// host index.html, and the client router takes over from there.
history.pushState({}, '', 'orders/123');
Best practices for client-side routing:
- Prefer HTML5 history routing over hash routing. URLs are cleaner, deep links survive copy/paste, and the server fall-through handles refreshes for you. Hash routing also works (the fragment never reaches the server) but you lose the URL polish for no benefit.
- Use relative paths in your router. The injected
<base href>already points at/api/apps/.../view/-/, sopushState({}, '', 'orders/123')lands at the correct absolute URL. Never hard-code the/api/apps/...prefix in your app — it changes per deployment. - For React Router: read the base from the document and pass it to
BrowserRouter:const basename = new URL(document.querySelector('base').href).pathname;<BrowserRouter basename={basename}>...</BrowserRouter> - For Vue Router: use
createWebHistory(new URL(document.querySelector('base').href).pathname). - Set explicit
Accepton programmatic fetches. The server usesAccept: text/htmlto decide between asset-404 and SPA-fallback, so a strayfetch('data.json')without anAcceptheader is fine (browsers default to*/*which doesn't trigger the fallback), but explicit is better:fetch('data/manifest.json', { headers: { accept: 'application/json' } });
Error Responses:
404 Not Found— App doesn't exist, OR file is missing AND the request is not an HTML navigation403 Forbidden— User lacks run permission
API Proxy Routes
The proxy routes allow apps to make API calls while enforcing whitelist restrictions.
GET /api/apps/{id}/view/api/{path*}
Proxy GET requests to Informer APIs.
Authentication: App view token (from context)
Permissions Required: Whitelist validation
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
path | string | API path to proxy (e.g., datasets/sales-data/data) |
Example:
// In app code
const response = await fetch('/api/apps/my-app-id/view/api/datasets/sales-data/data?limit=100');
const data = await response.json();
Whitelist Enforcement:
The proxy checks the app's informer.yaml file to ensure the requested path is allowed. See the Data Access section for whitelist configuration.
POST/PUT/PATCH/DELETE /api/apps/{id}/view/api/{path*}
Proxy write requests to Informer APIs.
Authentication: App view token
Permissions Required: Whitelist validation + write permissions
Payload:
Request body is passed through to the target API.
Example:
// In app code
const response = await fetch('/api/apps/my-app-id/view/api/datasets/sales-data/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filters: [...] })
});
Restrictions:
- Write operations are subject to stricter whitelist validation
- Read-only tokens cannot make write requests
- User's actual permissions are checked on the target resource
Server Route Dispatch
When an app's client code fetches a URL containing the _server/ prefix, the proxy dispatches the request to a registered server route handler instead of forwarding it to an Informer API endpoint.
// Client code — works in both dev mode and production
const orders = await fetch('/api/_server/orders');
const data = await orders.json();
Server routes are defined by files in the app's server/ directory and deployed via POST /api/apps/{id}/_deploy. See Server Routes for full documentation.
Snapshots
Apps can have snapshots for version control and rollback.
GET /api/apps/{id}/snapshots
List all snapshots for an app.
Authentication: Required
Permissions Required: Member+ role (write permission)
Response:
[
{
"id": "snapshot-uuid-1",
"appId": "d4f8a2b1-1234-5678-90ab-cdef12345678",
"name": "Before redesign",
"createdBy": "john.doe",
"createdAt": "2024-02-10T10:00:00.000Z"
}
]
POST /api/apps/{id}/snapshots
Create a snapshot of the current app state.
Authentication: Required
Permissions Required: Member+ role (write permission)
Payload:
{
"name": "Before redesign"
}
Response:
Returns the created snapshot.
{
"id": "snapshot-uuid-1",
"appId": "d4f8a2b1-1234-5678-90ab-cdef12345678",
"name": "Before redesign",
"createdBy": "john.doe",
"createdAt": "2024-02-13T10:30:00.000Z"
}
DELETE /api/apps/{id}/snapshots/{snapshotId}
Delete a snapshot.
Authentication: Required
Permissions Required: Member+ role (write permission)
Response:
Returns 204 No Content on success.
POST /api/apps/{id}/snapshots/{snapshotId}/_restore
Restore an app to a previous snapshot.
Authentication: Required
Permissions Required: Member+ role (write permission)
Response:
Returns the restored app.
Behavior:
- App's defn, settings, and library are restored to snapshot state
- Current state is not automatically snapshotted (create one manually if needed)
- Restoration cannot be undone (unless you have another snapshot)
Data Access Whitelist
Apps should include an informer.yaml file in their library root to configure API access:
# informer.yaml
access:
datasets:
- admin:sales-data
- admin:customers
queries:
- admin:monthly-summary
integrations:
- salesforce
apis:
- POST /api/custom/endpoint
Resource types:
| Type | API Access Granted |
|---|---|
datasets | _search, fields |
queries | _execute |
datasources | _query |
integrations | request |
libraries | contents/*, files/*/contents |
apis | Exact method + path match |
Default Behavior:
Without an informer.yaml file (or its access section), the proxy denies all requests.
Common Runtime Patterns
Initialize App with Context
// In your app's JavaScript
const context = window.__INFORMER__;
console.log('Running as:', context.user.displayName);
console.log('Available datasets:', Object.keys(context.datasets));
console.log('User roles:', context.roles);
// Use the view token for API calls
fetch(`/api/apps/${context.report.id}/view/api/datasets/${context.datasets.sales.id}/data`, {
headers: {
'Authorization': `Bearer ${context.viewToken}`
}
});
Load Dataset Data
async function loadSalesData() {
const context = window.__INFORMER__;
const datasetId = context.datasets.sales.id;
const response = await fetch(
`/api/apps/${context.report.id}/view/api/datasets/${datasetId}/data?limit=1000`
);
if (!response.ok) {
throw new Error('Failed to load data');
}
const data = await response.json();
return data.items;
}
Run a Query
async function runQuery(filters) {
const context = window.__INFORMER__;
const datasetId = context.datasets.sales.id;
const response = await fetch(
`/api/apps/${context.report.id}/view/api/datasets/${datasetId}/run`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filters })
}
);
return await response.json();
}
Handle Errors
async function fetchWithErrorHandling(path) {
const context = window.__INFORMER__;
const url = `/api/apps/${context.report.id}/view/api/${path}`;
try {
const response = await fetch(url);
if (!response.ok) {
if (response.status === 403) {
throw new Error('This API endpoint is not whitelisted');
} else if (response.status === 404) {
throw new Error('Resource not found');
} else {
throw new Error(`API error: ${response.status}`);
}
}
return await response.json();
} catch (error) {
console.error('API call failed:', error);
throw error;
}
}
Create Snapshot Before Changes
// Create a snapshot
const snapshot = await POST('/api/apps/analytics:sales-dashboard/snapshots', {
name: `Before ${new Date().toLocaleDateString()}`
});
// Make changes...
await PUT('/api/apps/analytics:sales-dashboard/contents/index.html', {
content: newHTML
});
// If something goes wrong, restore
await POST(`/api/apps/analytics:sales-dashboard/snapshots/${snapshot.id}/_restore`);
Security Best Practices
For App Developers
- Use the Proxy: Never try to access
/api/directly - Validate Input: Always validate user input before making API calls
- Handle Errors: API calls may fail due to permissions or whitelist restrictions
- Respect Read-Only: Don't assume write permissions
For App Administrators
- Configure Whitelist: Always include an
informer.yamlfile with anaccesssection - Minimize Access: Only whitelist necessary endpoints
- Review Regularly: Audit app API usage periodically
- Use Snapshots: Create snapshots before major changes
- Test Thoroughly: Test apps with different user roles
For Platform Administrators
- Monitor Usage: Track app API calls for abuse
- Enforce Limits: Rate-limit app API calls if needed
- Review Whitelists: Audit informer.yaml files in apps
- Educate Users: Train developers on security best practices