# Hosted Applications

Last updated: 2026-10-11

Claude 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.

```js
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:

```json
{
  "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](/docs/call-an-approved-request/).

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](/docs/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](/docs/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](/docs/errors/).
