Skip to content

Data App runtime reference

14 min read · Updated 30 Oct 2026

@astrato-io/app-runtime is the canonical browser entry point for Data Apps. It bootstraps the App’s authorized context from the current hosting origin, creates an Astrato SDK client, and manages metadata access and direct requests to the selected data exit node.

It is required for Data Apps and is not used by externally hosted apps or Workbooks.

Install the runtime in your frontend project:

Terminal window
npm install @astrato-io/app-runtime

The package is ESM-only and browser-only. createAstratoApp() runs at the origin printed by astrato dev or an enabled hosted publication URL. It fails at a raw Vite origin, during server-side rendering, or anywhere location.origin and fetch are unavailable.

import { createAstratoApp } from '@astrato-io/app-runtime';
async function start(): Promise<void> {
const app = await createAstratoApp();
// Load field metadata, then create and mount objects.
}
start().catch((error) => console.error(error)); // show it in the page; see Errors

The factory accepts an optional signal and bootstrap timeoutMs for lifecycle control. Pass them in the call in start:

async function start(): Promise<void> {
const controller = new AbortController();
const app = await createAstratoApp({
signal: controller.signal,
timeoutMs: 15_000,
});
}

It does not accept a token, tenant ID, semantic-layer ID, datasource type, API URL or custom transport. The hosting boundary derives and authorizes this context for your configured semantic layer.

It resolves to:

interface AstratoAppRuntime {
readonly client: AstratoClient;
readonly context: {
readonly version: 2;
readonly publication: { readonly id: string; readonly version: number };
readonly tenantId: string;
readonly semanticLayer: {
readonly id: string;
readonly datasourceType: string;
};
readonly capabilities: readonly AstratoAppCapabilityV1[];
};
readonly dataBinding: {
readonly semanticLayerId: string;
readonly datasourceType: string;
};
destroy(): void;
}

The context values are available at runtime for SDK and visualization calls, but they are not authored browser configuration and must not be copied into source files. Only the semantic-layer ID is selected in astrato.json; the CLI resolves its tenant and datasource type through Astrato. version: 2 identifies the hosted bootstrap contract, not a semantic-layer revision.

Create one app runtime per browser application and retain it for the page lifetime. Call app.destroy() when the application is permanently torn down. Destruction is idempotent and releases the runtime transport and SDK client.

app.client is already configured with the runtime transport and visualization engine. Do not call createAstratoClient() again.

Share this client across charts. The SDK shares matching visualization metadata loads and reuses completed snapshots. Query results remain live. If your app also declares @astrato-io/sdk directly, keep it compatible with the SDK dependency declared by your installed runtime and update both dependencies and the lockfile together. Preview bootstrap and the runtime must resolve to the same SDK module to share their iframe connection. See metadata reuse in the SDK reference.

App Runtime 0.4.0 requires SDK ^0.6.0, including support for visualization fields from custom SQL views and tables without a physical schema. Runtime 0.3.1 requires SDK ^0.5.0; update both packages together when moving an app to SDK 0.6.0.

import type { AstratoAppRuntime } from '@astrato-io/app-runtime';
async function loadFieldMetadata(app: AstratoAppRuntime) {
const getFieldMetadata = app.client.services.engine.fields.getFieldMetadata;
if (!getFieldMetadata) {
throw new Error('Astrato field metadata is unavailable.');
}
const metadata = await getFieldMetadata({
datasourceType: app.dataBinding.datasourceType,
semanticLayerId: app.dataBinding.semanticLayerId,
});
return metadata;
}

Call it from start with app, and await it before mounting native visualizations. Metadata collections are optional; do not assume metadata.fields exists. Pass the returned metadata to createVisualizationAvailableFields when compiling chart definitions.

The rest of the SDK surface is documented in the SDK reference. When it shows a configured client, use app.client. When it requires semantic-layer or datasource context, use the current values from app.dataBinding or app.context; never replace them with hard-coded values.

