Hosted Applications
Last updated:
Read as MarkdownClaude Code builds the application and publishes it; Ekaya hosts it. Your team opens it in a browser without a coding agent, MCP client, or AI subscription.
Publish and share
An Administrator's agent publishes a static application with publish_app_version. Publication makes the version current immediately. The initial publication is Just me, owned by the verified human Administrator who publishes it.
The owner shares the application from Hosted Apps or with set_app_access. Access can remain private, include everyone with access to the Project, or name selected Roles. Sharing with everyone in the Project does not make the application public.
Members see the applications they can open on their Project home. Application access is separate from enabling MCP for their Role.
Act as the viewer
The application's data operations use the signed-in viewer's identity, Role, and permitted Requests. Permission to open an application grants no additional Requests.
A Hosted Application calls the same-origin Data API. It does not need an embedded Data Access Point key or an SDK. The browser runs the application's JavaScript; hosting does not provide a general-purpose server, a background job runner, or an agent runtime.
Grant the application's Requests to the intended Roles as well as sharing the app. Keep credentials and user identifiers out of the app's files as a substitute for the viewer's identity.
Read data from the app
Use the same-origin route POST /api/data/v1/projects/<projectId>/requests/<requestId>/execute. The helper below reads the viewer's catalog to distinguish a Query from an Action. The illustrative call selects an approved Query granted to the viewer's Role with a string parameter named region; replace the identifiers and use your Request's declared parameters and output.
async function runRequest(projectId, requestId, parameters) {
const base = `/api/data/v1/projects/${projectId}`;
const catalogResponse = await fetch(`${base}/catalog`, {
credentials: "same-origin",
});
if (!catalogResponse.ok) throw new Error("Cannot read the catalog.");
const catalog = await catalogResponse.json();
const request = catalog.queries.find((entry) => entry.id === requestId);
if (!request) throw new Error("Request is not in the catalog.");
const response = await fetch(`${base}/requests/${requestId}/execute`, {
method: "POST",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ parameters }),
});
const result = await response.json();
if (response.status === 202 || result.outcome === "indeterminate") {
throw Object.assign(new Error("Effect is uncertain; investigate before another call."), { result });
}
if (!response.ok || result.outcome !== "succeeded") {
throw Object.assign(new Error("Request did not report success."), { result });
}
if (result.mocked === true) {
throw Object.assign(new Error("Answer contains generated mock data."), { result });
}
if (request.kind === "query" && result.stream_status !== "complete") {
const message = result.stream_status === "truncated"
? "Result is partial. Narrow the Request parameters before reading again."
: "Read completeness was not confirmed.";
throw Object.assign(new Error(message), { result });
}
return result;
}
const result = await runRequest("<projectId>", "<requestId>", { region: "emea" });
console.log(result.rows);
The browser sends the viewer's session with the same-origin call. The app sends no key or Authorization header. Do not copy a Data Access Point key from a server-side example into browser code.
A tabular Query answer has this shape; these column names and values are illustrative:
{
"columns": [
{ "name": "customer", "type": "string" },
{ "name": "region", "type": "string" }
],
"rows": [{ "customer": "Example customer", "region": "emea" }],
"row_count": 1,
"execution_id": "<executionId>",
"outcome": "succeeded",
"stream_status": "complete"
}
Rows are objects keyed by column name, not positional arrays. Some Requests return a document instead of a table; follow the Request's contract.
For a Query's buffered JSON rows or document answer, including an empty result, require stream_status === "complete", HTTP success, and outcome: "succeeded" before treating it as complete. Browser fetch cannot read the terminal X-Ekaya-Stream-Status trailer; the body field supplies that completeness signal for JSON. A missing field, truncated, failed, timeout, or any unknown value means completeness has not been established. Neither row_count nor HTTP 200 alone proves it. Also check mocked: a mock reports stream_status: "complete" beside mocked: true, but it is generated data and proves nothing about a real execution.
A read's effective row ceiling includes the caller's own limit, alongside the deployment and approved contract limits. Reaching it can return valid rows with stream_status: "truncated"; that is a partial result, not a failed execution. This example omits limit because it needs a completeness verdict. On truncated, tell the viewer the result is partial, avoid presenting it as a full table or computing totals from it, and ask them to narrow the Request's parameters before reading again.
The catalog's kind: "action" identifies an Action. Its JSON summary carries rows_affected, batch_size, row_count, execution_id, and outcome, with returned columns and rows or a document when applicable. It has no stream_status, even when it returns data. Check HTTP success, the outcome, and mocked without applying the read completeness rule. HTTP 202 or an indeterminate outcome means the effect is uncertain: follow Errors and investigate before considering another call.
If fetching or JSON parsing fails, or another acceptance check fails, show the viewer an error and keep any returned execution identifier and diagnostic fields. Keep mock data clearly labeled if you use it while building; this example rejects it. The helper makes one execution call and does not retry.
Application records
An application can use an existing datasource or, where enabled, a Managed Project Database for its own records. Storage requires deployment enablement and an Administrator's installation and authorization of the Managed Project Database solution. MCP does not purchase or install it.
For records belonging to a person, the approved Requests must use the verified person binding correctly in their SQL predicates and inserted rows. The caller omits the bound parameter; Ekaya supplies the viewer's verified identity. Binding a parameter alone does not make SQL isolate each person's records.
See Limits for the published database capacities.
Versions and evidence
Restoring an application version restores app files, not business data. It does not reverse changes an Action made to a database or service.
Successful static page views are not audited as data operations. Do not treat the data-operation audit trail as a complete record of page views or clicks. For a data operation whose effect is uncertain, follow Errors.