Primitives-First: Why Nexigon Doesn't (Yet) Ship a Magic Update Button
Most fleet management platforms give you a fixed set of convenience features. When your requirements outgrow them, you end up with brittle workarounds. We took a different path: building Nexigon around composable primitives that let you construct exactly the workflows you need.
You sign up for a fleet management platform. It has a nice dashboard, a one-click deploy button, and a handful of integrations. It solves the problems you think you have right now.
Six months in, the real requirements show up. You need coordinated multi-stage rollouts where a gateway updates its child devices before updating itself. Or conditional updates that check a device-side health score before proceeding, report the result back to the cloud, and wait for manual approval under certain conditions. Your configuration management needs to reconcile desired state from multiple sources. Your telemetry needs to flow into an existing observability stack rather than a platform-specific dashboard. The platform does not support any of this. So you start building workarounds: scripts that fight the platform’s convenience features and a growing pile of glue code that nobody wants to maintain.
This is the inevitable consequence of building a platform around convenience features instead of composable primitives with convenience layered on top.
The Cost of Convenience-First
Most fleet management platforms take the convenience-first approach. They give you a “Deploy Update” button that handles everything behind the scenes: artifact delivery, installation, rollback, status tracking. It works well as long as your needs fit within the boundaries they have drawn.
To be fair, these platforms are typically not completely rigid. Many offer extension points like state scripts or lifecycle hooks that let you run custom logic at predefined stages of the update process. That helps with straightforward customizations.
But the underlying workflow is still theirs. The hooks run within a frame the platform defines. When you need to fundamentally change the orchestration, coordinate updates across parent and child devices, tie rollout decisions to business logic that lives outside the platform, or integrate an update framework they did not anticipate, you are working against the grain. You are extending someone else’s architecture rather than building your own.
And this is not just about updates. The same pattern applies to configuration management, telemetry, and device authentication. Each of these is typically a self-contained feature with its own assumptions baked in. When your requirements cross the boundaries between them, or simply do not match what the platform anticipated, you are back to workarounds.
The convenience is real, but it comes with a structural constraint: the platform decides what the workflows look like, and you fill in the gaps.
Primitives-First
Nexigon is built on a different premise. Instead of shipping opinionated features, we provide a set of well-defined primitives that you compose into exactly the workflows your deployment requires.
These primitives are:
- Properties: Key-value pairs attached to each device, readable and writable from both the cloud and the device itself. Protected properties flow from cloud to device. Unprotected properties flow in both directions.
- Events: Structured telemetry messages with severity levels, categories, and arbitrary attributes. Emitted from the device, observable in the cloud.
- Repositories: Versioned artifact storage with packages, semantic tags (locked and floating), and content-addressable assets. Bring your own S3-compatible storage.
- Device Tokens: Short-lived, scoped credentials that let device-side applications authenticate against external services through Nexigon.
None of these primitives know about OTA updates. None of them know about remote configuration. None of them know about monitoring dashboards. They are general-purpose building blocks, and that is exactly the point.
An OTA Update Mechanism Built from Primitives
To make this concrete, let us look at how OTA updates actually work with Nexigon and Rugix. There is no built-in update orchestrator. Instead, the entire update lifecycle is a shell script that composes the primitives above. Here it is, walking through each step.
Step 1: Read the Update Configuration
The script reads where to look for updates from a device property. The key piece of configuration is a path that points to a floating tag of a package, for example my-repo/my-os/latest. This is not a reference to a a specific artifact. It points to whatever version the tag currently resolves to, so promoting a new release is just reassigning the tag.
OTA_CONFIG=$(nexigon-agent device properties get "dev.nexigon.ota.config")
if [ "$(echo "$OTA_CONFIG" | jq -r '.result')" == "NotFound" ]; then
# Fall back to defaults baked into the image
source /etc/nexigon-rugix-ota.conf
OTA_CONFIG=$(jq -n --arg path "$VERSION_PATH" '{ "path": $path }')
else
OTA_CONFIG=$(echo "$OTA_CONFIG" | jq -r '.value')
fi
Because the configuration lives in a property, you can change a device’s update channel from the cloud at any time. Point it at a testing tag, a stable tag, or a completely different repository path. This is how you build canary groups and staged rollouts with Nexigon: point a handful of canary devices at my-repo/my-os/canary, let the rest follow my-repo/my-os/stable, and promote a release by reassigning the floating tag once you are confident. Moving a single device between channels is just setting a property.
Step 2: Track State Transparently
The script maintains its own state in another device property:
OTA_STATUS=$(jq -n \
--arg current "$CURRENT_VERSION" \
--arg target "$TARGET_VERSION" \
--arg state "installing" \
'{ "currentVersion": $current, "targetVersion": $target, "state": $state }')
nexigon-agent device properties set "dev.nexigon.ota.status" "$OTA_STATUS"
This means the update status is not locked to a platform-specific database schema. It is a JSON value associated with the device that you can read from the CLI, query from the API, or display in your own dashboard. The state model is yours to define. Add fields, change the schema, track whatever matters to your deployment. Need to record a failure reason, a retry count, or the duration of the last install? Just add it to the JSON. Need to build a dashboard that shows which devices are mid-update? Query the property across your fleet. The data model is not something we dictate. It is something you own and extend as your requirements evolve.
That said, we do not leave you to figure everything out from scratch. As we see useful patterns emerge from real deployments, we standardize them. The dev.nexigon.ota.status property structure used by the Rugix OTA recipe is one example: it follows a schema that the Nexigon UI understands, so you get an update status dashboard out of the box. But the dashboard reads the same property your scripts write. If you extend the schema with custom fields, the dashboard still works. If you replace the schema entirely for a use case we did not anticipate, the primitives still work. Convenience is layered on top, not baked in at the foundation.
Step 3: Emit Events at Every Stage
Every meaningful transition emits an event with structured attributes:
nexigon-agent events emit --category "dev.nexigon.ota" \
--attribute "currentVersion=$(jq -n --arg v "$CURRENT_VERSION" '$v')" \
--attribute "targetVersion=$(jq -n --arg v "$TARGET_VERSION" '$v')" \
'"update available, installing"'
There is an event for “checking for updates”, “update available”, “installing”, “rebooting”, “completed”, and “failed”. Each carries the relevant context as attributes. You are not parsing log files. You are working with structured telemetry.
Our event system is inspired by OpenTelemetry, and we plan to add first-class OTLP export so you can hook up standard monitoring tools like Grafana or Datadog directly. The key difference from having devices push OTLP to a backend directly is that Nexigon sits in between: it filters, enriches, and annotates events with trusted device identity and metadata before forwarding them. You get the openness of a standard protocol with the security guarantees of a platform specifically built for the challenges of managing IoT devices at scale.
Step 4: Fetch Artifacts from Repositories
The script resolves artifact URLs through the repository system:
VERSION_PATH=$(echo "$OTA_CONFIG" | jq -r '.path')
BUNDLE_URL=$(nexigon-agent repositories issue-url \
"$VERSION_PATH/$SYSTEM_NAME.rugixb" | jq -r '.url')
rugix-ctrl update install --reboot no "$BUNDLE_URL"
The issue-url command returns a signed, time-limited download URL for the artifact stored in your own S3-compatible storage. The script then hands that URL to Rugix for the actual A/B system update. But it could just as easily hand it to RAUC, SWUpdate, or a custom installer. Nexigon does not care what you do with the URL. It just gives you authenticated access to your artifacts. What you do with them is entirely up to you.
Step 5: Handle Failures Explicitly
When something goes wrong, the script detects it and reports through the same primitives:
OTA_STATUS=$(echo "$OTA_STATUS" | jq '.state = "failed" | .active = false')
nexigon-agent events emit --category "dev.nexigon.ota" \
--severity "error" \
'"update failed"'
nexigon-agent device properties set \
"dev.nexigon.ota.status" "$OTA_STATUS"
The failure is visible as a property (queryable) and an event (alertable). There is no hidden retry logic. No silent swallowing of errors. If you want automatic retries, you add them. If you want to escalate to a human after three failures, you build that. The script is yours.
Why This Matters
The entire update script is about 150 lines of Bash. You can read it, understand it, modify it, and debug it. There is no framework to learn, no plugin API to implement, no YAML dialect to master. And Bash is just what we chose for this recipe. The Nexigon Agent exposes its primitives through a local CLI, so you can interact with it from any language. We are also working on SDKs that will make this even more natural for languages like Python, Rust, and TypeScript.
The same primitives are also accessible from the cloud side through the Nexigon API. Set properties, query events, manage repository tags, all programmatically. This is the natural integration point for CI/CD pipelines, custom dashboards, and automation tooling. The API uses the same building blocks as the on-device agent, so everything you build stays consistent. For a practical example, take a look at the upload-release and stabilize-release scripts in our Rugix template, which use the API to automate the release lifecycle from your CI pipeline.
Every piece of the workflow is observable from the outside. The update configuration, the current state, and every state transition are all expressed through properties and events, the same primitives everything else in Nexigon uses. Your monitoring, your alerting, and your automation all work the same way whether they are watching OTA updates, device configuration changes, or application health.
This also means less vendor lock-in. The primitives are simple, well-defined concepts: key-value properties, structured events, versioned artifact storage. Your update script does not call a proprietary orchestration API. It reads properties, emits events, and downloads artifacts. If you ever need to move away from Nexigon, the logic you built is yours. There is no platform-specific framework to untangle.
This is what we mean by primitives-first. At its core, the OTA update mechanism is not a feature we shipped. It is a pattern we documented, a recipe built from the same ingredients available to everyone using the platform. If it does not match your needs, you do not file a feature request, wait, and hope. You simply write a different script.
Where the Convenience Comes In
You might wonder: does primitives-first mean you always have to build everything yourself? No. The point is not to avoid convenience. It is to build convenience on the right foundation while leaving the door wide open for customizations.
We are actively working on higher-level features: pre-built update workflows, a visual rollout manager, one-click deployment recipes. But every one of these will be built on the same primitives described in this post. The “Deploy Update” button is coming. When it arrives, it will write properties, emit events, and fetch artifacts from repositories, just like the shell script above. If it does exactly what you need, use it. If it does not, you can look under the hood, understand what it does, and build your own version. That is the difference.
Getting Started
The OTA update recipe described here is available as part of the nexigon-rugix integration. You can read the full update script on GitHub. It is designed for Rugix-based systems but serves as a reference for building update workflows on any Linux-based device.
If you want to explore the primitives yourself, the Nexigon documentation covers properties, events, repositories, and device tokens in detail. You can also sign up for a free trial and start building on the primitives today.