Quickstart: a Vite + SDK app
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-sslvite.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.
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
<!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
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.
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.