# Develop, debug and test

> How you build an NXM module with the nexxus extension for VS Code or the command line nxm. Problems show up on save, every change runs in a scratch tenant right after, services run under the debugger, and a log per installation shows what happened.

Building a module should feel like building a web app: you save a file and see the change a moment later, in a real nexxus with your sample data. When something goes wrong, you read the reason in a log instead of guessing. And before you hand a package on, you already know whether nexxus will take it.

That is what the extension **nexxus Module** for Visual Studio Code is for. It checks your package on save, installs it in a **scratch tenant** and keeps it current there with every save. It does not know the rules itself: every package goes to the instance, which runs the same checks as an upload. Without VS Code, and for coding agents, the command line [nxm](#without-vs-code-nxm) does the same.

The extension speaks German for now. This page quotes its commands as it shows them; the labels of the app are quoted in English.

<Flow
  steps={[
    { icon: 'folder-plus', title: 'Create a package', text: 'A package that installs as it is: manifest, view, sample data.' },
    { icon: 'plug', title: 'Connect', text: 'To an instance, with your personal API token.', tone: 'neutral' },
    { icon: 'zap', title: 'Dev-Modus', text: 'Every save is installed in the scratch tenant, the open view reloads.', tone: 'cyan' },
    { icon: 'bug', title: 'Debug', text: 'Breakpoints in your services and views, as in any web project.' },
    { icon: 'scroll-text', title: 'Log', text: 'Service calls, migrations and view errors, live and with replay.', tone: 'lavender' },
  ]}
  caption="The development loop with the VS Code extension."
/>

## What you need

- **Visual Studio Code** 1.90 or later.
- **The extension** as the file `nexxus-modules.vsix`. It is not on the Marketplace. You get it from your contact at Systaro; with access to the nexxus GitLab, download the latest [here](https://git.systaro.de/systaro/nexxus/nexxus/-/jobs/artifacts/main/raw/tools/vscode-nexxus/nexxus-modules.vsix?job=vscode-extension). Install it in VS Code from the <Ui>Extensions</Ui> view, menu `...`, <Ui>Install from VSIX...</Ui>, or on the command line with `code --install-extension nexxus-modules.vsix`. A newer file installs the same way over the old one.
- **An instance with scratch tenants.** A nexxus you run from the sources with the Spring profile `development` or `local` has them out of the box. On any other instance its operator switches them on, with `nexxus.modules.scratch.enabled` and `nexxus.modules.scratch.domain-template`, the host per scratch tenant (for example `{slug}.module.example.com`, with DNS and a certificate for those hosts).
- **An administrator account** on that instance, and a [personal API token](/setup/security/#personal-api-tokens) for it.
- **Deno 2**, only when your package has services of its own ([installation](https://docs.deno.com/runtime/getting_started/installation/)).

## Create a package

Open an empty folder and click the cube **nexxus Module** in the activity bar. The sidebar offers <Ui>Neues Paket anlegen</Ui> (create a new package). The command asks for the key (`acme.tasks`: publisher and name, in lower case) and the name in the app, and writes:

<FileTree>
- acme.tasks/
  - 1.0.0/
    - module.json one business object with list and record view, a page of its own, an action
    - CHANGELOG.md
    - ui/
      - offen.html
    - demo/ sample records
</FileTree>

The package installs as it is. The extension then offers to start the Dev-Modus on it. The files are ordinary files: you edit them in the explorer, and the extension checks and installs them on save. How the parts fit together is in [Your first module](/modules/first-module/).

## Connect to an instance

<Steps>

1. Create an API token in the app, as described in [Personal API tokens](/setup/security/#personal-api-tokens). The account has to be an administrator of the tenant: checking and uploading is the same right as uploading in the app.

2. Run the command <Ui>nexxus: Mit Instanz verbinden</Ui> (connect to instance) in VS Code. Give the address the API answers on, for example `https://nexxus.example.com`, and paste the token (`cx_...`).

</Steps>

The address must start with https. Only `localhost`, `127.0.0.1` and `[::1]` also work with http, because every call carries an administrator's token. The extension checks the token before it keeps it, and stores it only in your operating system's keychain, never in settings or files. The status bar shows the instance; clicking it reconnects or disconnects.

:::note[A local instance]
Locally the API answers on `http://localhost:7040` and the app on `http://localhost:7041`. Connect to 7040 and set the setting `nexxus.appUrl` to `http://localhost:7041`, so the extension opens the right pages. On an instance on the network, app and API share one address; leave the setting empty then.
:::

## Problems on save

<Badge text="Available" variant="success" />

The extension checks every `module.json` against the nexxus JSON schema as you type, even without a connection: completion of the fields, their types, and a mistyped `entitys` or a version `1.0` are underlined.

With a connection, every save sends the whole package to the instance (`POST /api/module-packages/validate`). The instance checks it as it checks an upload and, when the tenant has the package installed, also by the rules of an upgrade against the installed version. It stores nothing. Each problem shows in the <Ui>Problems</Ui> panel at its place:

- in `module.json` and in the files under `demo/` at the JSON path the check names, or at the nearest existing part above it when a property is missing,
- on the first line of its file when it has no path (`CHANGELOG.md`, templates),
- on the first line of `module.json` when it has no file.

A problem you fixed disappears on the next save.

:::note[Without the extension]
The schema ships with the nexxus sources, as `backend/src/main/resources/modules/module-runtime/module-package.schema.json`. Copy it next to your packages and tell VS Code in `.vscode/settings.json` that it applies to every `module.json`:

```json
{
  "json.schemas": [
    { "fileMatch": ["**/module.json"], "url": "./module-package.schema.json" }
  ]
}
```
:::

## The Dev-Modus: changes visible on save

<Badge text="Available" variant="success" />

### Scratch tenants

The Dev-Modus works in a **scratch tenant**: a tenant of its own on the instance, empty but for an administrator who stands for you. Nothing you do there touches another tenant. A scratch tenant expires after 7 days, you can extend it up to 30 days after it was created, and each person has at most 5. Once it has expired, the instance deletes it with its packages and data within an hour. In the app a slim bar at the top reads <Ui>Scratch tenant "Tasks" · expires 02.11.2026</Ui>.

The sidebar lists your scratch tenants under **Testmandanten**, with their expiry dates. Its buttons and context menu create one (<Ui>Testmandant anlegen</Ui>), open it in the browser already signed in (<Ui>Testmandant im Browser öffnen</Ui>), extend or delete it, and <Ui>Für den Dev-Modus verwenden</Ui> makes the Dev-Modus take it without asking.

### Start the Dev-Modus

Start it on the version folder in the sidebar (lightning icon), from the context menu of a file in that folder, from the <Ui>Dev-Modus</Ui> entry in the status bar, or with <Ui>nexxus: Dev-Modus starten</Ui>. The first time, the extension asks which scratch tenant to use, or creates one. Then:

1. It packs the folder in memory as a **dev version**, for example `1.2.0-dev`. Your `module.json` on disk stays as it is and still says `1.2.0`.
2. The first time, the instance installs the package with its sample data and gives you every role of the package, without a dialog.
3. After that, the extension installs again on every save of a file in the folder, shortly after the last save. The instance applies the version like an upgrade: your records stay, and migration steps that have not run yet run. So a broken migration shows on the save that broke it.
4. Open views of the module reload in place, on the same page.

While an install runs, the status bar shows a spinner, and its duration afterwards. Problems of the package go to the <Ui>Problems</Ui> panel as above. Clicking the entry opens its menu: the scratch tenant in the browser, the log, tracing an action, debugging a service or the views, resetting the data or ending the Dev-Modus.

A dev version needs no heading of its own in `CHANGELOG.md`: `## [1.2.0]` also covers `1.2.0-dev`. Migration steps go under the release version as always (`"1.2.0"`). A released version stays fixed as before: nexxus refuses the same version with other content. Only a dev version takes new content, and an installed one only through the Dev-Modus.

### When the instance refuses a step

Some things nexxus does not allow in an upgrade, such as a change that would lose data the scratch tenant already has. The extension then shows the reason. Where it helps, it offers <Ui>Daten zurücksetzen</Ui> (reset data): the package is removed with its data and installed again with the sample data; the rest of the scratch tenant's data stays. The package page in the app has the same button, <Ui>Reset data</Ui>, in scratch tenants only.

## Services on your machine

<Badge text="Available" variant="success" />

If your package brings its own [services](/modules/services/) (`services` in `module.json`), their code lives in the version folder under `services/<service>/main.ts`. That folder does not go into the package. <Ui>nexxus: Dienst anlegen</Ui> (create service) writes the start of a service the manifest names: `main.ts` with an event handler and an action, and a `deno.json` that points `@nexxus/module-kit` at the service kit. The extension puts the kit under `.nexxus/kit/` in your workspace folder, with a `.gitignore` of its own.

In the Dev-Modus every service runs on your machine, with `deno serve --watch --inspect` and `PLATFORM_URL` set to the instance. Deno restarts it on every change of its files. The instance reaches it through a tunnel the extension keeps open: the scratch tenant's calls to the service's address go to your machine, without you exposing an address or consenting to a new one. Each service writes to an output channel of its own, <Ui>nexxus Dienst: acme.tasks/deadlines</Ui>, one line per call with status, duration and call ID.

The kit answers `/<name>/events` and `/<name>/actions/<operation>`, with the name `main.ts` gives the kit. The service's address in `module.json` therefore ends in that name, for example `http://localhost:8390/fristendienst`.

When the Dev-Modus is not running, a call fails at once, and the log says the local service is not connected and asks you to start the Dev-Modus in VS Code. Events wait meanwhile; once the tunnel is open, the instance delivers them within a few seconds.

:::note[Without the extension]
A service runs outside nexxus, so it can run on your machine too. Start it yourself and point the installation at it with the installation setting `serviceUrls.<service>`, as described in [Connecting a service on an instance](/modules/services/#connecting-a-service-on-an-instance). The instance then has to reach your machine; a local one can.
:::

## Debugging

<Badge text="Available" variant="success" />

The extension brings the debug type `nexxus`. It starts the Dev-Modus if it is not running yet and hands over to VS Code's JavaScript debugger:

- **A service:** press F5 in a file under `services/<service>/`. The debugger attaches to the service, and again after every restart by `--watch`. Breakpoints in `main.ts` hold.
- **The views:** F5 in a file under `ui/` opens Chrome in the scratch tenant, already signed in. Breakpoints in `ui/*.html` hold.

A `.vscode/launch.json` sets both up:

```json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "nexxus",
      "request": "attach",
      "name": "Debug deadlines",
      "folder": "${workspaceFolder}/acme.tasks/1.2.0",
      "service": "deadlines"
    },
    {
      "type": "nexxus",
      "request": "launch",
      "name": "Debug views",
      "folder": "${workspaceFolder}/acme.tasks/1.2.0"
    }
  ]
}
```

### In the browser's developer tools

A view runs in a sandboxed frame inside the page, so the browser's developer tools work as usual. Its scripts show there under the view's file name, such as `ui/offen.html`, with the same line numbers as in the editor. What the view writes with `console.log` or `console.error`, and errors it does not catch, appear in the console of the page. The frame's address is `about:srcdoc` and its origin is `null`: to run commands in the view's document, choose that frame as the context of the console. A `fetch` to your own server fails on purpose, because the sandbox allows no network; use `nx.records` and the other SDK calls instead. How the view and nexxus talk is in [The protocol, for debugging](/modules/views/#the-protocol-for-debugging).

## See what happened: the log

<Badge text="Available" variant="success" />

Every installation has a log. You find it on its package page under <MenuPath to="Administration > Operations > Modules" />, tab <Ui>Packages</Ui>, in the tab <Ui>Log</Ui>, and in VS Code with <Ui>nexxus: Protokoll</Ui> in a panel beside the editor. Both show new entries as they happen and filter by kind and level. The log holds, with time and outcome:

- install, upgrade, reset, activate, deactivate and uninstall, and every migration step that ran,
- uploads and Dev-Modus installs that nexxus refused, with every problem found,
- every call of a service: action, event delivery, customer portal endpoint, with status, duration, the error and a call ID the service gets too (`X-Nexxus-Correlation-Id`), so you find the same call in your service's output,
- for dev versions only: the request sent and the response of every service call, every declarative action that ran with its evaluated values, and what your views write with `console.error` or do not catch.

For entries that recorded a service call of a dev version, <Ui>Send again</Ui> sends it to the service once more, in the Dev-Modus to the one on your machine: set a breakpoint, send again, step through. The new call shows as an entry of its own.

The log goes back 30 days. The AI assistant can read it for administrators too, for example when you ask it why a delivery fails.

## Trace an action

<Badge text="Available" variant="success" />

<Ui>nexxus: Aktion durchspielen</Ui> (trace action) asks for an action of the package, a record and the inputs the action reads. The extension then asks the instance what the action would do. Nothing runs, nothing is written. The panel shows whether it would run and what it would do, whether you hold the right for it, every condition from `when` with the value in the record, and every template such as `{{record.title}}` with the value it renders to. <Ui>Erneut durchspielen</Ui> asks again with the same inputs, for example after a save. If the instance refused the last save, it traces the last installed state, and the panel says so.

## Testing

**Services** <Badge text="Available" variant="success" />. The service kit comes with a fake platform for tests: `FakePlatform.start()` serves the keys, signs invocation tokens like nexxus, runs the token exchange and keeps records in memory behind the record API. `seed(module, entity, data)` puts records in, `list(module, entity)` reads them back, `stop()` ends it. It lives in the nexxus sources under `module-services/src/kit/testing/`; the tests of the Fristendienst use it, in `module-services/src/fristendienst/service.test.ts`. Run them with `bun test`. The copy of the kit the extension writes does not include it.

**Views** <Badge text="Planned" variant="caution" />. A fake host stands in for nexxus, so a view can be tested with Vitest: you give it records and context, the view runs, and you check what it called and showed. Until then, test views in the Dev-Modus.

## Upload

<Ui>nexxus: Hochladen</Ui> (upload) builds the package, uploads it to the connected instance and opens its package page. The command never installs: an administrator does that on that page, choosing roles and consenting. It uploads the version `module.json` names, never the Dev-Modus's dev version. <Ui>nexxus: Paket bauen</Ui> (build package) only writes the file, as `dist/<key>-<version>.nxm` in the workspace folder.

The extension builds **unsigned** packages. Outside the profiles `development`, `local` and `demo`, an instance only takes them when an administrator of the tenant has switched on <Ui>Allow unsigned packages</Ui> on the tab <Ui>Publishers</Ui>. You build a signed package on the command line, see [Trust the publisher](/modules/publish/#trust-the-publisher).

### On the command line and in CI

Without the nexxus sources, use `nxm validate` and `nxm build` (above) with `NXM_URL` and `NXM_TOKEN` set. With the sources, the Gradle task `validatePackage` checks a package folder the way nexxus does: schema, every rule, the templates and the data files. Run it in `backend/`:

```bash
./gradlew validatePackage -Pdir=/path/to/acme.besucher/1.1.0   # one version folder
./gradlew validatePackage -Pdir=/path/to/acme.besucher         # every version of a package
./gradlew validatePackage -Pdir=/path/to/acme.besucher/package -Pprevious=/path/to/last-release
```

A folder named like a version must sit in `<key>/<version>`; a folder with any other name holds the current version of a package, for example in a repository of its own that publishes versions as Git tags. `-Pprevious` compares with the last release and checks the rules of an upgrade: a higher version number, nothing removed or redefined without a new major version. The task names every folder it checked, lists every problem when there is one, and ends with an error, so a CI job fails. `node backend/scripts/module-package.mjs verify <file>.nxm` checks a built package: hashes, file list and the heading in the changelog.

## Without VS Code: nxm

**nxm** is the same loop as a command line, for any editor, for scripts and for coding agents such as Claude Code, Codex or Cursor. Like the extension it talks to an instance and runs nothing of nexxus on your machine; only your own services run locally, under Deno.

```bash
npm install -g @nexxus-platform/nxm     # Node.js 20.12 or later
nxm init acme.tasks                     # a package that installs as it is, plus agent skills
nxm login https://nexxus.example.com    # once; asks for your personal API token
nxm dev                                 # installs into the scratch tenant on every save
```

The token goes into your operating system's keychain, as with the extension. Both use the same scratch tenants of an instance.

| Command | Does |
|---|---|
| `nxm validate` | Checks locally, then with the rules of the instance; problems print as `file:line:col: message` |
| `nxm dev` | Installs into the scratch tenant on every save and runs the services under Deno with an inspector on port 9229 |
| `nxm apply` | Installs once and ends; `--reset --yes` starts over with the sample data |
| `nxm logs`, `nxm logs -f` | The installation's log, as in the panel |
| `nxm replay <id>` | Sends a recorded service call again |
| `nxm trace <action> --record <id>` | What an action would check and do for a record, without doing it |
| `nxm open` | Opens the scratch tenant, signed in as its administrator |
| `nxm build`, `nxm verify` | Builds the `.nxm` (signed with `--key`) and checks one |
| `nxm upload` | Uploads the version to the connected tenant; an administrator installs it there |

`nxm help <command>` lists the options. With `--json` every command prints one JSON line (`nxm logs` one per entry, `nxm dev` one per event), and the exit code is `0` when done, `1` when it failed, `2` when called wrongly. In CI, set `NXM_URL` and `NXM_TOKEN` instead of logging in.

**Coding agents.** `nxm init` and `nxm skills` write skills to `.claude/skills/` and an `AGENTS.md` that points to them. They teach an agent the package format, the rules nexxus enforces and the loop with nxm, and they carry this documentation and the manifest's JSON schema as references, in the version that matches the installed nxm. An agent then checks and installs its own changes with `nxm validate` and `nxm apply` and reads the result in `nxm logs`. It never needs the token: the person signs in once with `nxm login`.

## Where to go next

<CardGrid>
  <LinkCard title="Your first module" description="A list, a dialog and a page, from the manifest to the installed package." href="/modules/first-module/" />
  <LinkCard title="Views and the view SDK" description="Every SDK function, styling and the rules of the sandbox." href="/modules/views/" />
  <LinkCard title="Module services" description="Tokens, events, the service kit and connecting a service." href="/modules/services/" />
</CardGrid>