The Builder preview integration uses the iframe connection introduced in SDK 0.5.0. SDK 0.4.0 does not provide this integration.

In a compatible Builder, trusted bootstrap establishes the connection before your app starts. Use the same createAstratoApp() call and app.client as in local development and hosted apps. Do not add a preview-specific data helper, credentials, platform IDs or iframe detection to app source. Being inside an iframe alone does not select preview mode.

The preview context uses the existing version 1 context shape and has no publication identifier. It binds one semantic layer and exposes read-only metadata and queries. Procedure and writeback methods reject before transport. runtime: 'preview' explicitly requires an established connection; a missing connection fails without trying a different environment. An established preview also rejects an explicit request for local or publication mode.

The platform must provide a compatible, frozen SDK and runtime pair. Existing apps do not automatically upgrade when a new runtime is released.

app.executeProcedure and app.writeback need @astrato-io/app-runtime 0.2.0 and @astrato-io/cli 0.1.1 or later, and work only during astrato dev. Published Apps reject both. They run under your Astrato permissions, OAuth scopes and warehouse permissions; astrato.json lists no procedures or views.

Call a procedure on the connected datasource without supplying a datasource ID:

async function saveStrategy(app: AstratoAppRuntime): Promise<void> {
const result = await app.executeProcedure({
schema: 'DEMO.PUBLIC',
procedure: 'SP_SAVE_STRATEGY',
params: [JSON.stringify({ allocations: [{ symbol: 'EXAMPLE', target: 100 }] })],
});
console.log(result.response.data);
}

This example assumes you have a SP_SAVE_STRATEGY procedure accepting that JSON string. Parameters support strings, finite numbers, booleans and null. Serialize object parameters yourself. The procedure and engine own validation and execution; the runtime does not generate SQL.

Insert rows into a writable model view:

async function insertTrade(app: AstratoAppRuntime): Promise<void> {
const result = await app.writeback({
view: 'Trades',
operation: 'insert',
fields: [{ name: 'trade_id', valueType: 'string' }],
values: [['example-trade']],
});
console.log(result.response.rowsAffected);
}

The example requires a writable Trades view with a trade_id field. Updates also require keys: { fields, values }, with one key row for every values row. Deletes require keys and omit fields and values. Fields use string, number, date or bool; values use the same scalar parameter types as procedures. This preview accepts flat field lists only.

Both methods accept an optional second argument { signal } and an optional queryTimeout in the request, from 1 to 60 seconds. The runtime binds row writes to the current layer and procedures to its datasource. It returns procedure data and write counts while omitting warehouse links and query IDs; the engine SQL statement is not passed to the App (see Showing the SQL behind a visualization). Row results retain failure counts. Procedure and per-row errors use generic text so warehouse diagnostics are not exposed to the App.

Writes are not automatically replayed after an execution failure, timeout or lost connection. Cancellation and timeout do not prove that a warehouse change was rolled back. Check its outcome before retrying. Refresh affected queries after a confirmed change; writes do not refresh charts.

import { createAstratoApp } from '@astrato-io/app-runtime';
import { createVisualizationRuntime, registry } from '@astrato-io/visualizations';
async function start(): Promise<void> {
const app = await createAstratoApp();
const visualizations = createVisualizationRuntime({ client: app.client, registry });
visualizations.createInstance({
id: 'sales-chart',
type: 'bar',
vSpec,
dataBinding: { ...app.dataBinding },
});
}
start().catch((error) => console.error(error)); // show it in the page; see Errors

Build vSpec from a visualization definition and live field metadata as described in the Visualizations reference. Do not construct visualization field keys from names.

app.context.capabilities reports the operations the App may request through its Data App runtime:

  • semantic-layer:read;
  • datasource-metadata:read;
  • query:execute;
  • procedure:execute (local runtime, when enabled);
  • data:writeback (local runtime, when enabled);

See Writeback and stored procedures for transport limits.

