Building a Site
Build a hosted Site in Nexus with AI through the MCP — author pages, wire live data via saved reports and the SDK, and style it with the built-in design system, all without writing a token.
A Nexus Site is a set of static web pages (HTML/JS/CSS/images) hosted by your Nexus server at /sites/{slug}/ and served from your own Azure tenant. Pages call the existing Nexus REST API as the viewing user — bounded by that user's RBAC — to list bridges, run reports, and start pipelines. This page is the build contract: the data model, the authoring tools, the CSP you must design within, the SDK, styling, and version control. Most builders drive every step by directing an AI assistant through the Nexus MCP tools, and the whole lifecycle is operable that way end to end.
For the security model and render modes, read Sites Overview first. This page assumes you know a Site runs as the viewer and cannot exceed their permissions.
The content model — config vs assets
A Site has two kinds of content, both authored through one unified tool, uql_save with kind="site", distinguished by whether you pass a sub_path:
Config (
sub_pathomitted) — a JSON object of Site metadata:{ "name": "Sales Dashboard", "description": "Live pipeline + report view", "status": "draft", // "draft" | "published" "default_document": "index.html", // served for the bare /sites/{slug} path "connect_src_extra": [] // admin break-glass extra CSP connect-src hosts }Saving config creates the Site (if it does not exist) or updates its settings. Config is not version-controlled.
Assets (
sub_pathset to a file path) — the actual files.contentis the asset text. Inline text assets are ≤ 1 MB and are version-controlled. The Site must exist before you add assets.
The Site id is the URL slug: it must match ^[a-z0-9][a-z0-9-]{0,62}$ — lowercase letters, digits, and hyphens, starting with a letter or digit, up to 63 characters.
Asset paths and MIME
Asset paths are normalized and traversal-guarded: no .. segments, no leading /, no empty segments, no backslashes or null bytes; each segment matches [A-Za-z0-9._-]+. Subdirectories are fine (js/app.js, assets/logo.svg). The content type is inferred from the file extension and served verbatim with nosniff, so the extension must be correct. Recognized types include html htm css js mjs json svg png jpg jpeg gif webp ico bmp woff woff2 ttf otf pdf wasm xml txt map. Text types (html/css/js/json/svg/xml/txt) are stored inline; everything else is stored as a blob. A request for an empty or directory path serves the Site's default_document; an unknown slug or path returns a clean 404.
The build flow
You can drive every step from an AI assistant connected to the Nexus MCP. The loop is:
- Create the Site — save config with a name and a
default_document:uql_save(kind="site", id="sales", content='{"name":"Sales Dashboard","default_document":"index.html"}') - Add each text asset — pass
sub_pathand the file text. Validate first withuql_validate:uql_validate(kind="site", sub_path="index.html", content="<!doctype html>...") uql_save(kind="site", id="sales", sub_path="index.html", content="<!doctype html>...") uql_save(kind="site", id="sales", sub_path="app.js", content="...") uql_save(kind="site", id="sales", sub_path="style.css", content="...") - Add binary or large files (images, fonts, anything over the 1 MB inline text limit) with
uql_upload_file, the proxy-native byte-ingress — the desktop proxy streams the local file server-side, so the bytes never pass through the model context:
When no desktop proxy is available,uql_upload_file(local_path, dest_kind="site", dest_id="sales", sub_path="assets/logo.png")uql_put_site_file(content_base64=..., content_type=...)is the base64 fallback (10 MB cap). Never base64 a text asset — that goes ascontentviauql_save. - Publish — flip the status:
uql_save(kind="site", id="sales", content='{"status":"published"}') - The Site is live at
/sites/sales/. Inspect it withuql_get(config + asset manifest, or one asset viasub_path), list Sites withuql_list(kind="site"), and remove a Site or one asset withuql_delete.
Site writes are human-only and require the site:manage permission — an app token or API key cannot create or edit a Site, only a signed-in human (or an agent running under a human's session). Reads require site:read. To keep a Site organized, you can file it into a resource group at create time with resource_group_id="<rg>" on the config save, or later with uql_assign_resource_group; resource groups are organizational folders, never a permission boundary.
Version control
Text assets are committed to your tenant's git-backed version control, so every change to a Site's HTML/JS/CSS has history you can review and roll back:
uql_history(kind="site", id="sales", sub_path="app.js")— the commit log for one asset.uql_diff(...)— what changed between versions.uql_restore(...)— roll an asset back to an earlier version.
Two caveats: config is not versioned (a status/name/default_document change is not restorable via uql_restore), and blob assets are not versioned (images and fonts have no history). Only inline text assets are committed. See Version Control for how the substrate works across pipelines, reports, sites, and skills.
Getting data onto the page
Pages get data through the first-party SDK, served by Nexus at /sites/_sdk/nexus-client.js. The SDK owns authentication internally and never exposes a token to your page code.
<script src="/sites/_sdk/nexus-client.js"></script>
const nexus = await Nexus.connect(); // probe-only: detects desktop vs browser mode
const me = await nexus.whoami(); // claims only (name/email/roles), never a token
const { bridges } = await nexus.listBridges(); // GET /api/bridges
const result = await nexus.runReport("monthly-sales", { /* parameters */ });
The SDK runs the same code whether the page is opened in the Nexus desktop app or a standalone browser; it abstracts the difference. In desktop mode the loopback proxy injects the keyring bearer and the page is always authenticated. In browser mode the SDK runs AAD PKCE and keeps the token in a module-private closure, establishing a refresh-stable HttpOnly session cookie so reloads stay signed in.
Nexus.connect() is probe-only — it never force-redirects on load. On the first /api/* call that returns 401, or when you explicitly call Nexus.signIn(), the browser flow escalates to the interactive Microsoft login. A robust page therefore renders a static snapshot first, then connects and loads live data, and on an unauthenticated response swaps in a Sign in button wired to Nexus.signIn() — so the page is never blank. Sign-out is Nexus.signOut(), which expires the session cookie; the shared nav's account menu already wires it.
Data comes from saved reports
The SDK's surface is the /api/* REST routes — there is no direct query call. To show data on a page, author a saved report and have the page run it with nexus.runReport(reportId, params). The report executes as the viewing user, so each visitor sees only the rows their permissions allow. This is also the only way to surface internal-workspace data on a page: author a report whose query targets the workspace bridge, then run it from the Site — a page cannot query the workspace directly. See Reports.
Styling with the design system
Nexus ships a first-party, self-hosted design system under /sites/_sdk/ so your pages look polished by default and stay CSP-legal (nothing is loaded from a CDN). Link the theme and set the light theme:
<html lang="en" data-theme="light">
<link rel="icon" href="/sites/_sdk/stingray-logo.svg" type="image/svg+xml" />
<link rel="stylesheet" href="/sites/_sdk/stingray.css" />
The theme is a clean, white, Stingray-blue skin built on Pico CSS. You get two ways to use it:
- Classless — write semantic HTML (
<h1>,<table>,<form>,<button>) and it is styled automatically, no classes needed. - Brand classes — opt into
nx-*classes for chrome:
| Class | Purpose |
|---|---|
.nx-container / .nx-container--wide |
Centered content column (1000px, or 1400px for dashboards). |
.nx-hero / .nx-dashhead |
Marketing hero, or a slim dashboard header. |
.nx-grid / .nx-grid--kpi |
Responsive card grid; the KPI variant stretches cards to fill the row. |
.nx-card |
Rounded, soft-shadowed card with .nx-card__title / .nx-card__desc. |
.nx-badge |
Status pills (--published, --draft, --live). |
An optional shared nav bar (/sites/_sdk/stingray-nav.js) adds a sticky top bar with the logo, a Home link, and a sign-in/account control in one line. Load the SDK before the nav so the account control can read identity. The theme, nav, logo, and SDK are served by Nexus — you reference them by URL and never upload them.
The security boundary you author within
Every Site page is served under the strict Sites CSP, so design your page to live inside it:
- No inline scripts and no third-party CDNs. All JavaScript lives in
.jsfiles you serve from the same origin; an inline<script>block will be silently blocked (a common cause of "my JS never runs"). Self-host everything. - No external images. Use same-origin assets or
data:URIs — no hotlinking or remote avatars. - Inline
<style>is allowed, as is an externalstyle.css, but not an external CSS CDN — self-host. - Network egress is locked to your Nexus origin plus Microsoft sign-in. The page can only reach its own data API, which means there is no channel to leak data off-tenant — even though it reads live, permissioned data.
To reach a third-party service, do it server-side in a pipeline or connector and have the page call Nexus, rather than widening the page's network access. Widening connect_src_extra is an admin break-glass (SSRF-validated, recorded as a security event), not a default.
One authoring habit worth keeping: render API values with textContent or DOM construction rather than innerHTML. The page already runs as the user, but you still should not render data values as HTML.
Authoring with AI
Because create, validate, save, upload, publish, and version are all MCP tools, an AI agent connected to the Nexus MCP can build and deploy a working internal app end to end — write the HTML/JS/CSS, validate each asset, upload images, and publish — with no separate build or deploy step and no token ever leaving the server. The agent operates under the connecting human's identity and permissions, so it can only author Sites the human is allowed to. See Connecting AI Agents.
Checklist before publishing
data-theme="light"set andstingray.csslinked (unless fully custom).- Live data via the SDK, with a static snapshot fallback and a
Nexus.signIn()path. - No inline scripts, no CDNs, no remote images — everything self-hosted or
data:. - Text assets validated with
uql_validate; binaries uploaded viauql_upload_file. - Status set to
published.
See also
- Sites Overview — the security model, render modes, and the full Sites CSP.
- Reports — the saved queries a page runs.
- MCP Server — the authoring tool surface.
- Connecting AI Agents — how an agent authors and deploys.
- Version Control — history, diff, and restore.
- Roles & RBAC — the
site:read/site:managepermissions.