Quickstart: a Vite + SDK app

A small Business NXT App scaffolded with Vite and the @business-nxt/app-messaging SDK - it runs an authenticated GraphQL query and reads the user’s selection.

The quickest way to a working Business NXT App: a Vite project plus the @business-nxt/app-messaging SDK. The bundler inlines the SDK, so there is no CDN to allow-list in your CSP. The app reads the current company from the URL, runs a GraphQL query, and asks Business NXT for the current selection.

It also pulls in @vsn-ux/gaia-styles, the Visma design system, so the app looks at home inside Business NXT - import the stylesheet once and apply ga-* classes to your elements.

Scaffold

npm create vite@latest my-app -- --template vanilla-ts
cd my-app
npm install @business-nxt/app-messaging @vsn-ux/gaia-styles
npm install -D @vitejs/plugin-basic-ssl

vite.config.ts

Business NXT serves over HTTPS and loads your app in an iframe, so the dev server must also be HTTPS - an HTTP child is blocked as mixed content.

vite.config.ts
import { defineConfig } from "vite";
import basicSsl from "@vitejs/plugin-basic-ssl";

export default defineConfig({
  plugins: [basicSsl()],
});

The plugin attaches its generated certificate to server.https for you, so no extra server config is needed.

The plugin generates a self-signed certificate on first run. Visit https://localhost:5173/ once in your browser and accept the warning so the iframe can load.

index.html

index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Business NXT App sample</title>
  </head>
  <body>
    <h1>Associates</h1>
    <button id="load" class="ga-button ga-button--primary">Load associates</button>
    <p id="status"></p>
    <ul id="list"></ul>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

src/main.ts

src/main.ts
import "@vsn-ux/gaia-styles/all.css";
import {
  ExecuteGraphQL,
  GraphQLRequestError,
  SendMessage,
} from "@business-nxt/app-messaging";

// Business NXT passes the current context in the iframe URL.
const companyNo = Number(new URLSearchParams(location.search).get("companyNo"));

const status = document.getElementById("status")!;
const list = document.getElementById("list")!;

const GetAssociates = /* GraphQL */ `
  query GetAssociates($companyNo: Int!, $first: Int!) {
    useCompany(no: $companyNo) {
      associate(first: $first) {
        items { associateNo name }
      }
    }
  }
`;

interface Associates {
  useCompany: { associate: { items: { associateNo: number; name: string }[] } };
}

document.getElementById("load")!.addEventListener("click", async () => {
  status.textContent = "Loading...";
  list.replaceChildren();
  try {
    const data = await ExecuteGraphQL<Associates>(
      GetAssociates,
      { companyNo, first: 10 },
      { operationName: "GetAssociates" },
    );
    const items = data.useCompany.associate.items;
    status.textContent = `${items.length} associate(s) in company ${companyNo}`;
    for (const a of items) {
      const li = document.createElement("li");
      li.textContent = `${a.associateNo} - ${a.name}`;
      list.appendChild(li);
    }
  } catch (err) {
    status.textContent =
      err instanceof GraphQLRequestError
        ? err.message
        : "The associates couldn't be loaded.";
  }
});

// Ask Business NXT for the current row selection on startup.
SendMessage({ messageType: "selection-state-request" })
  .then((selection) => console.log("Current selection:", selection))
  .catch((err) => console.warn("No selection yet:", err));

Run it

npm run dev     # local dev server
npm run build   # outputs static files to dist/

Deploy the contents of dist/ to the HTTPS origin your app is served from. Business NXT loads it inside an iframe.

Note

The app only does something when it runs inside Business NXT - that is the window.parent the SDK talks to. Opened standalone, requests stay pending; open it from within Business NXT instead.

Building with React? @business-nxt/app-messaging-react ships the same API plus hooks like useSelection and useEditStatus.

Last modified May 28, 2026