> For the complete documentation index, see [llms.txt](https://v2.dataos.info/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://v2.dataos.info/consume/v1/recipes/deploy-a-data-application-on-a-perspective.md).

# Deploy a Data Application on a Perspective

You have a governed query a team keeps re-running or asking an analyst for (a KPI breakdown, a channel comparison, a funnel step) and you want them to open it themselves instead. This recipe turns that query into an app: save it as a Perspective, deploy a Data Application against its Data API, and confirm the app inherits governed access from the product itself rather than needing its own. The journey is save as Perspective → grab its Data API → build the app → deploy → confirm it landed on the product.

{% hint style="info" %}
**Prerequisites:** a business user working in [Studio](/consume/v1/evaluate/query-in-studio.md)'s UI (not raw API calls), and a container image for the app.
{% endhint %}

## Steps

### 1. Run the query in Studio and save it as a Perspective

Run the query in Studio:

![Running a channel-performance query against the orders Data Product in Studio](/files/NpQ26oJvyy7svTdFgpst)

Then open the **Save as Perspective** dialog: name it, set visibility, and turn on **Auto refresh** so the app you build on this never needs its own refresh logic: the Perspective updates itself when the upstream data changes.

![Save as Perspective dialog with name, visibility, and Auto Refresh toggle](/files/ze4amrfAjXnYcbcrFLO8)

The saved Perspective gets its own page with **Data**, **Columns**, **Lineage**, and **Activate** tabs:

![Saved Perspective detail page showing the Data tab with channel-level results](/files/jgEk7eDrhjA97A9ikqem)

See [Save as Perspective](/consume/v1/evaluate/query-in-studio/save-as-perspective.md).

### 2. Grab the auto-provisioned Data API

Saving the Perspective already provisions its Data API; there is no separate step to "expose" it. Open the new Perspective and select **Activate**: it shows the endpoint, the required `Authorization` header, and the response schema.

![Perspective Activate tab showing the GET endpoint, Authorization header, and response schema](/files/iBlk4CE8oIdvuF2OGIfn)

```
GET https://$BASE_URL/api/v1/perspectives/channel-performance-monthly/result?format=json
Authorization: Bearer <api-token>
```

The app only needs to call this endpoint and render the result however fits (table, chart, metric tiles); it never queries the semantic layer directly. Keep the FQDN, tenant, product name, token, and Perspective slug as environment variables so the same app image works against any product.

### 3. Build the app against that endpoint

From here, the work shifts from Studio's UI to code: building, containerizing, and deploying the app takes developer tooling (a language runtime, Docker, `dataos-ctl`), so bring in a developer teammate for steps 3 and 4 if that isn't your own skill set. The app itself is ordinary code, nothing DataOS-specific beyond calling the endpoint from step 2. Use the [Python SDK](/consume/v1/activate/apis/sdk/python-sdk.md) instead of raw HTTP calls; a minimal example using Streamlit:

{% code title="app.py" expandable="true" collapsedlinecount="5" %}

```python
# app.py
import os
import pandas as pd
import streamlit as st
from vulcan_sdk import VulcanClient, VulcanError

FQDN = os.environ["VULCAN_FQDN"]
DP_NAME = os.environ["VULCAN_DP_NAME"]
AUTH_TOKEN = os.environ["VULCAN_AUTH_TOKEN"]
TENANT = os.environ["VULCAN_TENANT"]
PERSPECTIVE_SLUG = os.environ["PERSPECTIVE_SLUG"]

st.title("Channel Performance")

client = VulcanClient(fqdn=FQDN, dp_name=DP_NAME, access_token=AUTH_TOKEN, tenant=TENANT)

try:
    result = client.perspectives.get_result(PERSPECTIVE_SLUG)
except VulcanError as e:
    st.error(f"Could not load perspective '{PERSPECTIVE_SLUG}': {e.status_code} {e.detail}")
    st.stop()

df = pd.DataFrame(result.rows, columns=result.cols)
st.bar_chart(df.set_index(df.columns[-1])[df.columns[0]])
st.dataframe(df, use_container_width=True)
```

{% endcode %}

Download the [Python SDK wheel](/consume/v1/activate/apis/sdk/python-sdk.md) and place it alongside `app.py`; `pip install` can install directly from a local wheel file.

{% code title="requirements.txt" expandable="true" collapsedlinecount="3" %}

```
# requirements.txt
streamlit
pandas
vulcan_sdk-<version>-py3-none-any.whl
```

{% endcode %}

{% code title="Dockerfile" expandable="true" collapsedlinecount="5" %}

```dockerfile
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt vulcan_sdk-<version>-py3-none-any.whl ./
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 8501
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0", "--server.headless=true", "--server.baseUrlPath=channel-performance-dashboard"]
```

{% endcode %}

`--server.baseUrlPath` must match the app's ingress path (step 4); Streamlit uses it to route its websocket connection correctly behind a path-prefixed reverse proxy. Without it, the app loads a blank page: the HTML shell arrives, but the websocket that renders the actual content connects to the wrong path.

Build and push it to a registry your compute target can pull from, targeting the cluster's architecture explicitly:

```bash
docker buildx build --platform linux/amd64 -t <your-registry>/<image-name>:<tag> --push .
```

DataOS compute typically runs on `linux/amd64` nodes. Without `--platform linux/amd64`, `docker build` defaults to your host's architecture: on an Apple Silicon Mac that's `linux/arm64`, which won't run on the cluster.

`VULCAN_FQDN`, `VULCAN_DP_NAME`, `VULCAN_TENANT`, `VULCAN_AUTH_TOKEN`, and `PERSPECTIVE_SLUG` are read from the environment, not hardcoded, so the same image works for any Perspective on any product, as long as the `app` manifest's `envs` point it at the right one.

### 4. Deploy the Data Application manifest

Adapt the `app` Resource template to this product, wiring the Perspective's slug and the product's Data API base URL into the environment:

{% code title="app.yaml" expandable="true" collapsedlinecount="8" %}

```yaml
version: v1alpha
type: app
name: channel-performance-dashboard
owner: <your-dataos-username>
spec:
  compute: <compute-name> # a Compute resource your tenant already has; see https://v2.dataos.info/operate/tenant-admin/create-compute
  runAsUser: <your-dataos-username>
  replicas: 1
  resources:
    requests: {cpu: 250m, memory: 256Mi}
    limits: {cpu: 500m, memory: 512Mi}
  service:
    type: python
    image: <your-registry>/<image-name>:<tag>  # the image built and pushed in step 3
    command: [streamlit]
    arguments: [run, app.py, --server.port=8501, --server.address=0.0.0.0, --server.headless=true, --server.baseUrlPath=channel-performance-dashboard]
    port: 8501
    envs:
      VULCAN_FQDN: "<DATAOS_FQDN>"
      VULCAN_DP_NAME: "<resource-name>"
      VULCAN_TENANT: "<TENANT>"
      VULCAN_AUTH_TOKEN: "${VULCAN_AUTH_TOKEN}"
      PERSPECTIVE_SLUG: "channel-performance-monthly"
    http:
      path: /channel-performance-dashboard
      stripPath: false
      noNetworkAuthorization: true
      ingress:
        enabled: true
        path: /channel-performance-dashboard
        stripPath: false
        appDetailSpec: channel-performance-dashboard
  app:
    domain: <business-domain>
    owners:
      - <your-dataos-username>
    products:
      - <data-product-name>
    link:
      title: Open dashboard
      url: "https://<DATAOS_FQDN>/channel-performance-dashboard"
```

{% endcode %}

Apply it:

```bash
dataos-ctl resource apply -f app.yaml
```

For a private image, add `spec.service.imagePullSecret` referencing a [Container registry Secret](https://v2.dataos.info/references/resources/secret/container-registries) resource.

`noNetworkAuthorization: true` is unrelated to whether your image is public or private — it controls whether opening the app's dashboard link requires a separate access grant. [Bifrost](https://v2.dataos.info/operate/access-model/access-control) (DataOS's access-control system) has no **Can Use** permission for Apps the way it does for other resource types, so without this setting, not even the app's owner can open the link. Set `noNetworkAuthorization: true` unless you deliberately want the dashboard access-gated.

If you change `noNetworkAuthorization` on an already-deployed app, the update may not take effect; delete and re-apply the app Resource to be sure the new setting is picked up.

### 5. Confirm it landed on the product

Open the product's catalog page. The app appears as its own tab, linked under `spec.app.products`: the app didn't get built next to the product, it got built into it.

## Troubleshooting

<table><thead><tr><th width="272.60467529296875">Issue</th><th>Resolution</th></tr></thead><tbody><tr><td>The Data API returns 401/403</td><td>The identity set in <code>runAsUser</code> needs a <strong>Can Use</strong> grant on the product itself (not just the Perspective) — owning a resource does not automatically grant Use on it. See <a href="https://v2.dataos.info/operate/access-model/access-control">permission levels</a> for how to grant Use.</td></tr><tr><td>The app shows stale data</td><td>Confirm <strong>Auto refresh</strong> was enabled when the Perspective was saved; without it, the Perspective only updates when re-run manually.</td></tr><tr><td><code>dataos-ctl resource apply</code> fails on image pull</td><td>For a private image, add <code>spec.service.imagePullSecret</code> pointing to a <a href="https://v2.dataos.info/references/resources/secret/container-registries">Container registry Secret</a>.</td></tr><tr><td>The pod crashes or never starts after a successful image pull</td><td>Rebuild with <code>docker buildx build --platform linux/amd64 ... --push</code>. An image built without an explicit platform defaults to the build machine's architecture (<code>linux/arm64</code> on Apple Silicon), which will not run on <code>linux/amd64</code> compute.</td></tr><tr><td>Opening the dashboard link returns 401/403 in the browser, even as the app's owner</td><td>Add <code>noNetworkAuthorization: true</code> under <code>spec.service.http</code> and re-apply. Bifrost (DataOS's access-control system) has no per-user <strong>Can Use</strong> grant for Apps, so this setting is currently the only way to make the dashboard link openable to anyone besides the deployer. If it was already deployed without this setting, delete and re-apply rather than just updating.</td></tr><tr><td>The app loads a blank or endlessly loading page</td><td>Add <code>--server.baseUrlPath=&#x3C;ingress-path></code> to the Streamlit launch arguments, matching <code>spec.service.http.path</code>. Without it, Streamlit's websocket connects to the wrong path and the page never renders past the empty HTML shell.</td></tr><tr><td>The app doesn't appear on the product's catalog page</td><td>Check that <code>spec.app.products</code> in the manifest lists the correct data product name, and re-apply.</td></tr><tr><td>The pod starts but crashes with a filesystem permission error</td><td>Apps run under stricter runtime permissions; if the image writes to a path like <code>/var/cache</code> or <code>/run</code>, add it under <code>spec.service.workingDirs</code> (a list of <code>id</code>/<code>directory</code> pairs) and re-apply.</td></tr></tbody></table>

## Outcome

You turned a one-off query into an app a team can open directly, with governed access and freshness inherited from the product rather than re-implemented. This app already pulls the Perspective via the [SDK](/consume/v1/activate/apis/sdk.md); the same Perspective can also be queried directly over raw HTTP or shared by its `slug` URL: three activation paths off one governed artifact.

## References

* [Save as Perspective](/consume/v1/evaluate/query-in-studio/save-as-perspective.md): the full Perspective save flow
* [APIs](/consume/v1/activate/apis.md): the Data API surface this app is built on
* [Python SDK](/consume/v1/activate/apis/sdk/python-sdk.md): the client this app uses to call the Perspective
* [Container registry Secret](https://v2.dataos.info/references/resources/secret/container-registries): for private image pulls


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://v2.dataos.info/consume/v1/recipes/deploy-a-data-application-on-a-perspective.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
