# NomiEsb — Full Documentation > Web-based management console for building, deploying, and operating .NET integration modules on Kubernetes. The documentation below covers product overview, installation, configuration, and a page-by-page user guide. Source files are published at https://nomiesb.com/content/.md and rendered at https://nomiesb.com/docs/. --- # NomiEsb Documentation NomiEsb is a web-based management console for building, deploying, and operating .NET integration modules on Kubernetes. ![NomiEsb Dashboard](images/dashboard.png) ## In this guide The documentation is organised into five sections. Use the sidebar to navigate, or follow the links below. ### Getting Started What NomiEsb is, who it's for, and the concepts you need to know before installing. - [What is NomiEsb?](/docs/gs-introduction) - [Core concepts](/docs/gs-concepts) - [Roles and access](/docs/gs-roles) ### Installation Step-by-step administrator guide to install NomiEsb on a server, register the Entra ID application, prepare the database, and apply the license. - [Prerequisites](/docs/install-prerequisites) - [Database setup](/docs/install-database) - [Entra ID application and groups](/docs/install-entra-id) - [Encryption key and license file](/docs/install-secrets) - [appsettings.json reference](/docs/install-configuration) - [Running and verifying NomiEsb](/docs/install-run) ### Configuration Everything you configure in the console after installation: license, app settings, integrations, environments, users, and tokens. - [License](/docs/config-license) - [App settings](/docs/config-app-settings) - [Environments, systems and tags](/docs/config-environments) - [Integrations](/docs/config-integrations) - [Users and CLI licenses](/docs/config-users) - [API and MCP tokens](/docs/config-tokens) ### User Guide Page-by-page walkthrough of every screen in the console. - [Layout and navigation](/docs/guide-overview) - [Dashboard](/docs/guide-dashboard) - [Solutions, modules and libraries](/docs/guide-solutions) - [Dependency compliance and licenses](/docs/guide-dependencies) - [SBOM](/docs/guide-sbom) - [Release notes generation](/docs/guide-release-notes) - [Dependency vulnerabilities](/docs/guide-vulnerabilities) - [CI pipeline history](/docs/guide-ci-pipeline) - [Clusters and hosts](/docs/guide-clusters) - [Host configuration comparer](/docs/guide-host-compare) - [Settings](/docs/guide-settings) - [Profile, applications and notifications](/docs/guide-account) - [NomiEsb Lens](/docs/guide-lens) ### Infrastructure Setup Standing up the external systems NomiEsb talks to: CI/CD pipelines on each DevOps platform, ArgoCD on Kubernetes, container registries, and the NuGet feed where modules publish. #### Azure DevOps - [Pipeline variables](/docs/infra-ado-pipelines) - [NuGet feed](/docs/infra-ado-nuget-feed) - [Kubernetes environment](/docs/infra-ado-k8s-environment) - [Connect ArgoCD to repo](/docs/infra-ado-argocd-repo) #### GitHub - [Pipeline variables](/docs/infra-github-pipelines) - [NuGet feed](/docs/infra-github-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-github-argocd-repo) #### GitLab - [Pipeline variables](/docs/infra-gitlab-pipelines) - [NuGet feed](/docs/infra-gitlab-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-gitlab-argocd-repo) #### Bitbucket - [Pipeline variables](/docs/infra-bitbucket-pipelines) - [NuGet feed](/docs/infra-bitbucket-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-bitbucket-argocd-repo) #### ArgoCD - [Cluster setup](/docs/infra-argocd-cluster) #### Kubernetes - [Container registry auth](/docs/infra-k8s-registry) --- # What is NomiEsb? NomiEsb is the management console for the Nomirun platform — a development and deployment system for .NET integration services running on Kubernetes. It is delivered as a single self-contained executable (or container image) that you host on your own infrastructure. The console is the place where teams: - organise their .NET **modules** and **libraries** into deployable **solutions**, - declare the **Kubernetes clusters** and **hosts** that run those modules, - connect **CI/CD platforms** (Azure DevOps, GitHub, GitLab, Bitbucket) and **ArgoCD** for GitOps delivery, - manage **users, roles, integrations, tags and licenses**, - and monitor **builds, deployments, and audit history** in one place. NomiEsb is the web tier. The companion **Nomirun CLI** is a separate tool that developers install on their workstations to scaffold and build modules locally; the console manages, schedules, and tracks the work the CLI produces. ## What NomiEsb is not NomiEsb does not replace your CI/CD platform, your Kubernetes control plane, or your Git provider — it orchestrates them. Builds run in your Azure DevOps / GitHub / GitLab / Bitbucket pipelines. Deployments are applied by ArgoCD against your own clusters. NomiEsb stores the configuration, triggers the actions, and shows you the status. ## How a typical day looks Once an administrator has installed NomiEsb, applied the license, and connected the integrations, a normal workflow is: 1. A **Developer** opens **Solutions → Quick Start**, fills in a solution name, picks a Git repo, lists the modules to create, and clicks initialise. NomiEsb pushes the solution template, creates the pipelines, and the first build runs. 2. A **DevOps** user opens **Clusters → Quick Start**, names the cluster, adds hosts, and assigns the new modules to them. 3. NomiEsb shows the live deployment state on the **Dashboard**, pulled from ArgoCD. 4. **Notifications** fire if a build fails, an integration token is about to expire, or a host becomes unhealthy. 5. The **Audit Trail** records every change for compliance. ## Next - [Core concepts](/docs/gs-concepts) — the six terms used throughout the console. - [Roles and access](/docs/gs-roles) — how Admin, Developer and DevOps map to Entra ID security groups. --- # Core concepts These six terms appear everywhere in the console. Read them once before going further — every page assumes you know what they mean. | Concept | Description | |---------|-------------| | **Module** | A single .NET project that does one job (an integration, a worker, an API). Versioned, packaged as NuGet, deployed as a container. | | **Library** | A reusable .NET project shared across modules. Same lifecycle as a module but consumed instead of deployed. | | **Solution** | A logical grouping of modules and libraries backed by a single Git repository. The unit of CI/CD initialisation. | | **Cluster** | A Kubernetes cluster definition in NomiEsb. Holds one or more hosts and is tagged with an environment and an optional system. | | **Host** | A deployable process running inside a cluster. A host runs one or more modules and can be extended with **extensions**. | | **Extension** | An optional capability (caching, telemetry, custom middleware) added to a host on top of its modules. | ## How they relate ``` Solution (Git repo) ├── Module A ├── Module B └── Library C Cluster (Kubernetes) ├── Host "api-gateway" ── Module A, Module B + Extensions └── Host "worker" ── Module B + Extensions ``` Solutions describe **what gets built**. Clusters describe **where it runs**. The same module can be deployed to multiple hosts in multiple clusters. ## How they map to console pages | Concept | Where you manage it | |---------|---------------------| | Solution | **Solutions** → expand a row, or **Solutions → Quick Start** for new solutions | | Module | **Solutions → Module Details** (deep link from any module table) | | Library | **Solutions → Library Details** | | Cluster | **Clusters** → expand a row, or **Clusters → Quick Start** | | Host | **Clusters** → expand a cluster, or **Host Details** | | Extension | Inside a host card on **Clusters** or **Host Details** | ## Next - [Roles and access](/docs/gs-roles) — who can do what. - [Installation prerequisites](/docs/install-prerequisites) — what you need before installing. --- # Roles and access NomiEsb has exactly three roles. Every page is gated by them, and every user is assigned to one through an Entra ID security group. | Role | Typical user | Responsibilities | |------|--------------|------------------| | **Admin** | Platform owner / IT lead | Installs and configures the system; manages users, integrations, license, audit trail. Can see everything. | | **Developer** | Backend / integration developer | Owns solutions, modules, and libraries. Creates and initialises new solutions through the Quick Start wizard. | | **DevOps** | Operations / SRE | Owns clusters, hosts, integrations, and deployments. Builds and deploys modules to hosts. | A user with no Nomirun group membership can sign in but will see only an unauthorised screen. ## What each role can see | Page | Admin | Developer | DevOps | |------|:-----:|:---------:|:------:| | Dashboard | ✓ | ✓ | ✓ | | Solutions, Module Details, Library Details | ✓ | ✓ | — | | Solution Quick Start | ✓ | ✓ | — | | Clusters, Host Details | ✓ | — | ✓ | | Cluster Quick Start | ✓ | — | ✓ | | Settings → Integrations | ✓ | — | ✓ | | Settings → App, Environments, Systems, Tags, Users, Roles, License | ✓ | — | — | | Audit Trail | ✓ | — | — | | Profile, Applications, Notifications, Documentation | ✓ | ✓ | ✓ | ## How roles are assigned Roles come from membership in three Microsoft Entra ID security groups. NomiEsb matches the group **display name** first; if your tenant uses different names, the **object ID** is matched as a fallback (configured under `NomirunGroups` in `appsettings.json`). | Entra ID group name | NomiEsb role | |---------------------|--------------| | `Nomirun_Admin` | Admin | | `Nomirun_Developer` | Developer | | `Nomirun_DevOps` | DevOps | When a user signs in, NomiEsb queries Microsoft Graph for the user's groups, picks the highest-precedence role (Admin > DevOps > Developer), and persists it to the database. Provisioning the groups is part of the [Entra ID setup](/docs/install-entra-id). ## Next If you are an administrator preparing to install NomiEsb, continue to [Installation prerequisites](/docs/install-prerequisites). Otherwise, skip ahead to the [User Guide](/docs/guide-overview). --- # Prerequisites This section starts the administrator-facing installation. Gather everything below before going further; each later page assumes the items here are in place. ## Hardware and OS NomiEsb is shipped as a self-contained binary, so you do not need to install .NET on the server. | Requirement | Notes | |-------------|-------| | **Operating system** | Windows 10/11, Windows Server, any modern Linux distribution, or macOS. x64 or arm64. | | **CPU / RAM** | 2 vCPU and 2 GB RAM are enough for small teams. The console is mostly idle between requests. | | **Disk** | A few hundred megabytes for the binary and assets. Logs and audit data live in the database. | | **Reachable hostname** | A DNS name and TLS certificate the browser can reach (for example `https://app.nomiesb.example.com`). Required by Entra ID for the OIDC redirect URI. | NomiEsb does **not** terminate TLS. Run it behind a reverse proxy (NGINX, IIS, Azure Front Door, …) that provides the certificate and forwards to NomiEsb's listening port. ## External services | Requirement | Used for | |-------------|----------| | **Database** | PostgreSQL 14+ **or** Microsoft SQL Server 2019+. NomiEsb creates and migrates its own schema. See [Database setup](/docs/install-database). | | **Microsoft Entra ID tenant** | Sign-in and role assignment. You will register one application and three security groups. See [Entra ID setup](/docs/install-entra-id). | | **License file** | A `nomiesb.license` issued by Nomirun. The application refuses to start in release mode without one. See [Encryption key and license](/docs/install-secrets). | | **Outbound network** | Access to `login.microsoftonline.com`, `graph.microsoft.com`, your CI/CD platform APIs, and your ArgoCD server. | ## Optional, recommended - A reverse proxy with TLS (already mentioned). - An SMTP server, if you want notification emails. - Access to your Kubernetes clusters and an ArgoCD installation, if you want NomiEsb to drive deployments. ## Next When you have the items above, continue to [Database setup](/docs/install-database). --- # Database setup NomiEsb supports two database providers, selected by the `Database:Provider` setting in `appsettings.json`. The default is **PostgreSQL**. NomiEsb creates and migrates its own schema on first start — there is no separate `dotnet ef database update` step for operators. ## PostgreSQL 1. Create an empty database, for example `nomirun-esb`. 2. Create a user with full rights on that database. 3. Note the connection string in this format: ``` Server=db.example.com;Port=5432;Database=nomirun-esb;User ID=nomirun;Password=...;Include Error Detail=false ``` In `appsettings.json` no `Database:Provider` value is needed (PostgreSQL is the default). Set the connection string under `ConnectionStrings:DefaultConnection`. ## Microsoft SQL Server 1. Create an empty database, for example `nomirun`. 2. Create a SQL login with `db_owner` on that database (or use Windows / managed identity). 3. Note the connection string, for example: ``` Server=sql.example.com;Database=nomirun;User Id=nomirun;Password=...;TrustServerCertificate=True ``` In `appsettings.json` set `Database:Provider` to `SqlServer`. NomiEsb runs the matching migration set (`Migrations/Postgresql/` or `Migrations/SqlServer/`) automatically. ## Backup Treat the NomiEsb database as the source of truth. It stores integrations (with encrypted tokens), users, audit history, notifications, and every solution / cluster / host configuration. Take a backup before every upgrade and as part of your regular DR plan. ## Health check After NomiEsb is running, the unauthenticated endpoint `GET /health` returns `Healthy` only if the database is reachable. Use it for load-balancer health checks. For Kubernetes, prefer the dedicated `/startup`, `/readiness`, and `/liveness` probe endpoints described in **[Running and verifying](/docs/install-run)**. ## Next Continue to [Entra ID application and groups](/docs/install-entra-id). --- # Entra ID application and groups NomiEsb signs users in through OpenID Connect against Azure Entra ID and looks up their group memberships through Microsoft Graph. Two artifacts must exist in your tenant: an **app registration** and three **security groups**. ## 1. Register the application In the Azure portal go to **Microsoft Entra ID → App registrations → New registration**. | Field | Value | |-------|-------| | Name | `NomiEsb` | | Supported account types | *Accounts in this organizational directory only* | | Redirect URI | **Web**, value `https:///signin-oidc` | After it is created, copy the **Application (client) ID** and the **Directory (tenant) ID**. ### Client secret Open **Certificates & secrets → New client secret** and copy the secret **value** (not the ID). Store it somewhere safe — Azure shows it only once. ### Authentication Open the **Authentication** blade: - Set **Front-channel logout URL** to `https:///signout-callback-oidc`. - Tick **ID tokens (used for implicit and hybrid flows)** under *Implicit grant and hybrid flows*. ### API permissions Open **API permissions → Add a permission → Microsoft Graph → Delegated permissions** and add: - `User.Read` - `User.Read.All` - `GroupMember.Read.All` Click **Grant admin consent for ** so users can sign in without an interactive consent prompt. ### Group claims (manifest) By default an app registration **does not** emit group memberships in the issued token. NomiEsb resolves the signed-in user's role from the `groups` claim, so the app must be told to include it. Two equivalent ways to configure this: **Option A — Token configuration blade (recommended)** 1. Open **Token configuration → + Add groups claim** 2. Tick **Security groups** 3. Under **Customize token properties by type**, set the *ID*, *Access* and *SAML* token property to **Group ID** (the default — emits the group object ID) 4. **Save** **Option B — Edit the manifest directly** 1. Open **Manifest** 2. Find the `groupMembershipClaims` property and change its value from `null` to `"SecurityGroup"`: ```json "groupMembershipClaims": "SecurityGroup", ``` 3. **Save** Either path adds a `groups` claim to the issued tokens containing the object IDs of every security group the user belongs to. Without this, a user can sign in but NomiEsb cannot resolve their role — they land on the app with no menu items. > **Token bloat:** if a user belongs to more than ~150 security groups (uncommon, but possible in large tenants), Entra ID emits a `_claim_names` overage indicator instead of the full list. NomiEsb then falls back to a Microsoft Graph call (`GET /me/memberOf`) to resolve memberships, which is exactly what the `GroupMember.Read.All` delegated permission added in the previous step covers — keep that consent granted. ## 2. Create the role security groups NomiEsb maps Entra ID security groups to its three roles. In **Microsoft Entra ID → Groups** create three groups: | Group name | Maps to NomiEsb role | |------------|----------------------| | `Nomirun_Admin` | Admin | | `Nomirun_Developer` | Developer | | `Nomirun_DevOps` | DevOps | Add at least one user to `Nomirun_Admin` — that account becomes your first administrator. Copy the **object ID** of each group. NomiEsb matches groups by their **display name** first; if you cannot use the names above (for example, you have a corporate naming standard), provide the object IDs in `appsettings.json` under `NomirunGroups` and the matching falls back to those. A user who authenticates successfully but is in none of the three groups can sign in but cannot reach any feature page. ## Next Continue to [Encryption key and license file](/docs/install-secrets). --- # Encryption key and license file Two secrets are required before NomiEsb starts: an encryption key for at-rest protection of integration credentials, and a license file from Nomirun. ## Generate the encryption key NomiEsb encrypts integration tokens, SMTP passwords, registry credentials, and other secrets at rest with AES-256-GCM. You must supply the 32-byte key as a Base64 string. Generate one with OpenSSL: ```bash openssl rand -base64 32 ``` Or with PowerShell: ```powershell [Convert]::ToBase64String([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(32)) ``` Keep the output safe. Losing it makes the encrypted database rows unreadable, and the only recovery is to delete and re-enter every integration. The value is read from the `ENCRYPTION_KEY` environment variable (recommended) or from the `ENCRYPTION_KEY` key in `appsettings.json`. Use the environment variable in production. ## Place the license file Copy your `nomiesb.license` into one of these two locations next to the binary: ``` /nomiesb.license /license/nomiesb.license ``` NomiEsb checks both paths on startup, validates the file, prints a banner showing the company name and expiration date, and refuses to start (in release mode) if the file is missing, invalid, or expired. Inside a container, mount the directory containing the license file at `/app/license`, or place the file at `/app/nomiesb.license`. ## Where the license appears in the console After sign-in, the Admin can review the loaded license at **Settings → License**. The page shows the company details, subscription plan, expiration date, and the number of CLI seats included in the subscription. The same data is read directly from the file plus a periodic call to the Nomirun licensing service. ## Next Continue to the [appsettings.json reference](/docs/install-configuration). --- # appsettings.json reference Edit `appsettings.json` (or `appsettings.Production.json`) shipped beside the binary. The minimal production configuration is: ```json { "Hostname": "https://app.nomiesb.example.com", "ENCRYPTION_KEY": "", "Database": { "Provider": "Postgres" }, "ConnectionStrings": { "DefaultConnection": "Server=db.example.com;Port=5432;Database=nomirun-esb;User ID=nomirun;Password=...;Include Error Detail=false" }, "AzureAd": { "Instance": "https://login.microsoftonline.com/", "TenantId": "", "ClientId": "", "ClientSecret": "", "CallbackPath": "/signin-oidc", "SignedOutCallbackPath": "/signout-callback-oidc" }, "Graph": { "Scopes": [ "User.Read", "User.Read.All", "GroupMember.Read.All" ] }, "NomirunGroups": { "Admin": "", "Developer": "", "DevOps": "" } } ``` ## Setting reference | Setting | Required | Purpose | |---------|----------|---------| | `Hostname` | Yes | Public URL of the console. Used for CORS, the OIDC redirect-URI check, and the absolute links in notification emails. | | `ENCRYPTION_KEY` | Yes | Base64 32-byte key for symmetric encryption of secrets at rest. | | `Database:Provider` | No (defaults to `Postgres`) | `Postgres` or `SqlServer`. | | `ConnectionStrings:DefaultConnection` | Yes | Connection string for the chosen provider. | | `AzureAd:TenantId` / `ClientId` / `ClientSecret` | Yes | Values from your Entra ID app registration. | | `AzureAd:CallbackPath` / `SignedOutCallbackPath` | Yes | Must match the redirect URIs registered in Azure (`/signin-oidc`, `/signout-callback-oidc`). | | `Graph:Scopes` | Yes | Delegated Graph permissions; leave the three defaults unchanged. | | `NomirunGroups:Admin` / `Developer` / `DevOps` | Recommended | Object IDs of the three role groups. Optional if your group display names match `Nomirun_Admin` etc. | | `LicensingApi:BaseUrl` | No (defaults to the public Nomirun service) | Override only when pointing to a private licensing endpoint. | | `Redis:ConnectionString` | No (single-replica default) | Set to a StackExchange.Redis connection string to run NomiEsb as multiple replicas. Leaving it empty preserves single-replica behaviour exactly. See [Scaling with Redis](/docs/install-scaling). | | `Logging:LogLevel:*` | No | Standard ASP.NET Core logging configuration consumed by Serilog (console sink). | ## Overriding with environment variables Every setting can be overridden by an environment variable using `__` (double underscore) for nesting. This is the recommended way to pass secrets in production: ```bash export ENCRYPTION_KEY="..." export ConnectionStrings__DefaultConnection="..." export AzureAd__ClientSecret="..." ``` `ASPNETCORE_ENVIRONMENT` defaults to `Production` if not set. `ASPNETCORE_URLS` controls the listening URLs (for example `http://0.0.0.0:8080;https://0.0.0.0:8443`). ## What is configured here vs in the console This file contains only the values NomiEsb needs at startup. Everything else — license details, app settings, integrations, environments, tags, users, tokens — is stored in the database and managed through the **Configuration** pages once you can sign in. ## Next Continue to [Running and verifying NomiEsb](/docs/install-run). --- # Running and verifying NomiEsb With prerequisites met, the database created, Entra ID configured, secrets in place and `appsettings.json` populated, NomiEsb is ready to start. ## Native binary Extract the release archive into your install directory and run the executable: ```bash ./Nomirun.Esb # Linux / macOS Nomirun.Esb.exe # Windows ``` On startup NomiEsb prints a banner, validates the license, runs database migrations, and begins listening on the URLs from `ASPNETCORE_URLS`. To run as a long-lived service, use **systemd** on Linux or **sc.exe** / **NSSM** on Windows. Point the service at the executable, set the working directory to the install folder, and pass the secrets through environment variables. ## Docker A container image is published with the binary and an Alpine base. Run it with: ```bash docker run -d \ --name nomiesb \ -p 8080:8080 \ -v /etc/nomiesb/license:/app/license:ro \ -e ASPNETCORE_URLS=http://0.0.0.0:8080 \ -e Hostname=https://app.nomiesb.example.com \ -e ENCRYPTION_KEY=... \ -e ConnectionStrings__DefaultConnection="Server=...;..." \ -e AzureAd__TenantId=... \ -e AzureAd__ClientId=... \ -e AzureAd__ClientSecret=... \ nomirun/nomiesb:latest ``` ## First sign-in 1. Open `https:///` in a browser. 2. Click **Sign in with Microsoft** and authenticate as a user belonging to `Nomirun_Admin`. 3. NomiEsb provisions your user record, persists your role, and lands you on the **Dashboard**. 4. The Dashboard shows yellow setup banners for any missing essentials (system settings, DevOps integration, ArgoCD integration). Follow the links to address them — they are covered in the **Configuration** section. ## Health checks NomiEsb exposes four unauthenticated health endpoints. `/health` is the aggregate check (every registered probe must pass); the other three match the Kubernetes probe lifecycle and only run the checks tagged for that probe so a temporary downstream blip cannot restart the pod unnecessarily. | Endpoint | Intended use | What it runs | |----------|--------------|--------------| | `GET /health` | Load-balancer or overall health page | All registered checks | | `GET /startup` | Kubernetes **startupProbe** | Waits until the application lifetime has fired `ApplicationStarted` (migrations done, listeners bound) | | `GET /readiness` | Kubernetes **readinessProbe** | Database reachable — the app can serve traffic | | `GET /liveness` | Kubernetes **livenessProbe** | Lightweight self-check — the process is alive | Example Kubernetes probe wiring: ```yaml startupProbe: httpGet: path: /startup port: 8080 failureThreshold: 30 periodSeconds: 10 readinessProbe: httpGet: path: /readiness port: 8080 periodSeconds: 10 livenessProbe: httpGet: path: /liveness port: 8080 periodSeconds: 20 ``` Each endpoint returns `200 Healthy` when every check matching its probe tag passes, and `503 Unhealthy` otherwise. ## Hardening notes These defaults are baked into the binary; you do not need to configure them, but you should be aware: - TLS: NomiEsb does **not** terminate TLS. Front it with a reverse proxy. - Security headers: HSTS, `X-Content-Type-Options`, `X-Frame-Options: DENY`, a strict `Referrer-Policy`, and a `Content-Security-Policy` are emitted on every non-development response. - Rate limiting: 1000 requests per minute per IP globally; 5 / minute on auth endpoints; 100 / minute on the MCP endpoint. - Authentication cookies are `HttpOnly`, `Secure`, `SameSite=Lax`, and stored server-side. - Incoming JSON bodies pass through HTML-stripping middleware to mitigate stored XSS. ## Upgrading Stop the service, replace the binary (or pull the new container image), and start it again. Migrations run automatically on the next startup. Always back up the database first. ## Troubleshooting | Symptom | Likely cause | |---------|--------------| | `No valid NomiEsb license found.` on startup | License file not in `./nomiesb.license` or `./license/nomiesb.license`, or expired. | | Sign-in loop or `AADSTS50011` | Redirect URI in the Entra ID app does not match `Hostname/signin-oidc`. | | User signs in but lands on the unauthorised page | User is in none of the three Nomirun groups, or `NomirunGroups` IDs are wrong. | | `Failed to map roles from MS Graph` warning in logs | Graph permissions not granted, or admin consent not given. | | `/health` or `/readiness` returns `Unhealthy` | Database unreachable, or `Database:Provider` does not match the actual server. | | `/startup` keeps returning `Unhealthy` | Application lifetime has not reported `Started` — migrations or bootstrap still running, or a fatal error before `app.Run()`. Check the startup log. | When in doubt, check the console output — Serilog logs to standard output and includes all startup decisions, the matched license file, the database provider, and any auth-pipeline warnings. ## Next Once the system starts successfully, continue with the [Configuration](/docs/config-license) section to wire up the rest. --- # Scaling NomiEsb with Redis NomiEsb runs as a single replica out of the box and stays that way until you tell it otherwise. For installs that need higher availability or want to absorb a bigger load, the console can scale horizontally as soon as it has a Redis backplane to coordinate cross-replica state. **Available since:** 25 May 2026. ## When to enable it Stay on a single replica if your install does not have an HA requirement — the default mode is byte-for-byte the same behaviour you had before this feature shipped, and there are no surprise differences in latency or memory footprint. Enable Redis when any of the following is true: - You run more than one replica of `Nomirun.Esb` behind a load balancer. - You need to survive a replica restart without losing live WebSocket notifications or auth sessions. - A background job (audit retention, expiry, monitoring) must run exactly once across the fleet. If you are unsure, run single-replica first — moving to Redis later is a configuration flip, not a migration. ## Turning it on Set the connection string in `appsettings.json`: ```json { "Redis": { "ConnectionString": "redis.example.com:6379,password=...,ssl=True,abortConnect=false" } } ``` Or via an environment variable (recommended for production): ```bash export Redis__ConnectionString="redis.example.com:6379,password=...,ssl=True" ``` Leave it empty or unset to stay in single-replica mode. Restart all replicas after the change. They will detect the connection at startup and wire every cross-replica surface to Redis. There is no opt-in per surface — it is all or nothing. ## What switches when Redis is on | Surface | Off (default) | On (Redis configured) | |---|---|---| | Auth session tickets | In-memory distributed cache | StackExchange.Redis cache | | MSAL token cache | In-memory token caches | Distributed token caches | | Delete progress | In-memory `IDistributedCache` | Redis `IDistributedCache` | | Tamper-lock state | In-memory store | Redis store + pub/sub invalidation + 5 s local cache | | Background-job leasing | No-op | `SET NX PX` + Lua release | | WebSocket fan-out | Local | Redis pub/sub channel | ## Background jobs that take a lease Every recurring job is wrapped with a distributed lease so only one replica runs each tick: audit-log retention, log retention, notification retention, notification email digest, subscription expiry, integration-token expiry, host-health monitor, tamper-guard scanner, device-code cleanup, and build-status background service. The **Infrastructure provisioning job** is intentionally not wrapped — it drains a per-process `Channel`, so every replica processes its own queue. If a replica that owns a queued delete dies, that work is orphaned; restart the replica or re-issue the delete from the console. ## Known caveats - The tamper-lock fast path is `lock + in-memory` thanks to the 5 s local cache, but the first read after an invalidation pays a Redis round-trip on a synchronous path. - Tamper-lock reads **fail open** if Redis is unreachable: NomiEsb keeps serving with the last cached state rather than crashing the request. This matches existing scanner semantics. - Auth sessions are encrypted by Data Protection. Make sure the Data Protection key ring is shared across replicas (mounted volume or `PersistKeysToStackExchangeRedis` — whichever you already use). - The `InfraJobQueue` is per-process. Pre-existing limitation; out of scope for the multi-replica feature. ## Sizing and operations - Use Redis 7+ with persistence (RDB or AOF) for production. The amount of data is small — sessions, leases, pub/sub — but cold restarts force every replica to recompute tamper-lock state and re-establish WebSocket subscriptions. - A single Redis primary with a replica is enough for most installs. Sentinel/Cluster work if your platform team already runs them. - Watch the `nomi-esb:lease:*` keys to confirm jobs are claiming leases as expected. Each key is short-lived (one lease window) and self-expires. ## See also - [appsettings.json reference](/docs/install-configuration) — every other startup setting. - [Running and verifying NomiEsb](/docs/install-run) — sanity checklist after first boot. --- # License Open **Settings → License** as your first stop after sign-in. The page is the single source of truth for the license currently driving the system. ## What the page shows | Card | Contents | |------|----------| | **Organisation** | License ID, company, address, VAT, DUNS, primary contact and email. All read-only — values come from the license file. | | **Subscription** | Plan, start date, expiration date with a colour-coded status badge: *Active* (green), *Expiring soon* (yellow, ≤ 30 days), *Renew now* (red, ≤ 7 days), *Expired* (red). The **Renew** button links to support. | | **CLI licenses** | Total seats included in the subscription, currently used, and available, with a coloured progress bar. | ## When the page is empty If the system is unlicensed (file missing, invalid, or expired), every other Configuration page is locked and the License page shows a warning with renewal instructions. Re-check the file path next to the binary — it must be at `nomiesb.license` or `license/nomiesb.license` — and restart the service. ## How CLI seats are consumed Seats are not consumed automatically by sign-in. They only count once an Admin enables the **CLI license** toggle on a user's card in **Settings → Users**. Releasing a seat is just toggling the flag back off; the user keeps their account and history. ## Background renewal check The **Subscription expiry check** background job runs daily and creates a notification 30, 14, and 7 days before the recorded expiration. The notification goes to all Admins. The job's schedule is configurable on **Settings → App**. ## Next Continue to [App settings](/docs/config-app-settings). --- # App settings Open **Settings → App**. This is where the system-wide knobs live. The page is split into two tabs. ## General tab | Setting | What it controls | |---------|------------------| | **Application log retention** | Days of Serilog rolling logs to keep. Pick from 1 / 3 / 7 / 30. | | **Audit log retention** | Days of audit-trail rows to keep. Pick from 30 / 60 / 90 / 180 / 365. | | **Notification retention** | Days resolved notifications survive before deletion. | | **Background job schedules** | Cron expressions for the maintenance jobs (see below). | | **Host health monitoring** | Enable continuous polling of host health, set the interval (cron), and optionally restrict it to specific environments. Off by default. | | **LLM endpoints** | Connection details for AI-assisted features (release notes, AI chat). Pick a provider, endpoint URL, API key, and model name. Only one endpoint is active at a time. | | **Auto-generate release notes** | When on, NomiEsb asks the active LLM endpoint to draft release notes after a successful module / library build. | ### Cron schedules All schedules accept the standard 5-field cron syntax (`minute hour day month dow`). | Job | Default | Purpose | |-----|---------|---------| | Subscription expiry check | `0 1 * * *` | Notifies admins 7–30 days before the NomiEsb license expires. | | Integration token expiry | `5 1 * * *` | Notifies admins / DevOps when an integration credential is approaching its recorded expiry. | | Application log retention | `0 2 * * *` | Deletes app log rows older than the configured retention. | | Audit log retention | `15 2 * * *` | Deletes audit rows older than the configured retention. | | Notification retention | `30 2 * * *` | Removes resolved notifications. | | Email digest | `*/15 * * * *` | Sends the queued notification email batch. | | Host health monitor | `*/2 * * * *` (when enabled) | Polls ArgoCD for sync / health and creates notifications on degradation. | Two more jobs run continuously and are not user-scheduled: - **Build status poller** — polls running CI builds and updates their status. - **Device-code cleanup** — removes expired CLI device-login codes. ## System tab The System tab is the post-install checklist. The Dashboard shows a yellow setup banner until everything here is filled in. | Setting | What to enter | |---------|---------------| | **SMTP server** | Host, port, username, password, from-address, SSL/TLS flag. The password is encrypted at rest. A **Test email** button sends a test message to the logged-in user's address. | | **Default container registry** | Host, username, and password used when modules are pushed as container images. | | **Default NuGet repository** | URL plus credentials of the feed used to publish module / library packages. | | **Host API key** | Token the Nomirun Host service uses when calling back into the ESB API. Generated here, copied into each host installation. | | **Custom NuGet repositories** | Additional feeds (private feeds, Azure Artifacts, etc.). | | **Custom container registries** | Additional registries (ECR, GCR, private Docker, etc.). | Sensitive fields (passwords, tokens, API keys) are encrypted at rest using the encryption key configured at install time. ## Next Continue to [Environments, systems and tags](/docs/config-environments). --- # Environments, systems and tags These three pages are simple tables but they shape how every cluster, solution and module is filtered and grouped throughout the console. Set them up once before you create your first cluster. ## Environments Open **Settings → Environments**. Environments are the labels you attach to clusters so you can tell production apart from staging at a glance. Click **Add environment** and provide: - **Name** — `Production`, `Staging`, `Dev`, etc. - **Colour** — shown everywhere a cluster is rendered. - **Sort order** — controls the order in lists. Existing entries can be edited or deleted from their card. Without at least one environment defined, every new cluster is unlabelled. ## Systems Open **Settings → Systems**. Systems are an optional second axis for grouping clusters: typically a business domain (`Billing`, `Payments`, `Reporting`) or a tenant. The form is identical to Environments — name, description, colour, sort order. Use systems if you operate multiple parallel platforms inside the same NomiEsb deployment; otherwise leave the page empty. ## Tags Open **Settings → Tags**. Tags are free-form labels attached to solutions, modules, libraries, clusters, and hosts. They are organised in **tag types** (e.g. *Region*, *Team*, *Compliance*) so the UI can group them coherently. 1. Click **Add tag type**, enter a type name (for example *Region*) and pick a colour. 2. Inside the new card, type values into the input box and press **Add** for each tag (`eu-west`, `us-east`, …). 3. Use the search box and the sort dropdown to navigate when the list grows. Tags are purely descriptive — they do not affect deployment behaviour, but they drive the filters on the Solutions, Clusters, and Audit Trail pages. ## Next Continue to [Integrations](/docs/config-integrations). --- # Integrations Open **Settings → Integrations**. This is the most important page after sign-in: nothing meaningful happens in NomiEsb until you connect at least one DevOps platform and (for deployments) ArgoCD. The page is split into two sections: **ArgoCD** and **DevOps platforms**. Each integration entry stores a name, an endpoint, a credential, an expiration date, and an active/inactive toggle. The credential is encrypted at rest. A **Test** button validates the connection without saving. ## ArgoCD 1. In ArgoCD, go to **Settings → Accounts** and create a token with read/write rights on the relevant projects. Copy the bearer token and note its expiration. 2. In NomiEsb click **Add ArgoCD** and fill in: - **Name** (free-form, used everywhere the integration is referenced). - **Server URL** (e.g. `https://argocd.example.com`). - **Token** and **Token expires at** (used to drive the expiry-warning notification). - **Project filter** (optional regex limiting which ArgoCD projects NomiEsb sees). - **Skip TLS verification** if your ArgoCD uses a self-signed certificate. 3. Click **Test** to confirm reachability, then **Save**. NomiEsb uses ArgoCD to query application sync status, fetch pod health, and trigger syncs from the Cluster and Host pages. ## Azure DevOps 1. In Azure DevOps, generate a **Personal Access Token** with **Code (read & write)**, **Build (read & execute)**, and **Packaging (read & write)** scopes. 2. In NomiEsb click **Add DevOps → Azure DevOps** and enter: - **Name**. - **Organization** (the slug from `https://dev.azure.com/`). - **Personal Access Token** and its **expiry date**. 3. Click **Test** then **Save**. NomiEsb auto-discovers the projects and repositories. ## GitHub 1. In GitHub, create a **Personal Access Token** (classic or fine-grained) with **repo** and **workflow** scopes. 2. Click **Add DevOps → GitHub** in NomiEsb. Enter the **organisation or user**, the **PAT**, and its expiry date. For GitHub Enterprise Server, also fill in the **endpoint** (e.g. `https://github.example.com/api/v3`). ## GitLab 1. Create a **Personal Access Token** in GitLab with `api`, `read_repository`, and `write_repository` scopes. 2. Click **Add DevOps → GitLab**. Enter the **instance URL** (`https://gitlab.com` or self-hosted), the **group or project path**, the **PAT**, and its expiry date. GitLab pipelines are wired up through the auto-generated root `.gitlab-ci.yml` produced by the solution Quick Start. ## Bitbucket 1. In Bitbucket, create an **App password** with **Repositories (read/write)** and **Pipelines (read/write)** scopes. 2. Click **Add DevOps → Bitbucket**. Enter the **workspace**, the **username** and **app password**, and the expiry date. ## Working with integrations afterwards - The **Active** toggle on each card temporarily disables an integration without losing its credentials. Inactive integrations are hidden from the Quick Start dropdowns. - The **expiring / expired** badge appears 14 days before the recorded expiry. The Integration Token Expiry background job also creates a notification for Admins and DevOps users. - Edit a card to rotate the token without recreating the integration. ## Next Continue to [Users and CLI licenses](/docs/config-users). --- # Users and CLI licenses Open **Settings → Users**. The page is the source of truth for who has access and which CLI licenses are assigned. ## Listing and filtering - **Sync** pulls the latest user list and group memberships from Microsoft Graph. NomiEsb runs this automatically at every login, so manual sync is only needed for bulk changes. - **Filters** — text search (name / email), role (Admin / Developer / DevOps), status (active / disabled). - **Sort** — name, email, role, status, ascending / descending. - Expand a user card to see the full details, the AD security group the role came from, and the CLI license toggle. Disabled users keep their history but cannot sign in. ## Why roles cannot be edited here Roles come from the user's Entra ID group membership and are refreshed at every login (and on **Sync**). To change a user's role, change the group they belong to in Microsoft Entra ID. ## Assigning CLI licenses Each NomiEsb subscription includes a fixed number of CLI seats (visible on **Settings → License**). When you toggle **CLI license** on for a user: - The seat count on the License page goes up by one. - The user gains the right to download the Nomirun CLI binary, the per-user CLI configuration file, and a CLI license file from their **Applications** page. Toggling it back off frees the seat. The user's account and history are preserved. ## Roles overview page **Settings → Roles** is a read-only reference: each card lists the menu items the role can see, and the bottom of the page is a step-by-step provisioning guide that walks through creating the three Entra ID groups. Use it when onboarding a new admin or to remind yourself which menu items are visible to which role. ## Next Continue to [API and MCP tokens](/docs/config-tokens). --- # API and MCP tokens End-users manage their own programmatic credentials from **Profile**. Two token types live there. ## CLI tokens (`nomi_…`) These are the tokens the Nomirun CLI uses to authenticate against the NomiEsb API. - A user runs `nomirun login` on their workstation. - The CLI starts the OAuth 2.0 device authorisation flow (RFC 8628). It prints a short user code and a verification URL. - The user opens the URL in any browser, signs in, and approves the device on the **Device login** page. - NomiEsb issues a `nomi_…` token, the CLI stores it locally, and the token shows up on the user's Profile page. From Profile, the user can: - See when each CLI token was created and last used. - Revoke any token at any time. The auth endpoints behind this flow are protected by a strict rate limit (5 requests per IP per minute) to mitigate brute force. ## MCP tokens (`nmcp_…`) These tokens are for external clients calling NomiEsb's Model Context Protocol endpoint at `/api/mcp`. On **Profile → MCP API keys**: 1. Click **Generate API key**, give it a name, and copy the value. The full token is shown **only once**. 2. The new key appears in the list with its created date, last used date, and scope. 3. Use it as a bearer token: `Authorization: Bearer nmcp_…`. The MCP endpoint has its own rate limit policy (100 requests per IP per minute). ## Notification preferences Each user can decide which notifications they receive in-app and by email from **Profile → Notifications**. The system delivers in-app notifications immediately over WebSocket; email is either sent immediately for high-severity events or batched by the **Email digest** background job (default every 15 minutes). ## What to do once configuration is complete When the Dashboard banners are gone and at least one DevOps integration plus one ArgoCD integration are active, hand the system over to the development and DevOps teams: - Developers run **Solutions → Quick Start** to create their first solution. - DevOps users run **Clusters → Quick Start** to create the first cluster. - The full walkthrough of every page is in the [User Guide](/docs/guide-overview). --- # Custom MCP servers NomiEsb Lens ships with a built-in set of tools that read your live solutions, modules, clusters, builds, dependencies and audit logs. Custom MCP servers let you bolt **additional** Model Context Protocol servers onto that surface — Slack, Jira, an internal RAG service, a homegrown API — without forking the app. **Available since:** 25 May 2026. **Roles:** Admin manages servers, every Lens-enabled user picks which ones to use per conversation. ## How the pieces fit | Piece | Where | What it does | |---|---|---| | Server entry | Settings → AI / Custom MCP Servers (Admin only) | URL + API key, encrypted at rest, with an active/inactive flag. | | Sidebar dropdown | Lens chat bubble, next to the LLM Skills picker | Per-user list of enabled servers, persisted as `EnabledMcpServerIds` (CSV of GUIDs). | | Tool routing | `AiChatService` → `CustomMcpClientPool` (singleton) | Resolves the user's selection at chat time, opens (or reuses) a connection, and routes `tool_call` dispatches back through the remote `McpClient`. | Tool names from remote servers are prefixed with `mcp___` before they reach the LLM. OpenAI caps tool names at 64 characters and remote names may collide with built-in ones; the prefix scheme handles both. ## Adding a server (Admin) 1. Open **Settings → AI → Custom MCP Servers**. 2. Click **Add MCP server** and fill in: - **Name** — free-form, shown to users in the sidebar picker. - **URL** — the MCP endpoint exposed by the remote server. Must be reachable from the NomiEsb host. - **API key** — encrypted with `EncryptionService` before it touches the database. - **Active** — global on/off. Inactive servers stay listed for users but appear muted and cannot be selected. 3. Click **Test** to verify the server speaks MCP and that the credentials work, then **Save**. 4. (Optional) inspect the tools the server advertises — the test response surfaces them, so you can sanity-check the prefix layout before users start picking the server. ## Selecting servers (Lens users) Open Lens, click the **MCP servers** chip next to the **Skills** chip, and tick the servers you want active for the next message. The choice persists in your profile (mirrored to `localStorage` under `nomirun-lens-selected-mcp-servers`) so it follows you across sessions and devices. Untick a server to drop its tools from subsequent turns — already-issued tool calls still complete. A muted entry means an admin has disabled the server globally; you cannot select it until the toggle goes back on. ## Persistence and security - Stored in the `McpServers` table (`McpServerEntity`). API keys are encrypted with the platform encryption key — the same one used for integration secrets and host configuration. - Per-user selection lives in `Users.EnabledMcpServerIds`. - The read endpoint `/api/mcp-servers` is **AllRoles** so non-admins can populate the sidebar. CRUD endpoints under the same prefix are **Admin** only. - Disabling a server globally takes effect for every user on their next chat turn — there is no per-user override of that decision. ## Troubleshooting | Symptom | Cause / fix | |---|---| | "Server is unreachable" on Test | The MCP URL must answer from inside the NomiEsb container. Check egress firewall and DNS. | | Tools never appear in the Lens picker | The server returned `tools/list` empty. Confirm the remote server is healthy and advertises tools. | | "Tool name too long" in chat log | Names like `mcp_a1b2c3d4__` exceed 64 chars. Shorten the remote tool name on the MCP server side. | | Users see the server but it is muted | Global **Active** toggle is off in Settings. Re-enable. | ## See also - [NomiEsb Lens](/docs/guide-lens) — the chat surface where these servers light up. - [LLM Skills](/docs/guide-llm-skills) — the sibling concept for reusable prompt templates. - [API and MCP tokens](/docs/config-tokens) — for clients calling NomiEsb's *own* MCP endpoint at `/api/mcp`. --- # Cloud Infrastructure The Cloud Infrastructure feature provisions an Azure landing zone — Resource Group + VNet + AKS + Log Analytics + an optional ArgoCD bootstrap — from the NomiEsb UI. NomiEsb renders Terraform (OpenTofu-compatible) files, pushes them to a Git repository, triggers a CI pipeline that runs `tofu apply`, and then layers ingress-nginx + cert-manager + ArgoCD on top of the resulting cluster. ## Provider support | Provider | Status | | --- | --- | | Azure | Generally available | | AWS | Generally available — see [AWS cloud integration](/docs/cloud-aws) | | GCP | Generally available — see [GCP cloud integration](/docs/cloud-gcp) | All three providers follow the same flow: a credentials integration in **Settings → Integrations**, an Infrastructure that renders Terraform into your Git repo, a CI pipeline that runs `tofu apply`, and an ArgoCD bootstrap with ingress-nginx + cert-manager on the resulting cluster. The provider-specific docs cover the deltas (auth modes, IAM/SA setup, state backend, registry conventions). ## What gets provisioned When you complete the wizard NomiEsb renders the following resources via the `azurerm` Terraform provider: - **Mandatory backbone** — Resource Group, VNet (with system / user / private-endpoint subnets), AKS cluster with two node pools, Log Analytics workspace wired to AKS's OMS agent. - **Optional modules** — Key Vault, Storage Account, Azure Container Registry, App Configuration, Azure DNS Zone (new or existing), Service Bus, Application Insights (workspace-based). - **ArgoCD bootstrap** — optional. When enabled with a DNS Zone, NomiEsb installs ingress-nginx + cert-manager + a Let's Encrypt ClusterIssuer + ArgoCD with a public Ingress and registers the resulting ArgoCD endpoint as an integration. Without a DNS Zone, ArgoCD lands as a ClusterIP-only service that you reach via `kubectl port-forward`. ## Pre-requisites Before opening the wizard you need: 1. An **Azure cloud integration** registered in **Settings → Integrations** with a service principal that holds **Owner** OR **Contributor + User Access Administrator** on the target subscription. See [Azure cloud integration](/docs/cloud-azure-integration). 2. A **DevOps integration** for the Git repository that will host the rendered Terraform tree and the CI pipeline. Any of Azure DevOps, GitHub, GitLab, or Bitbucket works. 3. A Git repository the DevOps integration can push to. NomiEsb writes `main.tf`, `versions.tf`, `outputs.tf`, `backend.tf`, and the `modules/` tree inside a per-infra subfolder named after the infrastructure (e.g. `prod-eu-a3xq7/`), so one repo can host many infrastructures side-by-side. The CI pipeline file lives at the platform-conventional path with a per-infra suffix (`.github/workflows/nomiesb-infra-.yml` on GitHub, `nomiesb-infra-.yml` elsewhere) and runs `tofu` inside its subfolder. Deleting an infrastructure removes only that subfolder + its pipeline file; sibling infras stay untouched. ## Next - Set up the [Azure cloud integration](/docs/cloud-azure-integration), the [AWS cloud integration](/docs/cloud-aws), or the [GCP cloud integration](/docs/cloud-gcp). - Walk through the [Add infrastructure wizard](/docs/cloud-infrastructure-wizard). - Understand the [provisioning lifecycle](/docs/cloud-lifecycle). --- # Azure cloud integration The Azure cloud integration stores the service principal NomiEsb uses to provision and tear down landing zones. It lives alongside the DevOps integrations in **Settings → Integrations**. ## Service principal requirements The SP needs one of the following at subscription scope: - **Owner**, or - **Contributor + User Access Administrator**. `Contributor` lets the SP create all the resource types in the rendered Terraform tree. `User Access Administrator` lets it create the role assignments AKS needs for its kubelet identity (e.g. `AcrPull` on the bound ACR). The **Test** button on the integration validates this combination up front so you don't discover it half-way through `tofu apply`. ## Register the integration 1. In the Azure portal create (or reuse) an **App registration** and add a **client secret**. Note the application's **Tenant ID**, **Client ID**, and the **secret value**. 2. Grant the SP `Owner` (or `Contributor + User Access Administrator`) on the subscription you intend to provision into. 3. In NomiEsb open **Settings → Integrations → Add Cloud → Azure**. Fill in: - **Name** (free-form). - **Subscription ID**. - **Tenant ID**. - **Client ID**. - **Client secret** (stored encrypted at rest). - **Region** (e.g. `westeurope`). The Test step verifies the region is available in this subscription. - **Token expires at** (drives the expiry-warning notification — rotate the secret in Azure before this date). 4. Click **Test**. NomiEsb performs four checks: authentication, subscription visibility, region availability, and the Owner-vs-Contributor+UAA RBAC check. The dialog shows the verdict including which roles were detected. 5. Click **Save**. ## How the SP is used - **Pre-flight** — every time you open the infrastructure wizard, NomiEsb can `Test` the integration (same four checks) so a stale token doesn't get caught at apply time. - **State bootstrap** — before any `tofu apply` runs, NomiEsb creates the Resource Group (tagged `NomiEsb=`) and a Storage Account + `tfstate` container that holds the Terraform state. This is done via the Azure SDK because Terraform can't manage its own backend. - **DNS A record write** — once ArgoCD's ingress is up, NomiEsb writes the `argocd.` A record to the Azure DNS Zone (in the infra's RG when a new zone is created, or in your own RG when you picked an existing zone). - **Resource group delete** — on infrastructure delete, NomiEsb deletes the Resource Group via the Azure SDK. It refuses to delete a group that isn't tagged for the infrastructure being removed, so a misconfigured row can't take out unrelated resources. ## Sharing the SP with the rendered Terraform The same four values (`ARM_CLIENT_ID`, `ARM_CLIENT_SECRET`, `ARM_TENANT_ID`, `ARM_SUBSCRIPTION_ID`) are surfaced in the **AwaitingSecrets** state of the infrastructure wizard, with per-platform instructions for pasting them into your DevOps platform's secret store. The pipeline references those secrets at run time — NomiEsb never ships the secret value into the rendered files. ## Next - Walk through the [Add infrastructure wizard](/docs/cloud-infrastructure-wizard). - Review the [provisioning lifecycle](/docs/cloud-lifecycle). --- # Add infrastructure wizard **Settings → Infrastructure** is the entry point. Click **Add infrastructure** to open the wizard. Each panel of the wizard maps directly to the Terraform spec NomiEsb persists as JSON on the row. ## Basics - **Name** — free-form, unique within the chosen cloud integration. - **Description** — optional. ## Cloud + DevOps - **Cloud integration** — pick a previously-registered Azure cloud integration. Immutable after the row is created (the InfraId allocated on save is baked into every Azure resource name and switching subscriptions mid-life would orphan everything). - **Region** — defaults to the cloud integration's region; can be overridden per row. - **DevOps integration** — picks where the rendered Terraform tree + CI pipeline land. Any of Azure DevOps, GitHub, GitLab, or Bitbucket. - **Repository** — populated from the chosen DevOps integration's repo list. ## Resource group - **Resource group name** — must follow Azure's RG name rules (1-90 chars; letters / digits / `-_().`). NomiEsb will create the RG if it doesn't exist, tagged `NomiEsb=`. It refuses to reuse an existing RG that doesn't carry the matching tag. ## AKS sizing - **VM size** — one of `Standard_B1ms`, `Standard_B2s`, `Standard_B2ms`, `Standard_D2s_v5`, `Standard_D4s_v5`, `Standard_E2s_v5`. Defaults to `Standard_B1ms` because trial / sandbox subscriptions typically have tight regional vCPU quotas. - **Node count** — defaults to 2. - **Kubernetes version** — defaults to `1.34`. - **Workload identity** — on by default. ## Log Analytics - **Tier** — `PerGB2018` (pay-as-you-go) or `CapacityReservation`. AKS's OMS agent ships logs and metrics to this workspace. - **Retention** — days. Defaults to 30. ## Optional services Each tick adds a module to the rendered tree; leaving one unticked omits the corresponding Terraform `module "…"` block (and its outputs) entirely. - **Key Vault** — Standard or Premium. - **Storage** — performance (Standard / Premium) and replication (LRS / ZRS / GRS / RAGRS). - **ACR** — Basic / Standard / Premium. When ticked, AKS gets a kubelet-identity `AcrPull` role assignment on the ACR so workloads can pull without a pull secret. - **App Config** — Free or Standard. - **DNS Zone** — Create new (lands in the infra RG) or Use existing (attaches via a Terraform data source to a zone in another RG you manage). See [DNS zones](/docs/cloud-dns-zones) for the trade-offs. - **Service Bus** — Basic / Standard / Premium. - **App Insights** — workspace-based, inherits the Log Analytics tier and retention. ## ArgoCD Single tick — when on, the post-AKS bootstrap installs ArgoCD on the cluster. Two paths: - **Public** (DNS Zone also enabled) — installs ingress-nginx + cert-manager + a Let's Encrypt ClusterIssuer + ArgoCD with an `argocd.` Ingress. NomiEsb writes the A record to the DNS zone and registers the resulting ArgoCD endpoint as an integration row named `argocd-`. - **In-cluster** (no DNS Zone) — installs ArgoCD as a ClusterIP-only service. NomiEsb does not register it as an integration; the Setup guide on the card walks you through `kubectl port-forward` and manual integration registration. When unticked the row reaches **Ready** straight after AKS comes up and you install ArgoCD yourself. ## Submit Clicking **Add** persists the row in **Draft** state. Nothing has been pushed to Azure or the repo yet — that begins when you click **Start provisioning** on the row's card. ## Next - Understand the [provisioning lifecycle](/docs/cloud-lifecycle). - See how [DNS zones](/docs/cloud-dns-zones) work. - See the [ArgoCD bootstrap](/docs/cloud-argocd-bootstrap) flow in detail. --- # Provisioning lifecycle Each Cloud Infrastructure row carries a `State` field driven by a single state machine. The progress strip on the card visualises the steps; this page is the underlying contract. ## States | State | What it means | | --- | --- | | `Draft` | Saved from the wizard but no Azure or repo changes yet. **Edit** is allowed only in Draft. | | `StateAccountReady` | NomiEsb created the Resource Group (tagged `NomiEsb=`) and the Terraform state Storage Account + `tfstate` container via the Azure SDK. | | `FilesPushed` | The rendered Terraform tree + CI pipeline file have been pushed to the repository. | | `AwaitingSecrets` | Waiting for you to paste the four `ARM_*` secrets into your DevOps platform's secret store. | | `Provisioning` | The CI pipeline is running `tofu apply`. NomiEsb polls the pipeline run every 10 s for the first 5 min, then every 30 s. | | `ProvisionFailed` | Pipeline finished with a non-success result. The `LastError` carries the failure verdict and a deep-link to the live pipeline run. | | `AksReady` | `tofu apply` succeeded. AKS is up. Transient state — flips immediately to ArgoCdBootstrapping (or Ready if ArgoCD was opted out). | | `ArgoCdBootstrapping` | helm-repos / ingress-nginx / LB IP / DNS record / cert-manager / ClusterIssuer / argocd-install / argocd-ready running serially. The card shows the granular step in `BootstrapStep`. | | `ArgoCdFailed` | A bootstrap step threw. The whole bootstrap is idempotent; click **Retry bootstrap** to re-run from the top. | | `Ready` | Everything succeeded. The card surfaces the health panel + apply history. | | `Destroying` | Cleanup is running in the background. The Delete dialog shows per-step progress. | ## Trigger points - **Start provisioning** — Draft → enqueue. The worker runs the state-bootstrap, file push, and stops at `AwaitingSecrets`. - **View secrets & provision** — opens the secrets modal. Clicking **Provision now** there moves AwaitingSecrets → Provisioning and dispatches the pipeline. - **Skip — already configured** — alternative path when you've already pasted the secrets out-of-band. - **Trigger apply** — available from `Ready` and `ProvisionFailed`. Re-runs `tofu apply`. The progress strip is replaced with a `Re-applying` pill on the Ready panel — ArgoCD and the Ready dot stay marked done because re-apply doesn't touch them. - **Cancel** — only during `Provisioning`. Cancels the CI run; the next poll sees the cancellation and writes `ProvisionFailed`. - **Retry bootstrap** — only on `ArgoCdFailed`. Re-runs the whole ArgoCD bootstrap; every step is `helm upgrade --install` / `kubectl apply` so a partial-success previous run won't poison the retry. - **Delete** — schedules cleanup: pipeline definition → repo files → resource group lock → deactivate auto-created ArgoCD integration → (shared DNS A record if applicable) → resource group delete. Cleanup runs in the background and the modal polls per-step progress. ## Pipeline secrets When the row hits `AwaitingSecrets` the secrets modal lists exactly four values: | Name | Source | | --- | --- | | `ARM_CLIENT_ID` | Cloud integration client ID | | `ARM_CLIENT_SECRET` | Cloud integration secret (mark as secret in the DevOps platform) | | `ARM_TENANT_ID` | Cloud integration tenant ID | | `ARM_SUBSCRIPTION_ID` | Cloud integration subscription ID | Per-platform paste instructions are surfaced in the modal: - **Azure DevOps** — Variable Group `nomiesb-infra-secrets`, then link to the pipeline and authorize. - **GitHub** — repository Actions secrets. - **GitLab** — CI/CD variables (mark Masked + Protected). - **Bitbucket** — repository pipeline variables (tick Secured). The values are never written to disk by NomiEsb — they leave the server only via the `/secrets` endpoint and are pasted by the user into the platform's native UI. ## Reconcile on restart If the server is restarted while a row is in `Provisioning`, `ArgoCdBootstrapping`, or `Destroying`, the startup reconciler re-enqueues those rows so the worker picks them back up. The pipeline run id is persisted so the poll resumes where it left off. ## Next - [Apply history](/docs/cloud-apply-history) records every `tofu apply` run with status and pipeline log link. - [Health panel](/docs/cloud-health-panel) replaces the progress strip once the row has reached Ready. - [ArgoCD bootstrap](/docs/cloud-argocd-bootstrap) walks through the ten sub-steps. --- # ArgoCD bootstrap When AKS is up the worker transitions the row to `ArgoCdBootstrapping` and runs one of two flows depending on whether the spec includes a DNS Zone. ## Public path (DNS Zone enabled) Ten serial steps. The granular step is persisted on `BootstrapStep` and broadcast over the WebSocket so the UI can show e.g. "Waiting for LoadBalancer IP" instead of the umbrella state. | Step key | What it does | | --- | --- | | `helm-repos` | Adds `ingress-nginx`, `jetstack`, `argo` helm repos and runs `helm repo update`. | | `ingress-nginx` | `helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx -n ingress-nginx --create-namespace --wait`. | | `lb-ip` | Polls `ingress-nginx-controller`'s `status.loadBalancer.ingress[0].ip` until Azure assigns the public IP (1–2 min). | | `dns-record` | Writes the `argocd.` A record (TTL 60) to the Azure DNS Zone via the Azure SDK. RG = the infra RG for new zones; the zone's RG for existing zones. | | `cert-manager` | `helm upgrade --install cert-manager jetstack/cert-manager` with CRDs enabled and `--wait`. | | `cluster-issuer` | Applies a `letsencrypt-prod` ClusterIssuer (HTTP-01 challenge via the nginx ingress). | | `argocd-install` | `helm upgrade --install argocd argo/argo-cd` with the `argocd.` Ingress wired to cert-manager. | | `argocd-ready` | Polls until the `argocd-initial-admin-secret` exists. | | `admin-token` | Logs into the ArgoCD API over HTTPS, reads the initial admin password, mints a 1-year API token. | | `register-integration` | Persists an `argocd-` integration row in NomiEsb pointing at `https://argocd.` with the token. | Once Ready the card surfaces: - The 4 Azure-assigned **nameservers** for the DNS zone (so you can delegate the apex at your registrar). - The **ArgoCD FQDN** (`argocd_fqdn`). - A note in `LastError` reminding you to delegate the domain — surfaced on the ArgoCD health pill as a tooltip so the same hint isn't duplicated as a separate alert. ## In-cluster path (no DNS Zone) Three steps: | Step key | What it does | | --- | --- | | `helm-repos` | Adds the `argo` helm repo only. | | `argocd-install` | `helm upgrade --install argocd argo/argo-cd` with the default ClusterIP service (no Ingress, no public hostname). | | `argocd-ready` | Waits for the `argocd-initial-admin-secret` to appear. | No integration row is auto-created on this path because there's no public endpoint NomiEsb can reach from the server. The card surfaces a **Setup guide** link on the ArgoCD health pill that opens a modal with the `kubectl port-forward` recipe + the steps to mint a token and register the integration manually. ## Retrying `ArgoCdFailed` exposes a **Retry bootstrap** button that re-runs the entire flow. Every step is `helm upgrade --install` or `kubectl apply`, so partial-success states aren't a problem. ## Cleanup On infrastructure delete the auto-created ArgoCD integration (if any) is deactivated and its encrypted token is rotated to an empty string. ArgoCD itself dies with the cascade when the Resource Group is removed. For shared DNS zones (UseExisting), the `argocd.` A record is removed explicitly so the zone you manage is left clean. ## Next - [DNS zones](/docs/cloud-dns-zones) — choose between create-new and use-existing. - [Health panel](/docs/cloud-health-panel) — surface ArgoCD reachability post-Ready. --- # DNS zones Ticking the **DNS Zone** option on the wizard gives the public ArgoCD bootstrap path a domain to mint records under. Two modes: ## Create new The default. Terraform creates an `azurerm_dns_zone` resource inside the infrastructure's own Resource Group. NomiEsb stamps the 4 Azure-assigned nameservers into the row's `dns_name_servers` output so you can copy them into your registrar's NS records to delegate the apex. Trade-offs: - **Pro** — single-row lifecycle. Delete the infrastructure and the zone goes with the RG cascade. - **Pro** — the public ArgoCD ingress (with Let's Encrypt) comes up automatically once delegation propagates. - **Con** — the zone disappears every time you reprovision from scratch (e.g. test environments). Your registrar's NS records then point at a missing zone until the new one is created and you re-delegate. ## Use existing Pick a zone that already exists in your Azure subscription (in any RG). Terraform attaches via a `data "azurerm_dns_zone"` source — it never owns the zone's lifecycle. The infra wizard's dropdown is populated by listing every DNS zone the cloud integration's SP can see. Trade-offs: - **Pro** — registrar delegation is done once and survives reprovisioning. Useful for controlled downtime windows and shared environments. - **Pro** — you can host multiple `argocd..example.com` records (one per infrastructure) in the same parent zone. - **Con** — NomiEsb writes the `argocd.` A record but does **not** delete the zone on infrastructure delete. It does delete the A record explicitly so the zone is left clean. When this mode is selected the wizard surfaces a dropdown labelled ` (rg: )`. Both fields are required — the `(name, resource group)` pair is the zone's identity, not just the name (zone names can repeat across RGs). ## How the renderer differs between the two modes | Path | Renders | Owns lifecycle? | | --- | --- | --- | | Create new | `resource "azurerm_dns_zone" "this"` in the infra RG | Yes — destroyed with the RG. | | Use existing | `data "azurerm_dns_zone" "existing"` in the user RG | No — Terraform never touches the zone, only reads its NS records to surface them as outputs. | In both modes the A record is written by NomiEsb (not Terraform) via the Azure SDK during the `dns-record` step of the ArgoCD bootstrap. That keeps the A record write idempotent across re-applies and re-bootstraps. ## Next - [ArgoCD bootstrap](/docs/cloud-argocd-bootstrap) walks through how the DNS step fits into the broader install. - [Apply history](/docs/cloud-apply-history) records every `tofu apply` against the infrastructure. --- # Apply history Every `tofu apply` triggered against a Cloud Infrastructure row is recorded. The card surfaces the table under an **Apply history** accordion once the row has reached Ready at least once. ## What's recorded | Column | Meaning | | --- | --- | | Kind | `Initial` (the very first apply after **Start provisioning**) or `Reapply` (every `Trigger apply` after the row has previously reached Ready). | | Status | `Running`, `Succeeded`, `Failed`, or `Cancelled`. | | Triggered by | Display name pulled from the user's auth claims (`preferred_username` / email / `system` for reconcile-driven dispatches). | | Started | Wall-clock start time. | | Duration | `FinishedAt - StartedAt` for terminal rows; live elapsed for `Running`. | | Pipeline | Deep-link to the live pipeline run in your DevOps platform. Empty for rows where the run never dispatched (e.g. when the trigger itself failed before reaching the pipeline). | For `Failed` rows an inline error message row appears below with the pipeline failure verdict — usually a copy of `"Pipeline 'tofu apply' finished with status 'failed'"` plus the WebUrl. ## What triggers a row - The first **Start provisioning** click on a Draft infrastructure row. - Each **Provision now** / **Skip — already configured** click on an AwaitingSecrets row. - Each **Trigger apply** click on a Ready or ProvisionFailed row. Each trigger inserts a row with `Status=Running` and the calling user's identity. The polling loop in the background worker stamps the pipeline `Id` + `WebUrl` once the platform dispatches the run, and updates `Status`/`FinishedAt`/`ErrorMessage` when the run reaches a terminal state. ## Programmatic access The MCP server exposes the same history via the `get_infrastructure_apply_history` tool. Useful for asking the AI assistant questions like "who re-applied production-aks in the last week?" or "show me failed applies for any infrastructure". The latest 5 runs are also embedded in the `get_infrastructure` response so single-call dashboards don't need a follow-up. ## Cleanup Apply runs are FK'd to the infrastructure row with `ON DELETE CASCADE`. When you delete the infrastructure, its history goes with it. The audit log still preserves the higher-level `Delete` entry independently. ## Next - [Health panel](/docs/cloud-health-panel) covers the live state pills shown alongside this table. - [Provisioning lifecycle](/docs/cloud-lifecycle) explains what each apply run does end-to-end. --- # Health panel Once a Cloud Infrastructure row has reached `Ready` at least once, the linear progress strip is replaced by a panel of five pills that show the live state of each component the user actually cares about. The panel auto-runs once when the card first mounts; **Refresh health** re-runs all five checks. ## The five checks | Pill | What's checked | Click-through | | --- | --- | --- | | **TF state** | Storage Account that holds `tfstate` exists in the infra Resource Group. | Azure portal storage account blade. | | **TF scripts** | `-/main.tf` is present in the repository on the default branch (per-infra subfolder so one repo can host many infras). | Repository URL (top-level). | | **TF pipeline** | On Azure DevOps: the pipeline definition `nomiesb-infra-` exists. On GitHub / GitLab / Bitbucket: the workflow YAML `nomiesb-infra-.yml` is present in the repo. | Pipeline `WebUrl` (Azure DevOps) or workflow file path (others). | | **ArgoCD** | Public path: `GET https:///healthz` returns 2xx. In-cluster path: pill is `Skipped` because the server can't reach a ClusterIP service. | The FQDN itself, or `null` for in-cluster. | | **Infrastructure** | Resource Group exists AND AKS `provisioningState=Succeeded`. | Azure portal RG overview. | Each pill carries a `Status` (`Ok` / `Warning` / `Error` / `Skipped`) and a short message in the body. Long messages are truncated in the pill body — the full text appears on hover via the pill's `title` attribute. ## How concurrency is handled The five checks fan out in parallel for speed, but the cloud + DevOps integrations are pre-resolved serially before the fanout so the scoped DbContext is never used concurrently. The DevOps platform client (`IDevOpsPlatformClient`) is invoked directly with the pre-resolved integration to bypass the facade that would otherwise re-resolve and re-touch the context. ## ArgoCD unreachable hint If the ArgoCD pill is in `Error` on the public path, the message includes a delegation hint: "Not reachable yet — …. Delegate `` to Azure DNS at your registrar (Setup guide → NS records), or use the port-forward fallback while DNS propagates." The Setup guide link in the same pill opens a modal with the registrar-delegation instructions + NS records to paste. ## Setup guide modal The ArgoCD pill has a **Setup guide** link that opens a wide modal with shell-toggleable (`bash` / `PowerShell`) snippets covering: - **Public-DNS path** — six steps: delegate domain → verify delegation → AKS credentials → read admin password → port-forward fallback → register in NomiEsb. - **In-cluster path** — six steps: AKS credentials → verify ArgoCD pods → read admin password → port-forward (option A) → expose via your own Ingress + cert-manager + DNS (option B) → register in NomiEsb. The modal replaces the older accordion that lived inside the card body. ## Programmatic access The MCP server returns the row state (and the `OutputsJson`) via `get_infrastructure`, which is the cached source of truth the panel uses for headings. The live check itself is a UI-side call to the server's `/api/infrastructure/{id}/health` endpoint and is not currently exposed as an MCP tool. ## Next - [Apply history](/docs/cloud-apply-history) tracks who triggered each `tofu apply`. - [Provisioning lifecycle](/docs/cloud-lifecycle) is the end-to-end state machine. --- # AWS cloud integration AWS support is generally available. The flow mirrors the [Azure cloud integration](/docs/cloud-azure-integration): a credentials integration in **Settings → Integrations** plus an Infrastructure that renders Terraform, runs it in your CI, and bootstraps ArgoCD on the resulting EKS cluster. ## Prerequisites - An AWS account where you control IAM. The integration's credentials provision EKS, VPC, IAM roles, S3, DynamoDB, Route53, KMS, ECR, and CloudWatch — see the IAM section below. - A DevOps integration (GitHub Actions, GitLab CI, Azure DevOps, or Bitbucket Pipelines) whose PAT can read + write a repository where the Terraform files will live. - AWS region with the services you want enabled. Some opt-in regions (`me-south-1`, `eu-south-1`, `af-south-1`, …) require manual enablement in the AWS console before the Test step passes. ## Create the IAM user The inline guide on the cloud integration form ships ready-to-paste commands. The short version: ```bash # 1. Create the IAM user aws iam create-user --user-name nomi-esb- # 2. Attach AdministratorAccess (broad). Fine-grained alternative below. aws iam attach-user-policy \ --user-name nomi-esb- \ --policy-arn arn:aws:iam::aws:policy/AdministratorAccess # 3. Create the access key — save the secret, AWS only displays it once. aws iam create-access-key --user-name nomi-esb- # 4. Print your 12-digit account ID for the form. aws sts get-caller-identity --query Account --output text ``` ### Fine-grained IAM policy If your security model doesn't allow `AdministratorAccess`, create a customer-managed policy with these 21 actions (also surfaced server-side by the Test step's `SimulatePrincipalPolicy` check): ``` eks:CreateCluster, eks:DescribeCluster, eks:DeleteCluster ec2:CreateVpc, ec2:DeleteVpc, ec2:CreateSubnet iam:CreateRole, iam:AttachRolePolicy, iam:CreateInstanceProfile iam:CreateOpenIDConnectProvider ecr:CreateRepository, ecr:DescribeRepositories route53:CreateHostedZone, route53:ChangeResourceRecordSets s3:CreateBucket, s3:PutBucketVersioning dynamodb:CreateTable kms:CreateKey logs:CreateLogGroup ssm:PutParameter elasticloadbalancing:CreateLoadBalancer ``` The actions cover everything Phase 04's Terraform modules exercise. Scope each one to the matching `arn:aws::::` resource pattern. ## Auth modes | Mode | When to use | What you paste in | |---|---|---| | **Access Key** (default) | Standard setup, equivalent to Azure SP-secret semantics. Long-lived key + secret. | `Access Key ID` + `Secret Access Key` | | **Assume Role via OIDC** | Federated CI (GitHub Actions OIDC, GitLab JWT). Bootstrap credentials still required in v1; pure OIDC federation is post-v1. | Bootstrap Access Key + `Role ARN` + optional `External ID` | ## Test connectivity preflight The Test button runs four checks in order: 1. **STS GetCallerIdentity** — confirms the credentials decode and resolve to the expected 12-digit account. 2. **EC2 DescribeRegions** — confirms the chosen region is enabled on this account (catches the opt-in-not-required trap). 3. **S3 region availability** — a deliberate 404 probe against a random bucket name verifies S3 is reachable in the region. 4. **IAM SimulatePrincipalPolicy** — checks the 21 required actions against the principal's effective policy. Failure lists the missing permissions. The RBAC step gracefully degrades to "skipped with warning" if the principal lacks `iam:SimulatePrincipalPolicy` itself (e.g. `AdministratorAccess` is attached but the policy excludes the simulator). The provision step still surfaces actual failures. ## Infrastructure landing zone The wizard renders a Terraform tree under `/`: - **VPC** — `/16` CIDR (default `10.50.0.0/16`), 2 AZs, public + private subnet pairs, per-AZ NAT gateway, VPC flow logs to CloudWatch. - **EKS** — managed cluster + node group (default `t3.medium` × 2 nodes, autoscale to 5), IRSA OIDC provider, control-plane logs, `deletion_protection` as the destroy gate. - **ECR** — optional. Either create a per-infra `nomi-esb-{infra_id}-app` repo or attach to existing repos by name. - **ECR push user** — IAM user + access key with `ecr:GetAuthorizationToken` + scoped push permissions. The CI pipeline runs `aws ecr get-login-password` at build time to mint short-lived docker passwords. - **Route53** — optional. New zone or attach to existing. Argo CD A/CNAME record is written by the bootstrap once the NLB hostname is known. - **KMS / S3 / SSM Parameter Store / CloudWatch / MSK / App Mesh** — all optional. - **IAM IRSA roles** — pre-created for `argocd-server` and `cert-manager` pods. cert-manager gets a Route53 DNS-01 policy automatically. ## State backend `AwsStateBackendBootstrap` provisions a single S3 bucket per account+region (`nomi-esb-state-{accountIdLast8}-{region}`) with versioning + SSE-AES256 + public-access block, plus a shared `nomi-esb-state-lock` DynamoDB table (pay-per-request). Both are created via SDK before any Terraform render — Terraform can't manage its own backend. The state objects live at `s3:////infra.tfstate`. Soft-delete via S3 versioning lets you recover from a stray `tofu destroy` apply. ## ArgoCD bootstrap After EKS becomes ACTIVE the bootstrap: 1. Fetches a kubeconfig via `aws eks update-kubeconfig` (uses an exec credential plugin — every later `kubectl`/`helm` call mints a fresh token). 2. Installs `ingress-nginx` with NLB annotations (`aws-load-balancer-type=external`, `nlb-target-type=ip`, `cross-zone-load-balancing-enabled=true`). 3. Waits for the NLB hostname. 4. Writes an A or CNAME record to Route53 (A when the NLB exposes an IP; CNAME when it exposes a hostname). 5. Installs cert-manager and annotates its K8s SA with the pre-created IRSA role ARN (`eks.amazonaws.com/role-arn=arn:aws:iam:::role/nomi-esb--irsa-cert-manager`). 6. Applies a Let's Encrypt ClusterIssuer using the DNS-01 Route53 solver. 7. Installs ArgoCD, waits for it to become ready, mints an API token, registers an `ArgoCdIntegration` row that points at `https://.`. ## CI variables The auto-push step at FilesPushed → Provisioning pushes per-cloud variables into the matching variable store. AWS shape: | Variable | Type | Use | |---|---|---| | `_AWS_ACCESS_KEY_ID` | plain | Access Key ID | | `_AWS_SECRET_ACCESS_KEY` | secret | Secret Access Key | | `_AWS_REGION` | plain | Region | | `_AWS_ROLE_ARN` | plain | (AssumeRole mode only) target Role ARN | | `_AWS_EXTERNAL_ID` | secret | (AssumeRole mode only) STS external ID | Azure DevOps stores these in a variable group named `nomi-esb-aws-credentials` so multiple AWS integrations coexist in the same project; the per-integration `_AWS_*` prefix prevents collisions. ## Delete safety EKS clusters are created with `deletion_protection = true` by default. The destroy flow lifts this server-side (via SDK) before `tofu destroy` and restores it if anything fails. S3 state bucket is preserved across infra delete (renamed; not destroyed) so a re-create can recover history if needed. ## Troubleshooting | Symptom | Cause / fix | |---|---| | Test step: "account mismatch" | Credentials belong to a different account than the Account ID you entered. Re-run `aws sts get-caller-identity`. | | Test step: "Region not enabled" | Opt-in regions need explicit enablement. Console → Billing → AWS Regions. | | Test step: "missing IAM permissions" | The fine-grained policy is missing one of the 21 listed actions. Attach `AdministratorAccess` to confirm the rest of the flow works, then narrow. | | `tofu apply` fails on `eks:CreateCluster` 5+ min in | EKS control plane provisioning is genuinely slow (10-20 min in our experience). Check the pipeline log — if it's still running, give it more time. | | ECR docker pulls fail in EKS | The node-group role gets `AmazonEC2ContainerRegistryReadOnly` automatically, but only for the same account's ECR. Cross-account pulls require an explicit `ECR repository policy` allowing the EKS account. | | ArgoCD Ingress doesn't get a cert | Check DNS delegation actually happened — the registrar must point at the Route53 zone's name servers. The bootstrap surfaces the NS list on the success card. | ## See also - [Azure cloud integration](/docs/cloud-azure-integration) — the reference implementation with deeper architectural notes. - [GCP cloud integration](/docs/cloud-gcp) — same patterns, GCP-specific calls. - [Cloud Infrastructure wizard](/docs/cloud-infrastructure-wizard) — common UI flow across all three providers. - [ArgoCD bootstrap](/docs/cloud-argocd-bootstrap) — the post-provision steps that wire ArgoCD onto the cluster. --- # GCP cloud integration GCP support is generally available. The flow mirrors the [Azure](/docs/cloud-azure-integration) and [AWS](/docs/cloud-aws) integrations: a credentials integration in **Settings → Integrations** plus an Infrastructure that renders Terraform, runs it in your CI, and bootstraps ArgoCD on the resulting GKE cluster. ## Prerequisites - A GCP project linked to a billing account. GKE requires billing enabled. - The required APIs enabled on the project: - `compute.googleapis.com` - `container.googleapis.com` - `iam.googleapis.com` - `iamcredentials.googleapis.com` - `artifactregistry.googleapis.com` - `dns.googleapis.com` - `cloudresourcemanager.googleapis.com` - `cloudkms.googleapis.com` - `logging.googleapis.com` - `secretmanager.googleapis.com` - A DevOps integration (GitHub, GitLab, Azure DevOps, Bitbucket) with a PAT that can read + write the Terraform repo. ## Create the service account The inline guide on the cloud integration form has copy-paste commands. The short version: ```bash # 1. Create the service account gcloud iam service-accounts create nomi-esb- \ --display-name="Nomirun ESB ()" \ --project= # 2. Grant Owner (broad). Fine-grained role list below. gcloud projects add-iam-policy-binding \ --member="serviceAccount:nomi-esb-@.iam.gserviceaccount.com" \ --role="roles/owner" # 3. Create and download the key JSON. gcloud iam service-accounts keys create ./nomi-esb-key.json \ --iam-account=nomi-esb-@.iam.gserviceaccount.com ``` Paste the contents of `nomi-esb-key.json` (the whole JSON document) into the **Service Account Key** field on the integration form. ### Fine-grained role set Instead of `roles/owner`: - `roles/container.admin` (GKE) - `roles/compute.networkAdmin` (VPC) - `roles/iam.serviceAccountAdmin` + `roles/iam.workloadIdentityPoolAdmin` - `roles/artifactregistry.admin` - `roles/dns.admin` - `roles/storage.admin` - `roles/cloudkms.admin` - `roles/logging.admin` - `roles/secretmanager.admin` - `roles/serviceusage.serviceUsageAdmin` (to enable APIs) - `roles/resourcemanager.projectIamAdmin` ## Auth modes | Mode | When to use | What you paste in | |---|---|---| | **Service Account Key** (default) | Standard setup. Long-lived JSON key. | The full key JSON blob. | | **Workload Identity Federation** | Federated CI without long-lived keys. v1 still requires a bootstrap key; pure WIF is post-v1. | WIF provider id + bootstrap SA key. | ## Test connectivity preflight The Test button runs five checks: 1. **Mint access token** — verifies the SA key JSON is well-formed and the SA is enabled. 2. **`projects.get`** — confirms the SA can read the project and the project is in state `ACTIVE`. 3. **`compute.regions.get`** — confirms the chosen region exists and is `UP`. 4. **`testIamPermissions`** — checks the 17 required permissions against the SA's effective bindings. Failure lists the missing permissions. Common failure messages include precise remediation hints ("Service account X cannot read project Y", "Grant roles/owner or the documented fine-grained role set"). ## Infrastructure landing zone The wizard renders a Terraform tree under `/`: - **VPC** — custom-mode network + subnet with two secondary IP ranges (pods + services — VPC-native GKE requires both). Cloud NAT for private-node egress, firewall rules allowing GCP health-check source ranges. - **GKE** — managed cluster with release_channel (default REGULAR), workload identity, optional private nodes, shielded nodes, `deletion_protection`. Dedicated node-pool SA with least-privilege bindings. - **Artifact Registry** — optional. New per-infra Docker repo (`nomi-esb-{infra_id}`) or attach to existing repos. - **Artifact Registry push SA** — dedicated SA with `roles/artifactregistry.writer` and a downloadable `google_service_account_key`. The SA JSON is used as the docker login password (literal `_json_key` username). - **Cloud DNS** — optional. New zone or attach to existing. ArgoCD A record written by the bootstrap. - **Cloud KMS / GCS / Secret Manager / Cloud Logging / Pub/Sub** — all optional. - **IAM workload identity** — pre-created GCP SAs for `argocd-server` and `cert-manager` K8s SAs with `roles/iam.workloadIdentityUser` bindings. cert-manager SA gets `roles/dns.admin`. ## State backend `GcpStateBackendBootstrap` provisions a GCS bucket per project+region (`nomi-esb-state-{projectId}-{region}`) with versioning + uniform IAM + enforced public-access prevention. GCS state has built-in generation-number locking — no separate lock table needed (one of the few places GCP is simpler than AWS). State objects live at `gs:////state/default.tfstate` (the `default.tfstate` filename is the `gcs` backend convention). ## ArgoCD bootstrap After GKE becomes RUNNING the bootstrap: 1. Fetches a kubeconfig via `gcloud container clusters get-credentials` (uses the GKE auth exec plugin). 2. Installs `ingress-nginx` with `externalTrafficPolicy: Local` (preserves client IP via NEGs). 3. Waits for the L4 LB IP. 4. Writes an A record to Cloud DNS. 5. Installs cert-manager and annotates its K8s SA with `iam.gke.io/gcp-service-account=nomi--cm@.iam.gserviceaccount.com`. 6. Applies a Let's Encrypt ClusterIssuer using the DNS-01 Cloud DNS solver. 7. Installs ArgoCD, waits for ready, mints an API token, registers an `ArgoCdIntegration` row. ## CI variables The auto-push step pushes: | Variable | Type | Use | |---|---|---| | `_GCP_PROJECT_ID` | plain | Project ID | | `_GCP_REGION` | plain | Region | | `_GCP_SA_KEY_JSON` | secret | Service account key JSON (the CI writes it to disk + sets `GOOGLE_APPLICATION_CREDENTIALS`) | Azure DevOps variable group: `nomi-esb-gcp-credentials`. ## Docker login for Artifact Registry GCP's docker login convention is unusual — the username is literally `_json_key` and the password is the SA key JSON verbatim. The cluster-host-build pipeline branches on the infra's `RegistryAuthKind` discriminator: ```bash case "$REGISTRY_AUTH_KIND" in gcp) echo "$CONTAINER_REGISTRY_PASSWORD" | docker login -u _json_key --password-stdin "$CONTAINER_REGISTRY" ;; esac ``` ## Delete safety GKE clusters are created with `deletion_protection = true`. The destroy flow lifts (project-level Cloud Resource Manager lien + cluster-level flag) before `tofu destroy` and restores on failure. State bucket is preserved across infra delete (renamed; not destroyed). ## Troubleshooting | Symptom | Cause / fix | |---|---| | Test step: "service account X cannot read project Y" | Missing `roles/viewer` (or higher) on the project. | | Test step: "Region X status NOT UP" | Rare. Wait for GCP to restore the region; check the [GCP status page](https://status.cloud.google.com/). | | Test step: "missing 17 permissions" | The SA needs at minimum the fine-grained role set above. Grant `roles/owner` to unblock + later narrow. | | `tofu apply` fails on `container.clusters.create` with `BILLING_DISABLED` | The project isn't linked to a billing account. Console → Billing → Link. | | `tofu apply` fails with "secondary IP range not found" | The spec's pod_secondary_cidr or services_secondary_cidr overlaps with the subnet CIDR. Pick disjoint /20 + /22 ranges within your `/16`. | | ArgoCD Ingress doesn't get a cert | Verify the DNS zone is delegated at your registrar — the bootstrap surfaces the zone's name servers on the success card. | ## See also - [Azure cloud integration](/docs/cloud-azure-integration) - [AWS cloud integration](/docs/cloud-aws) - [Cloud Infrastructure wizard](/docs/cloud-infrastructure-wizard) - [ArgoCD bootstrap](/docs/cloud-argocd-bootstrap) --- # Layout and navigation This page describes the parts of the console that are the same on every screen. Use it as a quick reference for the shell — the rest of the User Guide covers the individual pages. > **Screenshots.** The references throughout the User Guide point at filenames inside `docs/images/`. Capture them from a running NomiEsb instance and drop the PNGs in with the indicated names. ## The shell Every authenticated page shares the same layout: - **Sidebar** — collapsible, grouped navigation. Items appear or hide based on your role. - **Top bar** — page breadcrumb, theme toggle (light / dark), the AI Lens button (when an LLM endpoint is configured), and the user menu. - **Content area** — the page itself. - **Footer** — installed version. A badge appears when a newer version is available; clicking it opens the release notes. ## Notifications Notifications stream in over WebSocket. The bell icon in the sidebar shows an unread count and pops a toast when something arrives. Click the bell to open the [Notifications page](/docs/guide-account#notifications). ## Theme and language The theme toggle in the top bar switches between light and dark and persists per user. Language, region and timezone live on **Profile** and apply immediately. ## Sign-in and error pages These pages do not appear in the navigation but you will encounter them. | Page | When you see it | |------|-----------------| | **Login** (`/login`) | Anonymous users — single **Sign in with Microsoft** button. | | **Device login** (`/device-login`) | Reached when a CLI device-flow user-code points your browser here; you approve or deny the CLI's pending sign-in. | | **Unlicensed** (`/unlicensed`) | The NomiEsb installation has no valid license. | | **Unauthorized** (`/unauthorized`) | Authenticated user not in any Nomirun group. | | **Access Denied** (`/access-denied`) | The page exists but your role cannot view it. | | **Forbidden** (`/forbidden`) | The server rejected the action. | | **Not Found** (catch-all) | Route does not exist. | ## Next Continue to [Dashboard](/docs/guide-dashboard). --- # Dashboard **Route:** `/` · **Roles:** Admin, Developer, DevOps ![Dashboard](images/dashboard.png) The dashboard is the landing page for everyone. Sections are gated by role, so what you see depends on whether you sign in as Admin, Developer, or DevOps. ## What is on the page - **Counts strip** — clickable pills with the totals for solutions, modules, libraries (hidden when zero), clusters, hosts, and users. Each pill links to its list page. - **Pick up where you left off / Next steps** (adaptive) — on a fresh install this card surfaces an onboarding checklist; once you have solutions or clusters it switches to recent-activity tiles. - *Recent solutions* (Developer / Admin) — the most-recently active solutions, each tile showing module / library counts, last-build relative time, the latest build state (success / failed / running), and up to two module names with a `+N` overflow chip. - *Recent clusters* (DevOps / Admin) — the most-recently active clusters, each tile showing the environment colour, host count, last-deploy relative time, an unhealthy-host badge, and up to two host names with a `+N` overflow chip. - **Security findings** (Developer / Admin) — top vulnerable modules and libraries. Severity pills (Critical / High / Medium) with a CVSS score, a count of affected components, and a footnote when modules have not yet been scanned. Each row deep-links to the affected module or library. - **Recent build failures** (Developer / Admin) — the last seven days of failed, cancelled, or partial-success builds across modules, libraries, *and* host (cluster) builds. Each entry deep-links to the right detail page and (when available) opens the CI run in a new tab. - **Unhealthy hosts** (DevOps / Admin) — every host that is not in `Healthy` state, with a reason badge and a deep-link into the host details page. A `N hosts healthy` footnote summarises the rest. - **Clusters Deployment Activity** (DevOps / Admin) — KPI row (total deploys, success rate, active days, peak day) plus a D3 chart of deployments per day. Toggle between bar and line views and switch the time window (Last 30 / 90 / 180 days). A colour legend lists every host that contributed. - **Clusters Deployment Status** (DevOps / Admin) — search and status filter (Running, Stopped, Problem, Needs attention) over a tree of clusters. Each cluster expands to its hosts, with a health dot, modules count, and last-deploy time per row. - **Build History** (Developer / Admin) — paginated, filterable table of every build (modules, libraries, and host builds in one feed). Filters: Type (All / Modules / Libraries / Hosts) and Status (Success, Failed, In progress, Queued, Cancelled, Partial success). Columns include name, type, version, branch, date, status, and a link out to the CI run. - **Cluster Network Map** (DevOps / Admin) — interactive cluster → host → module diagram. Same component used on the Clusters page; pan, zoom, and click a node to focus. ## What to do here The Dashboard is a read-only summary; clicking any tile, row, or pill jumps to the relevant detail page. New installations should treat it as a checklist: 1. Follow the **Next steps** card until it switches to recent-activity mode. 2. Confirm builds appear in **Recent build failures** and **Build History** within minutes of being triggered in CI. 3. Verify the **Cluster Network Map** and **Deployment Status** show the expected hosts and ArgoCD sync state. 4. Watch **Security findings** and **Unhealthy hosts** — both should stay empty during normal operation. ## Next Continue to [Solutions, modules and libraries](/docs/guide-solutions). --- # Solutions, modules and libraries This page covers the four screens Developers and Admins use most: the Solutions list, the Solution Quick Start wizard, the Module Details page, and the Library Details page. ## Solutions **Route:** `/solutions` · **Roles:** Admin, Developer ![Solutions](images/solutions.png) Lists every solution. Each row expands to reveal its modules, libraries, configuration, and recent build activity. - **Filters and sort** — text search, *initialised* state filter, and sort by name or modified date. - **Per-solution actions** — view, create module, create library, **Initialise** (if not already), edit, delete. - **Inside an expanded solution** — - *Configuration* — tree-view editor for solution-level keys. - *Modules table* — name, type, package id, version, branch, status, row actions. - *Libraries table* — same shape as modules. - *Activity* — recent builds linked into module / library detail pages. When you click **Initialise** on a brand-new solution, NomiEsb runs a multi-step wizard (push template files, create CI pipelines, generate `.gitlab-ci.yml` for GitLab solutions, etc.) and shows progress in real time, with success / failed / skipped per step. ## Solution Quick Start **Route:** `/quickstart/solution` · **Roles:** Admin, Developer ![Solution Quick Start](images/solution-quickstart.png) A six-step wizard for spinning up a new solution end-to-end: 1. **Create solution** — name + description. The `.slnx` path is generated. 2. **Git repository** — pick a configured DevOps integration, choose or paste a repository URL. NomiEsb checks the repo is empty. 3. **Modules and libraries** — add as many as you like; each gets a generated `src//.csproj` path. 4. **Cluster (optional)** — name a cluster to create afterwards. 5. **Initialise** — flow diagram (Code → Pipeline → Ready); the **Start** button runs every step and shows live progress. 6. **Done** — confirmation, link to the new solution. The Quick Start works against Azure DevOps, GitHub, GitLab, and Bitbucket; per-platform pipeline files are pushed automatically. ## Module Details **Route:** `/modules/{Name}` · **Roles:** Admin, Developer ![Module Details](images/module-details.png) The full picture of a single module. - **Basic info** — name, package id, description, Git URL, project path, pipeline YAML path, latest version (clickable, opens release notes), target framework, language version, tags. - **Configuration** — tree editor of the module's configuration tree. - **Dependencies** — compliance badge, **Refresh dependencies**, **Download SBOM**, and a list of every NuGet dependency with version and license. - **Releases** — every published version with date and notes. - **Builds** — paginated CI history linked back to the pipeline run. - **Deployments** — where this module is currently running (cluster, host, version, sync status). ## Library Details **Route:** `/libraries/{Name}` · **Roles:** Admin, Developer ![Library Details](images/library-details.png) Same layout as Module Details — basic info, dependencies (with compliance and SBOM), releases, and builds. Libraries do not have a deployments tab because they are consumed, not deployed. ## Next Continue to [Dependency compliance and licenses](/docs/guide-dependencies). --- # Dependency compliance and licenses Every module and library has a **Dependencies** tab on its detail page that lists every NuGet package the build pulled in, the version that was resolved, and the license each package ships under. A header badge summarises license compliance at a glance. **Route:** `/modules/{Name}` → *Dependencies* tab · `/libraries/{Name}` → *Dependencies* tab · **Roles:** Admin, Developer ![Dependencies](images/dependencies-compliance.png) The same panel renders inline on the Solutions page when you expand a module or library row, so you can scan compliance across an entire solution without opening every detail page. ## What the Compliant badge means The badge reflects **license** compliance, not vulnerabilities (those have their own page). - **Compliant** (green shield) — every direct and transitive dependency uses a permissive license (MIT, BSD, Apache-2.0, …). Safe for commercial use without open-sourcing your own code. - **Not compliant** (grey shield) — at least one dependency carries a copyleft license (GPL, AGPL, LGPL when statically linked, …) that may require you to publish your code as open source. Review before shipping. - **No badge** — the compliance check has not yet run for this build. Click **Refresh** in the panel header to recompute. The check runs at build time from the SBOM, then again on demand when you click Refresh — it walks every component in the SBOM, looks up the SPDX identifier, and matches it against the permissive allow-list. ## The dependency table Each row is one resolved NuGet package: - **Package name** — links out to the package on nuget.org. - **Version** — the exact version locked at build time. - **License** — colour-coded SPDX badge. Green = permissive, amber = weak copyleft, red = strong copyleft, grey = unknown / proprietary. Click the badge to read the full license text. - **Per-package shield icon** — green when that single package is compliant, grey/red when it triggers the non-compliant state. ## Browse open source licenses The **Browse open source licenses** link in the panel header opens a curated reference of every SPDX license NomiEsb recognises, grouped by category (permissive, weak copyleft, strong copyleft, public domain). Use it when triaging an unfamiliar license to understand whether the package is safe to ship. ## Download SBOM **Download SBOM** in the header returns the CycloneDX JSON the pipeline produced for this build — the authoritative source the compliance check ran against. Feed it into Dependency-Track for continuous monitoring or attach it to a release for downstream consumers. See [SBOM](/docs/guide-sbom) for details on the file format and recommended tooling. ## Refresh **Refresh** re-walks the SBOM with the latest SPDX rules and updates the badge and per-package icons. Use it after changing `appsettings.json` license rules or after upgrading NomiEsb — both can change the compliance verdict without a new build. ## Next Continue to [SBOM](/docs/guide-sbom). --- # SBOM (Software Bill of Materials) A **Software Bill of Materials** is a structured, machine-readable inventory of every direct and transitive open-source component that goes into your built artifact — package name, version, license, and (for many formats) a content hash. NomiEsb produces a CycloneDX 1.6 SBOM for every successful **Module / Library Publish** pipeline run and stores it on the build record. The SBOM lets you answer three questions quickly: - *Which open-source packages am I shipping in this version?* - *Are any of them under licenses that conflict with my distribution policy?* - *When CVE-XXXX is published next week, am I affected?* ## Where SBOMs live in NomiEsb Open **Modules → {name}** or **Libraries → {name}** → **Dependencies** tab. The header shows a compliance badge (the highest-severity finding from the last scan), a **Refresh dependencies** button (re-reads the latest build), and a **Download SBOM** button. Clicking **Download SBOM** pulls the CycloneDX 1.6 JSON file the pipeline produced for the latest published version. The dependencies table below the buttons lists every NuGet package the module / library brings in transitively, with its version, license, and (when present) a vulnerability column linking to [Dependency vulnerabilities](/docs/guide-vulnerabilities). ## Viewing tools CycloneDX JSON is plain text but reading it raw is painful past a few dozen entries. The tools below all consume the file NomiEsb hands you. ### OWASP Dependency-Track (recommended) [Dependency-Track](https://dependencytrack.org/) is a self-hosted intelligent component analysis platform that ingests SBOMs, tracks them over time, correlates them against public vulnerability databases (NVD, GitHub Advisories, OSS Index, Snyk), and notifies you when **any** component in a tracked project becomes vulnerable — even if no one rebuilt the project. Workflow with NomiEsb: 1. Stand up Dependency-Track (Docker Compose is easiest). 2. Create a project per module / library; record its UUID. 3. Download the SBOM from NomiEsb → upload via Dependency-Track's UI **or** push it from CI to `POST /api/v1/bom`. 4. Dependency-Track parses the CycloneDX, builds an inventory, and re-evaluates vulnerabilities against it on a schedule (usually every few hours). The policy engine can also fail builds on license violations — a useful second pair of eyes alongside NomiEsb's compliance badge. ### CycloneDX CLI Lightweight command-line tool from the CycloneDX project — convert between formats, merge SBOMs, validate against schema: ```bash cyclonedx validate --input-file sbom.json cyclonedx convert --input-file sbom.json --output-format xml --output-file sbom.xml ``` Useful in pipelines and for quick spot checks. ### Anchore Grype (already shipped in the generated pipelines) Grype is the scanner the generated CI templates already run against the SBOM with `--fail-on critical` before publish. The same binary works locally: ```bash grype sbom:sbom.json ``` You see exactly the findings the pipeline saw, plus optional `--add-cpes-if-none` to expand CVE matching for components missing CPE identifiers. ### sbom-utility / cdxgen For ad-hoc inspection: [sbom-utility](https://github.com/CycloneDX/sbom-utility) lists, queries, and patches CycloneDX SBOMs from the terminal. [cdxgen](https://github.com/CycloneDX/cdxgen) can generate fresh SBOMs from arbitrary repos for cross-checking. ## Format details NomiEsb emits **CycloneDX JSON** pinned to **spec v1.6** — JSON (rather than the tool's default XML) and v1.6 (rather than v1.7) specifically because Grype's XML format detection against CycloneDX-dotnet output is unreliable and v1.7 is rejected by current Grype releases. If your downstream tool needs a different version or format, the CycloneDX CLI (above) converts losslessly. ## Next Continue to [Release notes generation](/docs/guide-release-notes). --- # Release notes generation Each successful **Module / Library Publish** build can have **release notes** attached to it. Notes are stored on the build record and shown in three places: - next to the version pill on the basic-info table (clickable; opens a modal with the latest version's notes), - on the **Releases** tab — every published version with date and notes, - inside the per-build expander on the **Builds** tab. Notes are either pasted in by hand or, more commonly, **generated by the configured LLM endpoint** from the commit range between this build and the previously published version. ## Generating notes from the UI 1. Open **Modules → {name}** (or **Libraries → {name}**) → **Builds** tab. 2. Find the build you want to annotate. Click the chevron / row to expand it. 3. Inside the expander, the **Release notes** card has a **Generate** button. 4. Click **Generate**. NomiEsb walks the commits between this build's commit and the previous published version, sends the changes to the configured LLM, and renders the markdown response inline. The generated-at timestamp is shown next to the button so you can tell when notes were last refreshed. If the button is disabled with a tooltip *"already generated"*, notes are present and the UI will not silently overwrite them — clear the existing notes (or wait for the next build) if you want to regenerate. ## Viewing existing notes - **Latest release** — click the version pill on the basic-info table; a modal pops up with the latest version's notes. - **Per build** — expand the row on the **Builds** tab. Markdown is rendered inline (headings, lists, code blocks, links). - **Releases tab** — all published versions in order, each with its notes block. ## Configuring the LLM Generation is gated on having an **active** LLM endpoint. Open **Settings → App → AI / LLM Endpoint**, pick a provider (OpenAI, OpenRouter, Azure Foundry, or Local), supply an endpoint URL, model name, and API key, then save and toggle **Active**. Until that's done the **Generate** button stays disabled and the tooltip explains why. The provider-agnostic prompt asks for a concise changelog grouped by *Added / Changed / Fixed* with bullet points. You can self-host a model via the **Local** provider — any OpenAI-compatible endpoint (e.g. `llama.cpp`, `vLLM`, Ollama with the OpenAI compat shim) — if your policy forbids sending source diffs to public providers. ## Auto-generate on build In **Settings → App** there's an **Auto-generate release notes** toggle (gated on an active LLM). When on, every successful publish triggers note generation in the background — by the time you open the build expander, notes are usually already there. When off (the default), notes are generated on demand by clicking the per-build button. ## Errors and quotas If generation fails (LLM endpoint unreachable, rate-limited, key revoked, …) the inline error block under the **Generate** button reports the message returned by the provider. Notes are never partially saved; the build keeps its previous notes (or stays blank). Retry once the upstream issue is fixed. For models behind paid quotas — keep an eye on the LLM provider's usage dashboard. NomiEsb does not currently meter tokens locally. ## Next Continue to [Dependency vulnerabilities](/docs/guide-vulnerabilities). --- # Dependency vulnerabilities Every **Module / Library Publish** pipeline run scans the just-built CycloneDX SBOM with [**Anchore Grype**](https://github.com/anchore/grype) before pushing the NuGet package. The scan report is uploaded to NomiEsb and surfaces in two places: - **Modules → {name}** / **Libraries → {name}** → **Dependencies** tab — a compliance badge in the header summarising the highest severity found in the most recent build. - **Builds** tab → expand any build → **Vulnerabilities** card — the per-build view: severity counts, the full findings table, and a download link for the raw `vuln-scan.json`. ## Severity model and the publish gate NomiEsb shows the standard Grype severity grades — **Critical / High / Medium / Low / Negligible / Unknown** — as colour-coded badges with counts. The pipeline gate in the generated CI templates is **`grype --fail-on critical`** — any Critical-severity finding fails the build **before** the NuGet push, so a vulnerable package never reaches the feed. Lower-severity findings are reported but do not block the publish; you triage them through the UI. To raise the gate (e.g. fail on **high** as well), edit the regenerated pipeline template in the source repo. To lower it, weaken the threshold or remove the step entirely — both are unrecommended without compensating controls. ## The findings table Each row is one CVE × component pair. Columns: - **Severity** badge (colour-coded) - **CVE id** — links out to NVD in a new tab - **Affected package + version** — the exact `name@version` from the SBOM - **Fixed in** — the upstream version that ships the fix (when known) - **Source** — the database the finding came from (NVD, GHSA, OSV, …) Sort and filter by severity to triage the worst first. ## Download the raw scan The per-build card has a **Download vulnerability scan** button. It returns the exact JSON Grype produced — useful when you want to feed it into Dependency-Track, JFrog Xray, or whatever vulnerability triage system your security team runs. The same artifact is also kept by the pipeline (under the build's run artifacts, alongside `sbom.json`) so SOC tooling that pulls from CI can grab it directly without going through NomiEsb. ## Scan staleness The card shows a *"Vulnerability scanned at: {timestamp}"* line. The scan happens **once per build**, against the vulnerability DB snapshot Grype downloaded at that moment. CVEs published *after* the build aren't reflected in NomiEsb until the module is rebuilt. For continuous re-scanning of past builds without rebuilding, push the SBOMs into [Dependency-Track](/docs/guide-sbom#owasp-dependency-track-recommended) — it evaluates them against the live database every few hours and notifies you on new findings. ## Builds without scan data Builds produced before the Grype gate was added (older NomiEsb releases, or pipelines you haven't regenerated) show *"No vulnerability scan available"* in the card. Re-run the latest pipeline template (Solution → Initialise → re-push CI files) to bring future builds under scan coverage. ## Next Continue to [CI pipeline history](/docs/guide-ci-pipeline). --- # CI pipeline history NomiEsb owns the CI pipeline file for every module and library — `azure-pipelines.yml`, the GitHub Actions workflow under `.github/workflows/`, `.gitlab-ci.yml`, or `bitbucket-pipelines.yml`, depending on the source-control platform. The **CI Pipeline** panel on each detail page shows the full history of that file with a side-by-side diff and a one-click sync from the repo. **Route:** `/modules/{Name}` → *CI Pipeline* tab · `/libraries/{Name}` → *CI Pipeline* tab · **Roles:** Admin, Developer ![CI Pipeline history](images/ci-pipeline-history.png) ## What the timeline shows The left column is a chronological timeline of every pipeline-file revision NomiEsb knows about. Each entry has: - **Version label** — `v1`, `v2`, `v3`, … assigned by NomiEsb in the order the file changed. - **Commit hash** — the short Git SHA that introduced this version (links out to the commit on the source platform). - **Timestamp** — when the file was committed, formatted in your timezone. - **Source-platform tag** — `AzureDevOps`, `GitHub`, `GitLab`, or `Bitbucket`. The same module can carry multiple tags over its lifetime if you migrated repositories. - **`latest` marker** — green pill on the most recent revision; this is the file currently in the repo. Clicking a row loads its YAML on the right. ## View vs. Diff vs. prev The toggle above the YAML pane switches between two reading modes: - **View** — render the selected version with syntax highlighting. Use this when reviewing what a build *did* run. - **Diff vs prev** — unified diff against the previous version on the timeline. Reds and greens make it obvious what changed between revisions: bumped action versions, new build-publish steps for added modules, broken-out `--fail-on critical` gates, etc. Diff is unavailable on `v1` (nothing to compare against). ## Refresh from repo The **Refresh from repo** button in the header pulls the latest commit history for the pipeline file from the source platform and reconciles NomiEsb's timeline with it. Use it when: - You committed a manual change to the pipeline file outside NomiEsb (the comment in every generated file warns against this — but it happens). - You see *"Manual changes will be overwritten"* in the file header and want to confirm the live state before re-initialising. - Build History shows runs that don't line up with any version on the timeline. ## How versions get created NomiEsb writes a new version every time it regenerates the pipeline file: - **Solution → Initialise** pushes the first version (`v1`) for every module and library. - **Adding a new module or library** to the solution regenerates the file with the new `build-publish-:` block (Bitbucket) or new pipeline step (Azure DevOps / GitLab) — bumping the version. - **Removing a component** removes its block and bumps the version. - **Upgrading NomiEsb** can bump the version when a release ships an updated template (the file header records the generator version). Manual edits in the repo are preserved on the timeline as their own version, but the next regeneration will overwrite them — keep custom logic out of the generated file. ## Next Continue to [Clusters and hosts](/docs/guide-clusters). --- # Clusters and hosts This page covers the three screens DevOps and Admins use to define and operate the deployment targets: the Clusters list, the Cluster Quick Start wizard, and the Host Details page. ## Clusters **Route:** `/clusters` · **Roles:** Admin, DevOps ![Clusters](images/clusters.png) Lists every Kubernetes cluster registered in NomiEsb. - **Filters** — environment, system, text search, status. - **Per-cluster card** — name, environment colour, system, tags, host count, module count, last sync. - **Actions** — expand to view hosts, edit, delete, **New host**. - **Clusters map button** — opens the same interactive diagram from the Dashboard, with a fullscreen toggle. When a cluster is expanded, each host appears as a sub-card showing its K8s status (healthy / progressing / unknown), sync status, running vs desired pods, service / ingress details, exposed ports, resource requests / limits, deployed modules, and deployed extensions, plus add-module and add-extension buttons. ## Cluster Quick Start **Route:** `/quickstart/cluster` · **Roles:** Admin, DevOps ![Cluster Quick Start](images/cluster-quickstart.png) Three-step wizard: 1. **Name cluster** — name, optional environment, optional system, optional tags. Cluster names are scoped per environment, so the same name can exist in dev, staging, and production simultaneously. 2. **Create hosts** — add any number of hosts; for each, attach modules (from the available list) and extensions, with a version per item. 3. **Done** — confirmation showing the created cluster structure. ### What cluster init/reinit does behind the scenes When you create or reinitialise a cluster, NomiEsb does three things on your behalf: - **Creates the Kubernetes namespace** automatically. The namespace follows the `{cluster}-{env}` convention and is committed to the GitOps base so the first ArgoCD sync never trips on a missing namespace. - **Pushes CI variables to your DevOps platform.** `CONTAINER_REGISTRY_*` and `ARGOCD_*` variables land in the matching variable store on Azure DevOps, GitHub, GitLab, and Bitbucket — no manual copy-paste of registry credentials. - **Installs ingress-nginx and cert-manager** on the target cluster. cert-manager is wired to a Let's Encrypt ClusterIssuer using the right DNS-01 solver per cloud (Azure DNS, Route53, or Cloud DNS). ## Host Details **Route:** `/clusters/{ClusterName}/{HostName}` · **Roles:** Admin, DevOps ![Host Details](images/host-details.png) Single-host deep dive. - **Overview** — name, description, address, framework, platform, version. - **Kubernetes status** — health, sync, pod counts, service name, ingress host & path, exposed ports, last sync timestamp, pod resource requests and limits. - **Modules and extensions** — two-column lists with version badges and add / remove controls. - **Configuration** — host-level configuration tree (ports, caching, metrics, Swagger, OpenTelemetry, rate limiting, authentication, Serilog, Azure Key Vault, Azure App Configuration, etc.). - **Build & deploy** — when integrations are connected, opens a dialog to pick modules + versions and trigger an Azure DevOps pipeline or ArgoCD sync. - **Deployment history** — table of past deploys with module, version, time, and sync status. ## Next Continue to [Host configuration comparer](/docs/guide-host-compare). --- # Host configuration comparer When you have several hosts of the same logical "kind" running across environments (dev / staging / prod, or per region) it's quick for them to drift apart — a port changed in dev that nobody propagated, a missing OpenTelemetry exporter on staging, an HSM rate-limit value that's only set on prod. The **Host configuration comparer** opens those configurations side-by-side so you can spot and fix drift in one go without bouncing between host pages. ![Host configuration comparer](images/host-compare.png) ## Opening the comparer The button lives on the **Clusters** page (`/clusters`). The cluster list is grouped by **system** (the optional logical group from Settings → Systems); each system header has a **Compare hosts** button on the right. 1. Click **Compare hosts** on the system you want to inspect. 2. The **picker modal** lists every host in every cluster under that system. 3. Tick the hosts you want to line up. Two minimum, no maximum; in practice four to six is the comfortable upper bound for screen real estate. 4. Click **Compare** to open the side-by-side view. ## What the side-by-side view shows Each picked host gets a column. Rows are the configuration tree keys, ordered by the canonical NomiEsb config schema (ports, caching, metrics, Swagger, OpenTelemetry, rate limiting, authentication, Serilog, Azure Key Vault, Azure App Configuration, …). - Cells are highlighted when they differ from the reference column (the leftmost host by default). Equal values are dimmed; differences are full-colour. - Missing keys (set on some hosts, not on others) render as **— Missing —** in muted text. - The **Highlight diffs** toggle in the header turns the colour overlay on or off; **Only differences** filters out rows where every column already agrees, so audits of large config trees fit on one screen. - Each column header carries the host name plus its cluster pill with the environment badge (Development / UAT / Production), so it's obvious which environment a value belongs to. Drift jumps out at a glance — a row where the dev column is a different colour from the staging / prod columns instantly tells you a change hasn't propagated yet. ## Editing in place Each cell is editable. Tab-navigation moves through the columns; values typed into a cell are staged locally and the **Save (N)** button in the header keeps a running count of pending changes. - **Save** opens a **Review changes** modal that lists every staged edit per host, with the *Path*, *Old*, and *New* value per row. Confirm to write the staged values to each host's configuration in one batch, with one server round-trip per host. Per-host save status is shown after the request completes (success / error per column). - The deployment manifests on the cluster are not touched directly — the changes follow the same flow as edits made on the Host Details page (write to NomiEsb's config tree; the next host build picks them up via `nomirun host pull-config`). ## Common workflows **Propagate a change from dev → prod.** Open the comparer with dev + staging + prod selected. Find the row with the new value. Type the same value into the staging and prod columns. Save. Trigger Build & Deploy on staging and prod from each Host Details page. **Audit a system before a release.** Open the comparer with every host in the system selected. Skim the highlighted rows: each one is a piece of drift you may want to reconcile before shipping. **Spot the canary host.** When one host is intentionally different (e.g. a canary running with extra OpenTelemetry sampling) the comparer makes the intentional differences explicit — and confirms that nothing else has crept in alongside them. ## What it doesn't do - **No cross-system compare** — the picker is scoped to one system at a time. Hosts from different systems usually don't have comparable configuration schemas, so the UI deliberately doesn't offer it. - **No history** — the comparer shows the *current* configurations, not a point-in-time snapshot. For "what changed since last week" use **Settings → Audit Trail** filtered by entity type *Host* and a date range. - **No cluster-level keys** — the comparer is for *host* configuration only. Cluster-level overrides live on the cluster row in the cluster list. ## Next Continue to [Settings](/docs/guide-settings). --- # App Settings **Route:** `/settings/app` · **Roles:** Admin The **App Settings** page is the system-wide configuration screen for a NomiEsb instance. Every other settings page (Environments, Systems, Tags, Users, Roles, Integrations, License, Audit Trail) edits one specific data type; this one is where the cross-cutting plumbing lives — log retention, scheduled jobs, host monitoring, SMTP, the LLM endpoint, the host API key, and the system / custom NuGet and container-registry credentials. The page is split into two tabs: - **General** — knobs you tune over the lifetime of the instance (retention, schedules, integrations of the *generic* sort). - **System** — the two credentials NomiEsb downloads from the licensing service in order to install Nomirun-shipped components: the Nomirun **container registry** and the Nomirun-SDK **NuGet repository**. A yellow *settings incomplete* warning sits at the top of the page until both System-tab credentials are populated. Until that's gone, the **System** tab also wears a warning triangle in the tab strip and the Dashboard shows the same banner. Once filled in, both go away. A single **Save** button at the bottom persists the entire page. The page only triggers a server round-trip on save and fetch — typing into a field doesn't autosave. ## General tab ### Application log retention How long NomiEsb keeps **application logs** and **audit log** entries. Each is a fixed dropdown: - **Log retention**: 1 / 3 / 7 / 30 days - **Audit log retention**: 30 / 60 / 90 / 180 / 365 days Cleanup is a background job — see the cron schedules below for when it actually runs. ### Background job schedules Six cron schedules driving the periodic background jobs. Each cell is a cron expression with a defaults hint underneath, and a **pencil** button next to the field opens the **CronEditorModal** — a builder UI that lets you assemble the expression without remembering the syntax. | Field | Default | What it does | |-------|---------|--------------| | **Subscription expiry** | `0 1 * * *` | Daily check for license/subscription expiry warnings. | | **Integration token expiry** | `5 1 * * *` | Daily check for nearing PAT/token expirations on configured integrations. | | **Log retention** | `0 2 * * *` | Prunes app logs past the retention window above. | | **Audit log retention** | `15 2 * * *` | Prunes audit logs past the retention window above. | | **Notification retention** | `30 2 * * *` | Prunes processed notifications. | | **Email digest** | `*/15 * * * *` | Flushes queued notification emails to recipients. | A small info hint reminds you that the NomiEsb cron flavour is the standard `minute hour day-of-month month day-of-week` 5-field syntax. ### Host health monitoring - **Enable** toggle - **Interval** dropdown — every 1, 2 or 5 minutes (disabled until enable is on) - **Environments** multiselect — tick any subset of the environments configured in **Settings → Environments**. Each environment shows its colour-coded badge. Leave all unticked to monitor every environment. When enabled, NomiEsb periodically pings the registered hosts (only those in the selected environments) and surfaces unreachable / unhealthy ones on the Dashboard and Cluster pages. ### SMTP NomiEsb's outbound mail configuration. Fields: - **Host**, **Port**, **Username**, **Password** (with eye-icon reveal), **From address**, **Use SSL** checkbox Once a password is stored, it is no longer returned by the API — the field shows a **saved** badge and a "leave blank to keep" placeholder. Saving an empty value preserves the existing one. A **Send test** button fires a real test email through the configured server and reports the result inline (recipient address on success; the error message on failure). The hint underneath reminds you to **save first** — the test uses the persisted config, not the unsaved form values. ### AI / LLM endpoint One LLM endpoint registered per provider (`OpenAI`, `OpenRouter`, `AzureFoundry`, `Local`). Switching the provider dropdown swaps the form fields to that provider's stored values: - **Display name** (free-form label) - **Model** (e.g. `gpt-4o-mini`, `claude-3.5-sonnet` — placeholder hints at the per-provider format) - **Endpoint** (URL) - **API key** with eye-icon reveal; **saved** badge once stored A separate **Save** inside this card persists just the LLM endpoint (independent of the page-level Save). The **Active** toggle (visible after first save) marks one provider as the one NomiEsb actually uses; only one can be active. Below the endpoint sits the **Auto-generate release notes** toggle. It is **disabled until at least one LLM endpoint is saved** — the helper text changes to a yellow warning explaining why when no endpoint exists yet. ### Host API key A single shared key Nomirun hosts use to authenticate back to the Esb. The field is read-only: - If a key already exists, the input shows **stored on server** — the actual value is never returned by the API again after creation. - **Regenerate** rotates the key. The new value is shown once in the field after regenerate; save the page, then copy it into the host config. - An eye toggle reveals the value while it's still in memory (i.e. immediately after regenerate, before navigating away). ### Custom NuGet repositories Per-instance NuGet feeds the Nomirun CLI uses when building modules. This is **the** place to register the feed where your CI publishes packages — see the platform-specific guides: - [Azure DevOps NuGet feed](/docs/infra-ado-nuget-feed) - [GitHub NuGet feed](/docs/infra-github-nuget-feed) - [GitLab NuGet feed](/docs/infra-gitlab-nuget-feed) - [Bitbucket NuGet feed](/docs/infra-bitbucket-nuget-feed) The card lists existing repositories as collapsed accordion rows showing the **name** and **source**. Each row has: - **Test connection** (Wi-Fi icon) — fires a server-side credential test against the source URL; result rendered in green / red strip below the row header. - **Delete** (trash icon) - **Chevron / row click** — expands to the full editor: name, source URL, username, password (with eye toggle and **saved** badge), plus four checkboxes — *Allow insecure*, *Skip duplicates*, *Use interactive*, *Use clear-text password*. Each row has its own **Save** to persist that single repo. Adding a new feed walks through a typed wizard: click **+ Add Repository**, pick a **Repository type** (Azure DevOps, GitHub, Baget, Feedz.io), then fill the fields. The username/password placeholders adapt to the chosen provider. **Add** stages the repo; the page-level **Save** persists. ### Custom container registries Same accordion-style list as Custom NuGet Repositories, but for container registries the cluster pulls from. Fields per row: - **Name**, **Host name** (e.g. `myregistry.azurecr.io`), **Username**, **Password** (with eye toggle and **saved** badge) The add wizard offers three types: **Azure Container Registry**, **Docker Hub**, **Generic**. Host-name placeholders adapt accordingly. ## System tab The two cards on this tab hold credentials NomiEsb fetches from the licensing service. You don't type these in — each card has a **Fetch credentials** button that pulls the current values and shows them read-only. ### Nomirun container registry The container registry NomiEsb uses to pull Nomirun-shipped images (host runtime, etc.). Click **Fetch credentials** to populate: - **Address** (read-only) - **User name** (read-only) - **Password** (read-only, eye-toggle to reveal; **stored on server** badge afterwards) If credentials are present, an expiry strip appears underneath; if not, a yellow alert prompts you to fetch them. Errors during fetch render as a dismissible warning at the top of the card. ### System NuGet repository (Nomirun SDK) Same shape as the container-registry card, but holds the credentials for the Nomirun-SDK NuGet feed (where the SDK packages your modules reference live). Fields displayed read-only after fetch: - **Name**, **Source**, **Username**, **Password** (eye toggle, **stored on server** badge), expiry strip **Fetch credentials** refreshes both fields; the next page-level **Save** persists them. ## Save A single **Save** button at the bottom of the page persists everything — both tabs, every card. The LLM-endpoint card and per-row "Save" buttons inside Custom Repositories / Registries are exceptions: they save just their own subset so you don't have to scroll back down for one-off edits. After save, a green **Saved** banner appears at the top. ## Next Continue to [Profile, applications and notifications](/docs/guide-account). --- # Profile, applications, notifications and documentation These four pages are available to every authenticated user regardless of role. ## Profile **Route:** `/profile` · **Roles:** Admin, Developer, DevOps ![Profile](images/profile.png) Your personal account page. - **Account** — your name, email, and current role (read-only). - **Preferences** — language (one of the seven supported), regional culture, timezone, theme. Changes apply immediately and persist for next sign-in. - **MCP API keys** — generate, copy, and revoke `nmcp_…` tokens used by external MCP clients. The full token is shown only once at creation. - **CLI tokens** — view and revoke the `nomi_…` tokens granted to the CLI through `nomirun login`. - **Notification preferences** — opt in or out of in-app and email delivery per notification type. ## Applications **Route:** `/applications` · **Roles:** Admin, Developer, DevOps (with a CLI license) ![Applications](images/applications.png) Self-service downloads for the Nomirun CLI and Host. Each card walks the three-step flow: 1. **Download installer** — Windows or Linux build of the binary. 2. **Download config** — a per-user config file pointing at this NomiEsb instance. 3. **Generate license** — paste the hardware key printed by the CLI / Host on first run, click **Generate**, and download the resulting license file. If you have not been granted a CLI license, the page shows an info card asking your administrator to enable it on **Settings → Users**. ## Notifications **Route:** `/notifications` · **Roles:** Admin, Developer, DevOps ![Notifications](images/notifications.png) Your notification inbox. Two view modes: - **Individual** — flat list, one row per notification, with mark-read, open-entity, and delete buttons. **Mark all as read** clears the unread badge. - **Grouped** — collapses repeats by *(type, entity)* with a count and the latest timestamp. Mark or delete the whole group at once. Filters by status (read / unread), severity (info / warning / error / resolved), and type. New notifications stream in over WebSocket and pop a toast. ## Documentation **Route:** `/documentation` · **Roles:** Admin, Developer, DevOps ![Documentation](images/documentation.png) Your in-app entry point to the NomiEsb docs and NomiEsb Lens. The page links out to the hosted documentation, highlights popular topics (Getting started, Solutions, Clusters, Integrations), and embeds a **Contact support** form — expand it to send a request straight to the Nomirun support team. Pick a category (Solutions, Clusters, Integrations, Other), enter a subject and a message, and submit. **Submit another** resets the form for a follow-up request. ## Next Continue to [NomiEsb Lens](/docs/guide-lens). --- # NomiEsb Lens NomiEsb Lens is the built-in AI assistant. It answers natural-language questions about your live NomiEsb data — solutions, modules, libraries, clusters, hosts, builds, deployments, dependencies, vulnerabilities, audit logs — by pairing an LLM with the same MCP server you can wire into Claude or any other MCP-aware client. **Available everywhere:** click the floating Lens icon in the bottom-right corner of any page. **Roles:** Admin, Developer, DevOps. ![NomiEsb Lens](images/lens.png) ## Asking a question Type the question into the input at the bottom of the panel and press Enter (Shift+Enter for newline). Lens reads the docs *and* the live database to answer in context. Examples that work well: - "Which builds finished but had a failed result?" - "Which tokens are expiring next?" - "Show me every dependency in production with a CVSS above 7." - "Analyze the CI pipeline of module Meters." - "How many modules have not been scanned for vulnerabilities yet?" - "List clusters in the Production environment that have an unhealthy host." The reply streams back with structured tables, code blocks, and entity links. Click any entity name (module, host, build) to deep-link to its detail page. ## Conversation, copy, share The thread persists across the session. Each reply has: - **Copy** — copies the answer as Markdown to your clipboard. - **Share** — generates a permalink to the answer (the data is re-queried server-side when the recipient opens it, so the result stays fresh). - **Details toggle** — top-right of the panel — expands the answer to show the underlying MCP tool calls Lens made (which queries it ran, what filters it applied, what came back). Useful for debugging unexpected answers and for learning the data model. ## Recent queries Below the conversation, the **Your recent queries** chips remember your last few prompts so you can re-run a question with one click. Pin the panel to the right rail (the pin icon in the header) to keep Lens open while you navigate the rest of the console — answers and queries follow you across pages. ## Where to launch it from - The floating round Lens icon in the **bottom-right corner of every page**. - The **Open NomiEsb Lens** button on the **Documentation** page (`/documentation`) — useful when you want to ask the docs first. - The MCP API endpoint exposed by NomiEsb itself — see [API and MCP tokens](/docs/config-tokens) for connecting external clients (Claude, ChatGPT, custom agents) to the same data Lens reads. ## Skills and custom MCP servers Two chips sit just above the chat input: - **Skills** — pick a reusable prompt template ([LLM Skills](/docs/guide-llm-skills)) to prime the next message. Use one for "Summarise this build", another for "Explain this CVE in our context", and so on. - **MCP servers** — tick the extra [Custom MCP servers](/docs/config-mcp-servers) you want active for the conversation. Tools from those servers are namespaced with an `mcp_` prefix to avoid colliding with built-in tools. The two selectors are independent: a Skill changes how Lens *thinks*, an MCP server changes what Lens can *reach*. ## What Lens can and can't do Lens is **read-only**. It does not create, edit, or delete entities — even when you ask it to. The MCP tools it has access to are scoped to query operations against the database and to fetch live ArgoCD / build / SBOM artefacts. Use the console (or the CLI) for any write action. Answers are scoped to your role. A Developer asking about cluster health gets only the clusters they can see; a DevOps user asking about module dependencies gets only modules they have access to. ## Next You have reached the end of the User Guide. From here: - Return to the [Dashboard](/docs/guide-dashboard) for an at-a-glance summary. - Set up a new Solution from the [Solutions, modules and libraries](/docs/guide-solutions) page. - Continue with the [Infrastructure Setup](/docs/infra-ado-pipelines) section if you still need to stand up CI/CD, ArgoCD, or container registries. --- # LLM Skills LLM Skills are reusable prompt templates that prime NomiEsb Lens for a specific job. A Skill bundles a system message, optional examples, and a recommended model into a named entry you can pick from the Lens chat bubble. **Available since:** 25 May 2026. **Roles:** Admin manages the catalogue, every Lens user picks Skills per conversation. ## Why use a Skill A Skill is the difference between typing 200 words of context into the chat every time and clicking once. Common patterns we see customers add: - **"Summarise this build"** — fed the build URL, returns a release-note paragraph. - **"Explain this CVE in our context"** — fed a vulnerability id, returns severity, blast radius, and the modules affected. - **"Onboarding answer"** — restricts answers to a curated subset of the docs to keep new joiners on the rails. - **"Production-only stance"** — forces the model to filter every query through the Production environment. Skills compose with [Custom MCP servers](/docs/config-mcp-servers) — pick the prompt template and the tool set independently for each conversation. ## Anatomy of a Skill | Field | Purpose | |---|---| | **Name** | Free-form, shown in the chat bubble dropdown. | | **Description** | One-liner shown on hover; helps users decide when to use it. | | **System message** | The prompt fragment prepended to the LLM context when the Skill is selected. | | **Examples** (optional) | Few-shot exchanges that nudge the model toward the response shape you want. | | **Recommended model** (optional) | If set, the chat switches to that model when the Skill is active. | | **Scope** | Who sees the Skill: all users, a specific role, or a single user. | | **Active** | Global on/off. Disabled Skills do not appear in the picker. | Skills live in the `LlmSkills` table (`LlmSkillEntity`) and are served through `/api/llm-skills`. ## Adding a Skill (Admin) 1. Open **Settings → AI → LLM Skills**. 2. Click **Add Skill** and fill in name, description, and the system message body. Markdown is allowed in the system message — the chat renders nothing, but it makes the prompt readable in the editor. 3. (Optional) add few-shot examples as `user → assistant` pairs. 4. (Optional) lock the Skill to a specific model from the dropdown — useful when a Skill is sensitive to the underlying model's style. 5. Pick a **Scope** and **Save**. Edit a Skill at any time. Updates take effect on the next chat turn — already-issued messages are not retroactively re-prompted. ## Using a Skill (Lens users) In the Lens chat bubble, click the **Skills** chip just above the input. Pick a Skill — it primes the next message. The Skill stays active for the rest of the conversation; click again to remove it or swap to a different one. The current Skill is highlighted at the top of the input so you always know which prompt frame you are in. If you want to layer in custom tools alongside, open the [MCP servers picker](/docs/config-mcp-servers) right next to the Skills chip and tick whichever servers you want active. ## Authoring tips - Keep the system message focused. The model already has the entire NomiEsb tool surface — the Skill should narrow behaviour, not re-explain the product. - Use examples sparingly. Two or three high-quality pairs beat ten low-quality ones. - Put hard rules in the first paragraph ("Never quote internal cost numbers", "Answer only in en-US"). They land better there than buried in the middle. - Add a Skill description that reads like a job title, not a paragraph — users scan the dropdown. ## Troubleshooting | Symptom | Cause / fix | |---|---| | Skill doesn't show up in the picker | Scope mismatch or Active flag off. Check the Skill in Settings. | | Wrong model used despite a Skill | Lens falls back to the workspace default if the recommended model is unavailable or unlicensed. Confirm the model is enabled in **Settings → AI**. | | Answers ignore the Skill instructions | The system message may be conflicting with the user's question. Test with a fresh conversation; clear the existing thread first. | ## See also - [NomiEsb Lens](/docs/guide-lens) — the chat surface where Skills are applied. - [Custom MCP servers](/docs/config-mcp-servers) — pair Skills with extra tools. --- # NomiEsb Documentation NomiEsb is a web-based management console for building, deploying, and operating .NET integration modules on Kubernetes. ![NomiEsb Dashboard](images/dashboard.png) ## In this guide The documentation is organised into five sections. Use the sidebar to navigate, or follow the links below. ### Getting Started What NomiEsb is, who it's for, and the concepts you need to know before installing. - [What is NomiEsb?](/docs/gs-introduction) - [Core concepts](/docs/gs-concepts) - [Roles and access](/docs/gs-roles) ### Installation Step-by-step administrator guide to install NomiEsb on a server, register the Entra ID application, prepare the database, and apply the license. - [Prerequisites](/docs/install-prerequisites) - [Database setup](/docs/install-database) - [Entra ID application and groups](/docs/install-entra-id) - [Encryption key and license file](/docs/install-secrets) - [appsettings.json reference](/docs/install-configuration) - [Running and verifying NomiEsb](/docs/install-run) ### Configuration Everything you configure in the console after installation: license, app settings, integrations, environments, users, and tokens. - [License](/docs/config-license) - [App settings](/docs/config-app-settings) - [Environments, systems and tags](/docs/config-environments) - [Integrations](/docs/config-integrations) - [Users and CLI licenses](/docs/config-users) - [API and MCP tokens](/docs/config-tokens) ### User Guide Page-by-page walkthrough of every screen in the console. - [Layout and navigation](/docs/guide-overview) - [Dashboard](/docs/guide-dashboard) - [Solutions, modules and libraries](/docs/guide-solutions) - [Dependency compliance and licenses](/docs/guide-dependencies) - [SBOM](/docs/guide-sbom) - [Release notes generation](/docs/guide-release-notes) - [Dependency vulnerabilities](/docs/guide-vulnerabilities) - [CI pipeline history](/docs/guide-ci-pipeline) - [Clusters and hosts](/docs/guide-clusters) - [Host configuration comparer](/docs/guide-host-compare) - [Settings](/docs/guide-settings) - [Profile, applications and notifications](/docs/guide-account) - [NomiEsb Lens](/docs/guide-lens) ### Infrastructure Setup Standing up the external systems NomiEsb talks to: CI/CD pipelines on each DevOps platform, ArgoCD on Kubernetes, container registries, and the NuGet feed where modules publish. #### Azure DevOps - [Pipeline variables](/docs/infra-ado-pipelines) - [NuGet feed](/docs/infra-ado-nuget-feed) - [Kubernetes environment](/docs/infra-ado-k8s-environment) - [Connect ArgoCD to repo](/docs/infra-ado-argocd-repo) #### GitHub - [Pipeline variables](/docs/infra-github-pipelines) - [NuGet feed](/docs/infra-github-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-github-argocd-repo) #### GitLab - [Pipeline variables](/docs/infra-gitlab-pipelines) - [NuGet feed](/docs/infra-gitlab-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-gitlab-argocd-repo) #### Bitbucket - [Pipeline variables](/docs/infra-bitbucket-pipelines) - [NuGet feed](/docs/infra-bitbucket-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-bitbucket-argocd-repo) #### ArgoCD - [Cluster setup](/docs/infra-argocd-cluster) #### Kubernetes - [Container registry auth](/docs/infra-k8s-registry) --- # NomiEsb Documentation NomiEsb is a web-based management console for building, deploying, and operating .NET integration modules on Kubernetes. ![NomiEsb Dashboard](images/dashboard.png) ## In this guide The documentation is organised into five sections. Use the sidebar to navigate, or follow the links below. ### Getting Started What NomiEsb is, who it's for, and the concepts you need to know before installing. - [What is NomiEsb?](/docs/gs-introduction) - [Core concepts](/docs/gs-concepts) - [Roles and access](/docs/gs-roles) ### Installation Step-by-step administrator guide to install NomiEsb on a server, register the Entra ID application, prepare the database, and apply the license. - [Prerequisites](/docs/install-prerequisites) - [Database setup](/docs/install-database) - [Entra ID application and groups](/docs/install-entra-id) - [Encryption key and license file](/docs/install-secrets) - [appsettings.json reference](/docs/install-configuration) - [Running and verifying NomiEsb](/docs/install-run) ### Configuration Everything you configure in the console after installation: license, app settings, integrations, environments, users, and tokens. - [License](/docs/config-license) - [App settings](/docs/config-app-settings) - [Environments, systems and tags](/docs/config-environments) - [Integrations](/docs/config-integrations) - [Users and CLI licenses](/docs/config-users) - [API and MCP tokens](/docs/config-tokens) ### User Guide Page-by-page walkthrough of every screen in the console. - [Layout and navigation](/docs/guide-overview) - [Dashboard](/docs/guide-dashboard) - [Solutions, modules and libraries](/docs/guide-solutions) - [Dependency compliance and licenses](/docs/guide-dependencies) - [SBOM](/docs/guide-sbom) - [Release notes generation](/docs/guide-release-notes) - [Dependency vulnerabilities](/docs/guide-vulnerabilities) - [CI pipeline history](/docs/guide-ci-pipeline) - [Clusters and hosts](/docs/guide-clusters) - [Host configuration comparer](/docs/guide-host-compare) - [Settings](/docs/guide-settings) - [Profile, applications and notifications](/docs/guide-account) - [NomiEsb Lens](/docs/guide-lens) ### Infrastructure Setup Standing up the external systems NomiEsb talks to: CI/CD pipelines on each DevOps platform, ArgoCD on Kubernetes, container registries, and the NuGet feed where modules publish. #### Azure DevOps - [Pipeline variables](/docs/infra-ado-pipelines) - [NuGet feed](/docs/infra-ado-nuget-feed) - [Kubernetes environment](/docs/infra-ado-k8s-environment) - [Connect ArgoCD to repo](/docs/infra-ado-argocd-repo) #### GitHub - [Pipeline variables](/docs/infra-github-pipelines) - [NuGet feed](/docs/infra-github-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-github-argocd-repo) #### GitLab - [Pipeline variables](/docs/infra-gitlab-pipelines) - [NuGet feed](/docs/infra-gitlab-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-gitlab-argocd-repo) #### Bitbucket - [Pipeline variables](/docs/infra-bitbucket-pipelines) - [NuGet feed](/docs/infra-bitbucket-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-bitbucket-argocd-repo) #### ArgoCD - [Cluster setup](/docs/infra-argocd-cluster) #### Kubernetes - [Container registry auth](/docs/infra-k8s-registry) --- # NomiEsb Documentation NomiEsb is a web-based management console for building, deploying, and operating .NET integration modules on Kubernetes. ![NomiEsb Dashboard](images/dashboard.png) ## In this guide The documentation is organised into five sections. Use the sidebar to navigate, or follow the links below. ### Getting Started What NomiEsb is, who it's for, and the concepts you need to know before installing. - [What is NomiEsb?](/docs/gs-introduction) - [Core concepts](/docs/gs-concepts) - [Roles and access](/docs/gs-roles) ### Installation Step-by-step administrator guide to install NomiEsb on a server, register the Entra ID application, prepare the database, and apply the license. - [Prerequisites](/docs/install-prerequisites) - [Database setup](/docs/install-database) - [Entra ID application and groups](/docs/install-entra-id) - [Encryption key and license file](/docs/install-secrets) - [appsettings.json reference](/docs/install-configuration) - [Running and verifying NomiEsb](/docs/install-run) ### Configuration Everything you configure in the console after installation: license, app settings, integrations, environments, users, and tokens. - [License](/docs/config-license) - [App settings](/docs/config-app-settings) - [Environments, systems and tags](/docs/config-environments) - [Integrations](/docs/config-integrations) - [Users and CLI licenses](/docs/config-users) - [API and MCP tokens](/docs/config-tokens) ### User Guide Page-by-page walkthrough of every screen in the console. - [Layout and navigation](/docs/guide-overview) - [Dashboard](/docs/guide-dashboard) - [Solutions, modules and libraries](/docs/guide-solutions) - [Dependency compliance and licenses](/docs/guide-dependencies) - [SBOM](/docs/guide-sbom) - [Release notes generation](/docs/guide-release-notes) - [Dependency vulnerabilities](/docs/guide-vulnerabilities) - [CI pipeline history](/docs/guide-ci-pipeline) - [Clusters and hosts](/docs/guide-clusters) - [Host configuration comparer](/docs/guide-host-compare) - [Settings](/docs/guide-settings) - [Profile, applications and notifications](/docs/guide-account) - [NomiEsb Lens](/docs/guide-lens) ### Infrastructure Setup Standing up the external systems NomiEsb talks to: CI/CD pipelines on each DevOps platform, ArgoCD on Kubernetes, container registries, and the NuGet feed where modules publish. #### Azure DevOps - [Pipeline variables](/docs/infra-ado-pipelines) - [NuGet feed](/docs/infra-ado-nuget-feed) - [Kubernetes environment](/docs/infra-ado-k8s-environment) - [Connect ArgoCD to repo](/docs/infra-ado-argocd-repo) #### GitHub - [Pipeline variables](/docs/infra-github-pipelines) - [NuGet feed](/docs/infra-github-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-github-argocd-repo) #### GitLab - [Pipeline variables](/docs/infra-gitlab-pipelines) - [NuGet feed](/docs/infra-gitlab-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-gitlab-argocd-repo) #### Bitbucket - [Pipeline variables](/docs/infra-bitbucket-pipelines) - [NuGet feed](/docs/infra-bitbucket-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-bitbucket-argocd-repo) #### ArgoCD - [Cluster setup](/docs/infra-argocd-cluster) #### Kubernetes - [Container registry auth](/docs/infra-k8s-registry) --- # NomiEsb Documentation NomiEsb is a web-based management console for building, deploying, and operating .NET integration modules on Kubernetes. ![NomiEsb Dashboard](images/dashboard.png) ## In this guide The documentation is organised into five sections. Use the sidebar to navigate, or follow the links below. ### Getting Started What NomiEsb is, who it's for, and the concepts you need to know before installing. - [What is NomiEsb?](/docs/gs-introduction) - [Core concepts](/docs/gs-concepts) - [Roles and access](/docs/gs-roles) ### Installation Step-by-step administrator guide to install NomiEsb on a server, register the Entra ID application, prepare the database, and apply the license. - [Prerequisites](/docs/install-prerequisites) - [Database setup](/docs/install-database) - [Entra ID application and groups](/docs/install-entra-id) - [Encryption key and license file](/docs/install-secrets) - [appsettings.json reference](/docs/install-configuration) - [Running and verifying NomiEsb](/docs/install-run) ### Configuration Everything you configure in the console after installation: license, app settings, integrations, environments, users, and tokens. - [License](/docs/config-license) - [App settings](/docs/config-app-settings) - [Environments, systems and tags](/docs/config-environments) - [Integrations](/docs/config-integrations) - [Users and CLI licenses](/docs/config-users) - [API and MCP tokens](/docs/config-tokens) ### User Guide Page-by-page walkthrough of every screen in the console. - [Layout and navigation](/docs/guide-overview) - [Dashboard](/docs/guide-dashboard) - [Solutions, modules and libraries](/docs/guide-solutions) - [Dependency compliance and licenses](/docs/guide-dependencies) - [SBOM](/docs/guide-sbom) - [Release notes generation](/docs/guide-release-notes) - [Dependency vulnerabilities](/docs/guide-vulnerabilities) - [CI pipeline history](/docs/guide-ci-pipeline) - [Clusters and hosts](/docs/guide-clusters) - [Host configuration comparer](/docs/guide-host-compare) - [Settings](/docs/guide-settings) - [Profile, applications and notifications](/docs/guide-account) - [NomiEsb Lens](/docs/guide-lens) ### Infrastructure Setup Standing up the external systems NomiEsb talks to: CI/CD pipelines on each DevOps platform, ArgoCD on Kubernetes, container registries, and the NuGet feed where modules publish. #### Azure DevOps - [Pipeline variables](/docs/infra-ado-pipelines) - [NuGet feed](/docs/infra-ado-nuget-feed) - [Kubernetes environment](/docs/infra-ado-k8s-environment) - [Connect ArgoCD to repo](/docs/infra-ado-argocd-repo) #### GitHub - [Pipeline variables](/docs/infra-github-pipelines) - [NuGet feed](/docs/infra-github-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-github-argocd-repo) #### GitLab - [Pipeline variables](/docs/infra-gitlab-pipelines) - [NuGet feed](/docs/infra-gitlab-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-gitlab-argocd-repo) #### Bitbucket - [Pipeline variables](/docs/infra-bitbucket-pipelines) - [NuGet feed](/docs/infra-bitbucket-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-bitbucket-argocd-repo) #### ArgoCD - [Cluster setup](/docs/infra-argocd-cluster) #### Kubernetes - [Container registry auth](/docs/infra-k8s-registry) --- # NomiEsb Documentation NomiEsb is a web-based management console for building, deploying, and operating .NET integration modules on Kubernetes. ![NomiEsb Dashboard](images/dashboard.png) ## In this guide The documentation is organised into five sections. Use the sidebar to navigate, or follow the links below. ### Getting Started What NomiEsb is, who it's for, and the concepts you need to know before installing. - [What is NomiEsb?](/docs/gs-introduction) - [Core concepts](/docs/gs-concepts) - [Roles and access](/docs/gs-roles) ### Installation Step-by-step administrator guide to install NomiEsb on a server, register the Entra ID application, prepare the database, and apply the license. - [Prerequisites](/docs/install-prerequisites) - [Database setup](/docs/install-database) - [Entra ID application and groups](/docs/install-entra-id) - [Encryption key and license file](/docs/install-secrets) - [appsettings.json reference](/docs/install-configuration) - [Running and verifying NomiEsb](/docs/install-run) ### Configuration Everything you configure in the console after installation: license, app settings, integrations, environments, users, and tokens. - [License](/docs/config-license) - [App settings](/docs/config-app-settings) - [Environments, systems and tags](/docs/config-environments) - [Integrations](/docs/config-integrations) - [Users and CLI licenses](/docs/config-users) - [API and MCP tokens](/docs/config-tokens) ### User Guide Page-by-page walkthrough of every screen in the console. - [Layout and navigation](/docs/guide-overview) - [Dashboard](/docs/guide-dashboard) - [Solutions, modules and libraries](/docs/guide-solutions) - [Dependency compliance and licenses](/docs/guide-dependencies) - [SBOM](/docs/guide-sbom) - [Release notes generation](/docs/guide-release-notes) - [Dependency vulnerabilities](/docs/guide-vulnerabilities) - [CI pipeline history](/docs/guide-ci-pipeline) - [Clusters and hosts](/docs/guide-clusters) - [Host configuration comparer](/docs/guide-host-compare) - [Settings](/docs/guide-settings) - [Profile, applications and notifications](/docs/guide-account) - [NomiEsb Lens](/docs/guide-lens) ### Infrastructure Setup Standing up the external systems NomiEsb talks to: CI/CD pipelines on each DevOps platform, ArgoCD on Kubernetes, container registries, and the NuGet feed where modules publish. #### Azure DevOps - [Pipeline variables](/docs/infra-ado-pipelines) - [NuGet feed](/docs/infra-ado-nuget-feed) - [Kubernetes environment](/docs/infra-ado-k8s-environment) - [Connect ArgoCD to repo](/docs/infra-ado-argocd-repo) #### GitHub - [Pipeline variables](/docs/infra-github-pipelines) - [NuGet feed](/docs/infra-github-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-github-argocd-repo) #### GitLab - [Pipeline variables](/docs/infra-gitlab-pipelines) - [NuGet feed](/docs/infra-gitlab-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-gitlab-argocd-repo) #### Bitbucket - [Pipeline variables](/docs/infra-bitbucket-pipelines) - [NuGet feed](/docs/infra-bitbucket-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-bitbucket-argocd-repo) #### ArgoCD - [Cluster setup](/docs/infra-argocd-cluster) #### Kubernetes - [Container registry auth](/docs/infra-k8s-registry) --- # NomiEsb Documentation NomiEsb is a web-based management console for building, deploying, and operating .NET integration modules on Kubernetes. ![NomiEsb Dashboard](images/dashboard.png) ## In this guide The documentation is organised into five sections. Use the sidebar to navigate, or follow the links below. ### Getting Started What NomiEsb is, who it's for, and the concepts you need to know before installing. - [What is NomiEsb?](/docs/gs-introduction) - [Core concepts](/docs/gs-concepts) - [Roles and access](/docs/gs-roles) ### Installation Step-by-step administrator guide to install NomiEsb on a server, register the Entra ID application, prepare the database, and apply the license. - [Prerequisites](/docs/install-prerequisites) - [Database setup](/docs/install-database) - [Entra ID application and groups](/docs/install-entra-id) - [Encryption key and license file](/docs/install-secrets) - [appsettings.json reference](/docs/install-configuration) - [Running and verifying NomiEsb](/docs/install-run) ### Configuration Everything you configure in the console after installation: license, app settings, integrations, environments, users, and tokens. - [License](/docs/config-license) - [App settings](/docs/config-app-settings) - [Environments, systems and tags](/docs/config-environments) - [Integrations](/docs/config-integrations) - [Users and CLI licenses](/docs/config-users) - [API and MCP tokens](/docs/config-tokens) ### User Guide Page-by-page walkthrough of every screen in the console. - [Layout and navigation](/docs/guide-overview) - [Dashboard](/docs/guide-dashboard) - [Solutions, modules and libraries](/docs/guide-solutions) - [Dependency compliance and licenses](/docs/guide-dependencies) - [SBOM](/docs/guide-sbom) - [Release notes generation](/docs/guide-release-notes) - [Dependency vulnerabilities](/docs/guide-vulnerabilities) - [CI pipeline history](/docs/guide-ci-pipeline) - [Clusters and hosts](/docs/guide-clusters) - [Host configuration comparer](/docs/guide-host-compare) - [Settings](/docs/guide-settings) - [Profile, applications and notifications](/docs/guide-account) - [NomiEsb Lens](/docs/guide-lens) ### Infrastructure Setup Standing up the external systems NomiEsb talks to: CI/CD pipelines on each DevOps platform, ArgoCD on Kubernetes, container registries, and the NuGet feed where modules publish. #### Azure DevOps - [Pipeline variables](/docs/infra-ado-pipelines) - [NuGet feed](/docs/infra-ado-nuget-feed) - [Kubernetes environment](/docs/infra-ado-k8s-environment) - [Connect ArgoCD to repo](/docs/infra-ado-argocd-repo) #### GitHub - [Pipeline variables](/docs/infra-github-pipelines) - [NuGet feed](/docs/infra-github-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-github-argocd-repo) #### GitLab - [Pipeline variables](/docs/infra-gitlab-pipelines) - [NuGet feed](/docs/infra-gitlab-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-gitlab-argocd-repo) #### Bitbucket - [Pipeline variables](/docs/infra-bitbucket-pipelines) - [NuGet feed](/docs/infra-bitbucket-nuget-feed) - [Connect ArgoCD to repo](/docs/infra-bitbucket-argocd-repo) #### ArgoCD - [Cluster setup](/docs/infra-argocd-cluster) #### Kubernetes - [Container registry auth](/docs/infra-k8s-registry)