The list describes the App boundary, not a promise that every signed-in account can perform every operation. Astrato also checks account permissions, OAuth scopes and data access on every remote request. Authored code cannot expand the list, and the runtime ignores capability names it does not recognise.

Published apps load their authorized model, obtain a short-lived data token and query the selected data exit node directly from the browser. The publication host exposes no generic message API or query proxy. Rebuild older hosted bundles with the current package.

Astrato governs data-token permissions, including writeback and export; the SDK does not determine them. Main Astrato OAuth credentials and refresh tokens remain on the server. Data tokens remain in memory, and requests to the data node omit browser cookies and reject redirects.

The data connection must select a supported non-legacy exit node. Publication hosting can differ from data placement, including another region or private infrastructure. The browser must be able to reach the selected data node. A node failure is reported without trying another execution path.

Local loopback origins use the local development adapter automatically. For another local host, use createAstratoApp({ runtime: 'local' }). The local adapter uses same-origin requests and is unavailable on hosted publications. Local development queries the current model without revision pinning, so reload after model edits.

During astrato dev, the local App runtime provides app.executeProcedure() and app.writeback() through the CLI development gateway. They require the corresponding procedure:execute or data:writeback capability, resource permissions and a supported datasource. These methods are unavailable through the hosted App publication transport; SQL SELECT requests are also unavailable through the CLI development gateway.

For an externally hosted app with an authenticated platform transport, use the generic SDK requests in Writeback and SQL. Those examples require that platform connection. OAuth scopes authorize the corresponding platform APIs, subject to resource permissions; they do not add routes or capabilities to an App transport.

When a published App cannot obtain data access because your data connection needs OAuth authorization, the runtime displays a centered recovery dialog over a blurred backdrop. Choose Sign in to data connection to open Astrato’s sign-in flow in a new tab. Complete authorization there, return to the App, and refresh your browser tab. The runtime never opens the sign-in tab or reloads automatically, and does not automatically replay the failed request.

Choose Continue without data or press Escape to dismiss the dialog; charts using the connection may remain unavailable. Concurrent failures with the same recovery details share one dialog, including after it is dismissed.

If the connection needs administrator configuration, the prompt asks you to contact the App owner or your Astrato administrator instead. Ordinary permission errors and exit-node failures do not produce a datasource sign-in prompt.

If the authorization response has no recovery decision, the prompt explains that no sign-in link was provided. This does not establish that administrator configuration is required.

For a recognized mint failure, error.response?.error.code is datasource_authorization_required. Its optional details contains provider, reason (missing_grant, expired_grant, or insufficient_scope) and the validated recovery decision. A sign-in recovery includes connectionId and authorizationUrl; a configuration recovery has status configuration-required. Credentials are never included. These details describe connection authorization, separately from your App viewer session.

Existing published builds need to be rebuilt with the updated runtime and published again to display this prompt.

import { AstratoAppRuntimeError, createAstratoApp } from '@astrato-io/app-runtime';
async function start(): Promise<void> {
try {
const app = await createAstratoApp();
} catch (error) {
if (error instanceof AstratoAppRuntimeError) {
console.error(error.code, error.status, error.response?.error.requestId);
} else {
throw error;
}
}
}
start().catch((error) => console.error(error));

Runtime error codes:

Code Meaning
unsupported_environment Required browser origin or fetch is unavailable
bootstrap_failed The bootstrap request failed
invalid_bootstrap The boundary returned an invalid versioned context
runtime_context_changed The publication or refreshed session’s data context changed
runtime_request_failed An SDK request failed; inspect status and the structured response
timed_out The bootstrap request exceeded its configured timeout
aborted Runtime work was cancelled

When Astrato returns a structured response, error.response.error contains code, message, requestId and retryable. Display the message and retain the request ID for support. Do not automatically retry non-retryable errors or invent alternate API routes.

