Nuvaka › Developer docs › Program compatibility
Program compatibility and dependencies
Every program package must declare where it runs (compat), and every version must declare what it needs (dependencies). A version with missing declarations cannot be submitted for review. The store uses them to show a summary such as "Ubuntu 22.04+, Fedora 39+ · x86_64" on the card. The app uses them to pick the right package for the device and to tell the user about missing dependencies.
Compatibility declaration (compat)
compat is given per package, i.e. once for each platform key. There are two ways:
- while uploading, in the
?compat=query field as URL-encoded JSON; - later, with
PUT /api/programs/dev/{id}/versions/{v}/packages/{platform}/compatand the JSON as the body.
The server validates and normalizes the declaration. The normalized declaration goes into the package record and is signed.
Format
// Windows: windows-msi, windows-exe, windows-exe-arm64, windows-msix
{ "arch": "x86_64", "os": { "minVersion": "10.0.19041" },
"launch": { "uninstallKey": "Nuvaka GPU Control", "exe": "bin\\GpuControl.exe" }, "processName": "GpuControl.exe" }
// macOS: macos-dmg
{ "arch": "universal", "os": { "minVersion": "12.0" }, "launch": { "bundleId": "com.nuvaka.gpucontrol" } }
// Android: android-apk
{ "arch": "universal", "os": { "minSdk": 26 } }
// Linux package managers: linux-deb, linux-rpm, linux-pacman
{ "arch": "x86_64", "linux": { "distros": [ { "id": "ubuntu", "min": "22.04" }, { "id": "debian", "min": "12" } ] },
"launch": { "desktopId": "gpu-control.desktop" }, "processName": "gpu-control" }
// AppImage: linux-appimage ("any distribution with glibc 2.31 or newer")
{ "arch": "x86_64", "linux": { "glibc": "2.31" }, "launch": { "desktopId": "gpu-control.desktop" } }
| Field | Rule |
|---|---|
arch | x86_64, arm64 or universal. windows-exe implies x86_64 and windows-exe-arm64 implies arm64: if you leave it out these values are used, and a different value is an error. Required for every other key. |
os.minVersion | Required on Windows and macOS. For Windows, the full build number: 10.0.17763 (1809), 10.0.19041 (2004), 10.0.22000 (Windows 11). For macOS, values such as 11, 12.0, 13.3. |
os.minSdk | Required on Android. The API level, an integer from 16 to 99 (e.g. 26 = Android 8.0). |
linux.distros | Required for deb, rpm and pacman packages: 1–20 items, each { id, min? }. id is the ID value from /etc/os-release and must belong to the key's family (table below). min is the lowest VERSION_ID. Without min, every release of the distribution is accepted; leave it out for rolling distributions such as Arch. On AppImage the field is optional and informational only. |
linux.glibc | In the form 2.31. Required for AppImage, optional for other Linux packages. ldd --version shows the glibc version of your build system. |
Unknown fields are rejected. Linux keys allow arch, linux, launch, processName, packageName, replaces, ui, bridge and background; Windows and macOS allow arch, os, launch, processName, ui, bridge and background; Android allows only arch and os. ui, bridge and background exist only in desktop packages (Local endpoints). The old minOsVersion field is derived from compat: minVersion on Windows and macOS, minSdk on Android, empty on Linux.
Launch details and process name
launch is required for desktop packages (Windows, macOS, Linux). The app starts the program only from these details and never uses a path supplied by an extension.
| Package | launch | Rule |
|---|---|---|
linux-* | { "desktopId": "gpu-control.desktop" } | The name (not path) of the .desktop file the package installs. |
windows-* | { "uninstallKey": "Nuvaka GPU Control", "exe": "bin\GpuControl.exe" } | uninstallKey: the key name under HKLM/HKCU …\Uninstall or the DisplayName (1–200). exe: a relative .exe path under InstallLocation; no .., no drive letter and no leading \ or /. |
macos-dmg | { "bundleId": "com.nuvaka.gpucontrol" } | The app's bundle identifier (reverse DNS). |
android-apk | none | No launch on Android. |
processName is optional: the process name (not a path) used for a "running" status, e.g. GpuControl.exe or gpu-control. Both are part of compat: they go into the signed package record and are returned in packages[].compat in the detail reply. Error paths include compat.launch, compat.launch.exe and compat.processName.
Distribution families
| Key | Allowed id values |
|---|---|
linux-deb | debian, ubuntu, linuxmint, pop, elementary, zorin, kali, raspbian, deepin, mx, neon, devuan, pureos, tuxedo |
linux-rpm | fedora, rhel, centos, rocky, almalinux, ol, opensuse-leap, opensuse-tumbleweed, sles, nobara, amzn, ultramarine |
linux-pacman | arch, manjaro, endeavouros, garuda, cachyos, artix, steamos |
A distribution outside the key's family is rejected. For example, fedora in a deb package returns:
{ "success": false, "error": "compat_invalid", "path": "compat.linux.distros[1].id",
"message": "fedora bu paket türüne uymaz; linux-deb için: debian, ubuntu, linuxmint, …" }
Error messages are in Turkish; rely on error and path in your tooling.
Example: five packages in one version
launch is left out for brevity; every desktop package needs it.
// windows-msi
{ "arch": "x86_64", "os": { "minVersion": "10.0.19041" } }
// windows-exe-arm64 (arch implied: arm64)
{ "os": { "minVersion": "10.0.22000" } }
// linux-deb
{ "arch": "x86_64", "linux": { "distros": [ { "id": "ubuntu", "min": "22.04" }, { "id": "debian", "min": "12" }, { "id": "linuxmint", "min": "21" } ] } }
// linux-rpm
{ "arch": "x86_64", "linux": { "distros": [ { "id": "fedora", "min": "39" }, { "id": "opensuse-tumbleweed" } ] } }
// linux-pacman
{ "arch": "x86_64", "linux": { "distros": [ { "id": "arch" }, { "id": "manjaro" }, { "id": "endeavouros" } ] } }
Local endpoints: ui, bridge, background
The program's UI and widget connect to the app through local endpoints. They are declared per package in compat: they are part of the signed package record, and their paths differ per operating system. They exist only in desktop packages (Windows, macOS, Linux); on Android these fields count as unknown fields and are rejected.
ui: the program's UI that opens inside the Nuvaka window. Required in every desktop package of arunMode:"embedded"program.bridge: the bridge through which the companion extension's widget talks to the program. Required in every desktop package of awidget.kind:"extension"version.background: a background part the app can start with the user's consent.
Embedded UI (ui)
// recommended: unix socket or named pipe (no port hijacking, no token needed)
"ui": { "transport": "unix", "path": "runtime:gpu-control-ui.sock", "args": ["--serve"] } // Linux, macOS
"ui": { "transport": "pipe", "pipe": "\\\\.\\pipe\\nuvaka-gpu-control-ui", "args": ["--serve"] } // Windows
// http: token file (handshake "file", the default with portFile)
"ui": { "transport": "http", "portFile": "gpu-control-ui.json" }
// http: handshake through an environment variable (the app starts the UI server)
"ui": { "transport": "http", "port": 47123, "handshake": "env", "args": ["--serve"], "urlPath": "/app" }
| Field | Rule |
|---|---|
transport | unix (Linux, macOS), pipe (Windows only) or http. unix and pipe are recommended. |
path (unix) | "/run/<name>.sock", "/run/<dir>/<name>.sock" (on macOS /var/run/…) or "runtime:<name>.sock" / "runtime:<dir>/<name>.sock". Each part matches ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$. The path ends in .sock, is at most 100 characters and contains no ... For the UI, runtime:<name>-ui.sock is recommended. |
pipe (pipe) | "\\.\pipe\<name>"; the name matches [A-Za-z0-9][A-Za-z0-9._-]{0,99}. Escape the backslashes in JSON. |
port or portFile (http) | Only one of them. port is 1024–65535. portFile: a .json file name relative to the runtime directory, at most one subdirectory ("gpu-control/ui.json"); no absolute path, no \ and no ... |
handshake (http) | file (default; needs portFile) or env. See below. |
urlPath | Optional start path. Starts with /, uses [A-Za-z0-9._~/-], at most 200 characters, no .. and no //. Default /. |
args | Optional, at most 16 strings (each at most 256 characters, no NUL). If given, the app starts the UI server: the compat.launch target with these arguments, with the user's rights (never as root or administrator). If not given, the program opens its UI server itself. |
Per-user runtime directory. runtime: paths and portFile resolve against it:
- Linux:
$XDG_RUNTIME_DIR(otherwise/run/user/<uid>); - Windows:
%LOCALAPPDATA%\Nuvaka\run; - macOS:
$TMPDIR(getconf DARWIN_USER_TEMP_DIR).
The app rejects any path that leaves this root (symbolic links included).
http handshake (required). It protects against port hijacking and DNS rebinding:
handshake:"file": the program's own user-level UI server (never a root service) writesportFilewith mode 0600:{ "port": 47123, "token": "<at least 32 random bytes, base64url>" }. The app checks that the file is owned by the user and not open to group or others.handshake:"env": the app starts the UI server itself (withargs) and putsNUVAKA_UI_TOKEN=<random>into its environment. Before the view opens, an unauthenticatedGET http://127.0.0.1:<port>/.well-known/nuvaka-uimust return{ "token": "<NUVAKA_UI_TOKEN>" }; otherwise nothing is shown. When used withportFile, the file may contain only{ "port" }.- In both modes the app proxies requests through its own private scheme, with
Host: 127.0.0.1:<port>andAuthorization: Bearer <token>. The token never reaches the view (webview). The program's server must reject any otherHostvalue and requests without the token (401 or 421), and must bind only to127.0.0.1. - unix sockets and pipes have no token; endpoint ownership is checked instead (below).
Widget bridge (bridge)
// Linux: system service (gpud)
"bridge": { "source": "service", "transport": "unix", "path": "/run/nuvaka-gpud/gpud.sock", "protocol": "jsonl-v1",
"hello": { "cmd": "hello", "proto_version": 1 },
"methods": [ { "name": "status", "readOnly": true }, { "name": "set_profile", "readOnly": false, "rateLimitPerMin": 30 } ] }
// Windows
"bridge": { "source": "service", "transport": "pipe", "pipe": "\\\\.\\pipe\\nuvaka-gpud", "protocol": "jsonl-v1", "methods": [ … ] }
// background part and a user socket
"background": { "args": ["--background"] },
"bridge": { "source": "background", "transport": "unix", "path": "runtime:gpu-control.sock", "protocol": "jsonl-v1", "methods": [ … ] }
| Field | Rule |
|---|---|
source | service: the package installs an always-running OS service (systemd unit, Windows service, launchd). background: with the user's consent, the app can start the program's background part when Nuvaka opens; the package must declare compat.background (otherwise compat_invalid, path: compat.background). |
transport and endpoint | As in ui (path, pipe, port or portFile and handshake). source:"service" uses only unix or pipe, because a system service cannot provide the user's token. http is allowed only with background. |
protocol | "jsonl-v1" (the only value). |
methods | 1–32 items { name, readOnly, rateLimitPerMin? }. name matches ^[a-z][a-z0-9_]{0,31}$ and is unique. readOnly is a required bool. rateLimitPerMin is 1–600; without it the app default applies (read 120, write 30). The app rejects any method not in the list (method_not_allowed). |
hello | Optional JSON object, at most 1 KB. The app sends it unchanged as the first line of every connection and waits for one reply line (e.g. {"cmd":"hello","proto_version":1}). If no reply comes or the reply contains error, the endpoint counts as bridge_unavailable. |
Bridge contract (jsonl-v1) and app limits
The extension calls the bridge with nuvaka.programs.call(method, params?), which returns a Promise. The call works only in an extension that has companionOf and the programs: "companion" permission, and that the program's installed version record names in widget.kind:"extension" with widget.extensionId (Home page widget). The app builds the request itself; the extension cannot send raw lines.
Framing: UTF-8, one JSON object per line, each line ends with \n (no raw newlines inside a line). Requests on a connection are sequential; every request line gets one reply line.
→ {"cmd":"hello","proto_version":1} // if hello is set: the connection's first line (unchanged)
← {"ok":true}
→ {"id":1,"method":"status","params":{}}
← {"id":1,"result":{"temp":61,"profile":"quiet"}}
→ {"id":2,"method":"set_profile","params":{"name":"perf"}}
← {"id":2,"error":{"code":"busy","message":"…"}}
- Over http (only
source:"background"): each request line is the body of aPOST /.well-known/nuvaka-bridge; the reply body is a single line. The handshake and theHost/Bearerrules are the same as forui. - Limits:
paramsat most 64 KB, a reply line at most 1 MB, reply timeout 5 s.rateLimitPerMinis enforced per method in the app. - Write methods (
readOnly:false) can be called only after a user gesture (a click or key press, within about 1 s); otherwisegesture_required. - Endpoint ownership: a unix socket must be owned by root or the user and must not be world-writable (
runtime:sockets belong to the user; 0600 is recommended). On Windows, the pipe server's process (GetNamedPipeServerProcessId) must be an image under the program'sInstallLocation. If this does not hold, the app does not connect (bridge_unavailable).
Errors arrive as a Promise rejection, with the code in error.code:
| Code | Meaning |
|---|---|
bridge_unavailable | No endpoint, connection failed, ownership or handshake did not match, or hello was rejected. The widget draws the standard "Program/service not running · Start" state (Widget standard). |
method_not_allowed | The method is not in methods. |
gesture_required | A write method was called without a user gesture. |
rate_limited | The method's rateLimitPerMin was exceeded. |
timeout | No reply within 5 s. |
too_large | params or the reply exceeded its limit. |
bad_response | The reply line is not valid. |
not_authorized | The extension is not allowed to call this program's bridge. |
An error code returned by the program itself (e.g. busy) arrives unchanged in error.code, without a prefix.
Background part (background)
"background": { "args": ["--background"], "persist": false }
args: 1–16 strings (each at most 256 characters, no NUL).persist: optional bool, defaultfalse.- The app starts only the
compat.launchtarget with these arguments: the Exec line of the.desktopfile on Linux (field codes removed),InstallLocation\exeon Windows, thebundleIdon macOS. It does so without root or administrator rights and only after the user's explicit consent ("Let GPU Control run in the background?"); the user can withdraw it in settings. - The background part starts when Nuvaka opens and stops when Nuvaka closes. With
persist:trueit keeps running.
Dependencies
dependencies are given per version: when you create the version with POST …/versions, or later with PUT …/versions/{v}. The field is required. If there are no dependencies, send the empty list []; it explicitly means "no dependencies". If you never send the field, the version cannot be submitted (dependencies_required).
[
{ "kind": "driver", "name": "NVIDIA driver", "version": ">=535", "required": false, "platforms": ["windows", "linux"],
"note": { "tr": "Yalnız NVIDIA ekran kartlarında", "en": "NVIDIA GPUs only" }, "url": "https://www.nvidia.com/drivers" },
{ "kind": "hardware", "name": "NVIDIA or AMD GPU", "required": true },
{ "kind": "runtime", "name": ".NET Desktop Runtime", "version": ">=8.0", "required": true, "platforms": ["windows"],
"url": "https://dotnet.microsoft.com/download/dotnet/8.0" },
{ "kind": "program", "name": "nuvaka.gpu-runtime", "version": ">=1.0.0", "required": true }
]
| Field | Rule |
|---|---|
kind | driver, runtime, hardware, program (another Nuvaka program) or system (an OS feature, e.g. WSL, systemd). |
name | 1–100 characters. For kind: program it must be the id of a published and listed program in the store. A program cannot depend on itself. |
version | Optional constraint such as >=535, 8.0 or >=1.2.0. It may start with >=, <=, >, <, =, ~ or ^; at most 40 characters. |
required | Required, true or false. false means "recommended". |
note | Optional { tr, en }, 1–300 characters per language. |
url | Optional https address (download or information page). |
platforms | Optional. The platform keys or operating systems (windows, linux…) the dependency applies to. Without it, the dependency applies everywhere. |
- Up to 30 dependencies. The same
kindandnamepair cannot appear twice. Unknown fields are rejected. - Declare Linux system libraries in the package's own dependency field (
Depends:,Requires:,depends=()), not here. The package manager installs those. This list is for things the package manager cannot install. - For a
programdependency, the app offers to install that program first. Its package is picked by its own compat declaration. If the program you depend on is later taken down, submitting and approving new versions is refused.
Error codes
| Code | When |
|---|---|
400 compat_invalid + path | compat is invalid. path is the JSON path of the bad field, such as compat.arch, compat.os.minVersion, compat.linux.distros[1].id or compat.linux.glibc. For local endpoints: compat.ui, compat.ui.transport|path|pipe|port|portFile|handshake|urlPath|args|args[i]; compat.bridge, compat.bridge.source|transport|path|pipe|port|portFile|handshake|protocol|methods|methods[i]|methods[i].name|methods[i].readOnly|methods[i].rateLimitPerMin|hello; compat.background, compat.background.args|args[i]|persist. |
400 compat_required + platforms | On submit or approval, some packages have no compat. |
400 dependencies_invalid + path | The dependency list is invalid or a program dependency was not found, e.g. dependencies[0].kind, dependencies[2].name. |
400 dependencies_required | The version never declared the dependencies field. |
400 ui_required + platforms | The program is runMode:"embedded" and these desktop packages have no compat.ui. On submit and approval. |
400 widget_invalid, reason: bridge_required | The widget is kind:"extension" and a desktop package has no compat.bridge: the reply carries path: "packages.<platform>.compat.bridge" and platforms. If the version has no desktop package at all, path: "packages". On submit and approval. |
409 not_editable | compat or dependencies of a version in review or published cannot be changed. |
What the store and the app see
List cards (GET /api/programs) carry a per-OS summary built from the latest version's packages:
"compat": {
"windows": { "arch": ["x86_64", "arm64"], "minVersion": "10.0.19041" },
"linux": { "arch": ["x86_64"], "distros": [ { "id": "ubuntu", "min": "22.04" }, { "id": "fedora", "min": "39" } ], "glibc": "2.31", "anyDistro": true },
"android": { "arch": ["universal"], "minSdk": 26 }
}
The values are the lowest across the packages. anyDistro: true means there is an AppImage package, so the program runs on any distribution with a recent enough glibc. The detail endpoint (GET /api/programs/{id}) returns each package's full compat, each version's dependencies and the latest version's dependencies, with a store card attached to program dependencies.
Matching rules
The app can decide on the device. It can also ask the server: add the device details to the detail request and the reply contains a compatibility field.
GET /api/programs/{id}?platform=linux&arch=x86_64&distro=ubuntu&distroVersion=24.04&distroLike=debian&glibc=2.39
"compatibility": {
"compatible": true, "recommended": "linux-deb",
"packages": [
{ "platform": "linux-appimage", "compatible": true, "reasons": [], "warnings": [] },
{ "platform": "linux-deb", "compatible": true, "reasons": [], "warnings": [] },
{ "platform": "linux-rpm", "compatible": false, "reasons": ["distro_not_supported"], "warnings": [] }
]
}
Parameters:
arch:x86_64(amd64andx64also work) orarm64(aarch64also works).osVersion: the build number on Windows (10.0.22631), the version on macOS (14.5), the API level on Android (34).distro,distroVersion,distroLike: theID,VERSION_IDandID_LIKEvalues from/etc/os-release.glibc: the device's glibc version.
Details you leave out are not checked. The rules apply to each package separately:
- Architecture: a
universalpackage runs on every architecture. The same architecture fits. arm64 Windows and Apple Silicon Macs run x86_64 packages under emulation: the package fits and the warningarch_emulatedis added. Any other difference fails witharch_mismatch. - Windows and macOS: if the device version is lower than
minVersion, the reason isos_too_old. Versions are compared part by part as dotted numbers. - Android: if the API level is lower than
minSdk, the reason isos_too_old. - glibc: if the device's glibc is older than the declared one, the reason is
glibc_too_old. - deb, rpm, pacman:
- If the distribution is listed and has a
min, the device version must be at leastmin; otherwisedistro_too_old. If the version is unknown, the warningdistro_version_unknownis added. - If the distribution is not listed, but it or one of its
ID_LIKEvalues belongs to the package's family, the package fits and the warningdistro_family_matchis added. The version is not checked in this case. Example: Linux Mint when only ubuntu is listed. - Otherwise
distro_not_supported.
- If the distribution is listed and has a
- AppImage: the distribution is ignored; only the architecture and glibc are checked.
recommended is picked among the fitting packages: exact matches first, then family matches and emulation, AppImage last. Ties follow the order of the platform key list. If no package fits, compatible is false and recommended is null.
Nuvaka Apps API v1 · last updated 2026-10-17