Skip to main content

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:

ParameterTypeDescription
idstringApp 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 (id and name)
  • theme - Current theme (light or dark)
  • roles - Array of the user's resolved role IDs (see Roles)
  • viewToken - Temporary read-only token for this session
  • user - Current user information
  • datasets - Dataset references with metadata
  • openChat(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 access
  • 403 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:

ParameterTypeDescription
idstringApp UUID or natural ID (ownerId:slug)
pathstringClient-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:

ParameterTypeDescription
themestringlight 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 /launchappDeepLink (informer://go/apps/{id}) and openInApp() (navigate to that deep link). What differs is the value of standalone:

  • standalonetrue on this /launch entry, false when 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:

ParameterTypeDescription
idstringApp 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:

ParameterTypeDescription
idstringApp UUID or natural ID
pathstringAsset 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 shapeBehavior
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/-/, so pushState({}, '', '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 Accept on programmatic fetches. The server uses Accept: text/html to decide between asset-404 and SPA-fallback, so a stray fetch('data.json') without an Accept header 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 navigation
  • 403 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:

ParameterTypeDescription
idstringApp UUID or natural ID
pathstringAPI 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:

TypeAPI Access Granted
datasets_search, fields
queries_execute
datasources_query
integrationsrequest
librariescontents/*, files/*/contents
apisExact 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

  1. Use the Proxy: Never try to access /api/ directly
  2. Validate Input: Always validate user input before making API calls
  3. Handle Errors: API calls may fail due to permissions or whitelist restrictions
  4. Respect Read-Only: Don't assume write permissions

For App Administrators

  1. Configure Whitelist: Always include an informer.yaml file with an access section
  2. Minimize Access: Only whitelist necessary endpoints
  3. Review Regularly: Audit app API usage periodically
  4. Use Snapshots: Create snapshots before major changes
  5. Test Thoroughly: Test apps with different user roles

For Platform Administrators

  1. Monitor Usage: Track app API calls for abuse
  2. Enforce Limits: Rate-limit app API calls if needed
  3. Review Whitelists: Audit informer.yaml files in apps
  4. Educate Users: Train developers on security best practices