Hosted apps built with @astrato-io/app-runtime 0.2.1 or later tolerate a device clock up to five seconds behind Astrato. When a data grant appears too far in the future, expired or too close to expiry, the runtime reports a runtime_request_failed error with guidance to check automatic date and time synchronization, reload, and contact support if the problem persists. This does not establish that the device clock is the cause of every expiry error. Larger clock changes during a session still require resynchronizing the device and reloading.

Existing app bundles need a runtime dependency update, rebuild and republish to get this behavior. Builder projects also need their saved runtime version updated.

When publication hosting is enabled for your workspace, open the app’s assigned URL and sign in with Astrato. The app uses the same createAstratoApp() entry point as local development. Your publication and semantic-layer permissions still apply. Hosted browser-IP restrictions are not enforced in the current preview. The app receives an independent session; your Astrato credentials remain on the server.

Hosted sessions currently last at most five minutes. If the session expires, reopen or reload the app to sign in again. Replacing a publication also requires a reload. An unpublished app cannot serve its files, including files previously cached by your browser.

The hosted runtime automatically reloads the current page when a request reports 409 app_changed. This loads the updated application and establishes its fresh session. No reload hook is required for this default behavior. Automatic reload can discard unsaved input; register onPublicationChanged when your app needs to preserve state or ask first.

Availability: use a runtime build that exposes CreateAstratoAppOptions.onPublicationChanged. Published runtime versions 0.2.1 and earlier do not provide this hook or automatic recovery. Existing apps need an updated runtime dependency, rebuild and republish. Check the installed package types before generating code that uses the hook. Do not silence a missing property with a type assertion.

The failed operation still rejects with runtime_context_changed. The runtime stops issuing requests and serving cached metadata, and later operations retain that error. Reload detection happens on a runtime request or data-token renewal; there is no push notification to idle apps. This behavior applies to hosted publications, including a failure during initial bootstrap. The local development adapter (runtime: 'local'), Builder previews and SDK-only apps do not invoke this hook.

Register this optional callback when calling createAstratoApp(), before the first request. It is called once per runtime that detects a publication change and is awaited before automatic navigation. It receives an AstratoAppPublicationChange:

Field Meaning
error The AstratoAppRuntimeError with code runtime_context_changed. A server error includes its status and structured response/request ID.
automaticReloadBlocked Whether the loop guard currently blocks automatic reload, including when session storage cannot be read. Storage write failures can also block reload after the callback.
reload() Explicitly reload the current page. Retain this function if you return false, and call it after the user agrees or state is saved. Repeated calls on the same event navigate only once.
Callback result Runtime behavior
undefined / no return, or true Reload automatically after the callback completes, subject to the loop guard.
false Stay on the page. Your app owns the recovery UI and can call reload() later.
A promise Wait for its result with the same rules. There is no timeout that forces navigation while saving or asking the user.
Throw or rejected promise Stay on the page and show the built-in manual reload prompt. Failed persistence does not trigger automatic navigation.

Requests reject promptly while the callback is pending. Do not wait for requests on the failed runtime, reinitialize it in a retry loop, or retry the failed operation to recover. Save local app state and reload instead. The callback must not rely on createAstratoApp() having resolved: bootstrap itself can detect the change. App teardown does not cancel a pending recovery callback.

Save and restore session state. This example expects a <textarea id="notes"> in the page. Chart setup is omitted; keep the returned runtime for your normal app lifecycle.

Browser storage can be unavailable, for example when browser settings block it, and reading it then throws. Wrap every storage access in try/catch, and keep the app working without saved state. The App Builder preview has no storage; see Constraints on app code.

import { AstratoAppRuntimeError, createAstratoApp } from '@astrato-io/app-runtime';
const notes = document.querySelector<HTMLTextAreaElement>('#notes');
if (!notes) throw new Error('The notes field is missing.');
const draftKey = 'example-app:notes:v1';
let saved: string | null = null;
try {
saved = sessionStorage.getItem(draftKey);
} catch {
// Continue without a restored draft when storage is unavailable.
}
if (saved !== null) notes.value = saved;
const start = async (): Promise<void> => {
const app = await createAstratoApp({
onPublicationChanged() {
// No return: reload after saving. If storage is unavailable this throws, and the runtime
// keeps the page and shows its manual reload prompt, so the notes are not lost.
sessionStorage.setItem(draftKey, notes.value);
},
});
// Load metadata and mount objects as usual.
};
start().catch((error) => {
// Recovery is already in progress or deferred: keep its UI.
if (error instanceof AstratoAppRuntimeError && error.code === 'runtime_context_changed') return;
console.error(error); // show other failures in the page; see Errors
});

For asynchronous persistence, make the callback async and await your save operation before returning. Store app-owned state only; never copy runtime credentials or data tokens into storage. Use a namespaced, versioned key and validate restored structured data against your app’s schema.

Ask the user and defer navigation. This example adds an app-owned recovery message and button. It works even if the change occurs before bootstrap completes. You can replace the DOM controls with your own dialog; retain reload and return false immediately if the dialog handles its answer later.

import { AstratoAppRuntimeError, createAstratoApp } from '@astrato-io/app-runtime';
async function start(): Promise<void> {
const app = await createAstratoApp({
onPublicationChanged({ automaticReloadBlocked, reload }) {
const notice = document.createElement('section');
notice.setAttribute('role', 'status');
const message = document.createElement('p');
message.textContent = automaticReloadBlocked
? 'The app still needs an update after a recent reload. Copy your work before trying again.'
: 'An updated app is available. Copy your unsaved work before continuing.';
const button = document.createElement('button');
button.type = 'button';
button.textContent = 'Reload when ready';
button.addEventListener('click', reload, { once: true });
notice.append(message, button);
document.documentElement.append(notice);
return false;
},
});
// Load metadata and mount objects as usual.
}
start().catch((error) => {
// Recovery is already in progress or deferred: keep its UI.
if (error instanceof AstratoAppRuntimeError && error.code === 'runtime_context_changed') return;
console.error(error); // show other failures in the page; see Errors
});

In both examples, the startup error handling recognizes AstratoAppRuntimeError with code runtime_context_changed: recovery is already in progress or has been deferred. Keep the recovery UI mounted and do not replace it with a generic sign-in failure. Handle other initialization errors normally.

The runtime records each reload attempt in session storage for the current publication origin and browser tab. It permits at most one automatic reload in 60 seconds, does not reset the guard on successful bootstrap, and never schedules a delayed retry. A subsequent change after the window can trigger a new attempt. If the guard blocks, or storage cannot retain it, the default fallback is This application has changed with a Reload application button. The prompt survives app teardown. A callback returning false supplies its own UI instead.

The callback’s reload() and the fallback button are explicit actions which can bypass the guard; they also record an attempt to guard the next page. Do not call reload() unconditionally from the callback: that defeats loop prevention. Return normally for the guarded automatic path, or call reload() after a deliberate user decision. Do not edit the runtime’s storage marker.

Other failures do not invoke the hook or trigger automatic reload:

Error Recovery
409 app_unavailable Ask the app author to republish against the current semantic layer.
401 unauthenticated Reopen the app and sign in.
403 forbidden Ask the owner to check your publication and data permissions.
404 not_found The publication or its data is no longer available; contact its owner.
409 publication_conflict Contact the app author; the service could not classify the conflict.

Datasource and hosting-location failures also keep their own structured error codes and recovery messages. Retain the request ID when reporting a failure.

A hosted publication binds to the model revision selected when it was published. Model reads and token issuance return 409 app_unavailable if that revision is no longer the current published model or the engine model has changed. Publish the app again against the current model. Historical model execution is not available. This restriction differs from local development, which uses the current engine model.

Publication access is rechecked when issuing or renewing a data token. Already-issued data tokens retain their centrally defined expiry, up to five minutes; unpublishing does not retroactively cancel a token already held by a browser.