diff --git a/lib/Cargo.toml b/lib/Cargo.toml
index eaf785e74..04189139c 100644
--- a/lib/Cargo.toml
+++ b/lib/Cargo.toml
@@ -19,6 +19,8 @@ hex = "^0.4"
 fn-error-context = "0.2.0"
 gvariant = "0.4.0"
 indicatif = "0.17.0"
+k8s-openapi = { version = "0.18.0", features = ["v1_25"] }
+kube = { version = "0.83.0", features = ["runtime", "derive"] }
 libc = "^0.2"
 liboverdrop = "0.1.0"
 once_cell = "1.9"
@@ -26,8 +28,10 @@ openssl = "^0.10"
 nix = ">= 0.24, < 0.26"
 regex = "1.7.1"
 rustix = { "version" = "0.37", features = ["thread", "process"] }
+schemars = "0.8.6"
 serde = { features = ["derive"], version = "1.0.125" }
 serde_json = "1.0.64"
+serde_yaml = "0.9.17"
 serde_with = ">= 1.9.4, < 2"
 tokio = { features = ["io-std", "time", "process", "rt", "net"], version = ">= 1.13.0" }
 tokio-util = { features = ["io-util"], version = "0.7" }
diff --git a/lib/src/cli.rs b/lib/src/cli.rs
index b1a252432..58db41b15 100644
--- a/lib/src/cli.rs
+++ b/lib/src/cli.rs
@@ -19,6 +19,9 @@ use std::ffi::OsString;
 use std::os::unix::process::CommandExt;
 use std::process::Command;
 
+use crate::spec::HostSpec;
+use crate::spec::ImageReference;
+
 /// Perform an upgrade operation
 #[derive(Debug, Parser)]
 pub(crate) struct UpgradeOpts {
@@ -174,9 +177,10 @@ pub(crate) async fn get_locked_sysroot() -> Result<ostree_ext::sysroot::SysrootL
 #[context("Pulling")]
 async fn pull(
     repo: &ostree::Repo,
-    imgref: &OstreeImageReference,
+    imgref: &ImageReference,
     quiet: bool,
 ) -> Result<Box<LayeredImageState>> {
+    let imgref = &OstreeImageReference::from(imgref.clone());
     let config = Default::default();
     let mut imp = ostree_container::store::ImageImporter::new(repo, imgref, config).await?;
     let prep = match imp.prepare().await? {
@@ -215,22 +219,35 @@ async fn pull(
 async fn stage(
     sysroot: &SysrootLock,
     stateroot: &str,
-    imgref: &ostree_container::OstreeImageReference,
     image: Box<LayeredImageState>,
-    origin: &glib::KeyFile,
+    spec: &HostSpec,
 ) -> Result<()> {
     let cancellable = gio::Cancellable::NONE;
     let stateroot = Some(stateroot);
     let merge_deployment = sysroot.merge_deployment(stateroot);
+    let origin = glib::KeyFile::new();
+    let ostree_imgref = spec
+        .image
+        .as_ref()
+        .map(|imgref| OstreeImageReference::from(imgref.clone()));
+    if let Some(imgref) = ostree_imgref.as_ref() {
+        origin.set_string(
+            "origin",
+            ostree_container::deploy::ORIGIN_CONTAINER,
+            imgref.to_string().as_str(),
+        );
+    }
     let _new_deployment = sysroot.stage_tree_with_options(
         stateroot,
         image.merge_commit.as_str(),
-        Some(origin),
+        Some(&origin),
         merge_deployment.as_ref(),
         &Default::default(),
         cancellable,
     )?;
-    println!("Queued for next boot: {imgref}");
+    if let Some(imgref) = ostree_imgref.as_ref() {
+        println!("Queued for next boot: {imgref}");
+    }
     Ok(())
 }
 
@@ -266,30 +283,30 @@ async fn prepare_for_write() -> Result<()> {
 async fn upgrade(opts: UpgradeOpts) -> Result<()> {
     prepare_for_write().await?;
     let sysroot = &get_locked_sysroot().await?;
-    let repo = &sysroot.repo();
     let booted_deployment = &sysroot.require_booted_deployment()?;
-    let status = crate::status::DeploymentStatus::from_deployment(booted_deployment, true)?;
-    let osname = booted_deployment.osname();
-    let origin = booted_deployment
-        .origin()
-        .ok_or_else(|| anyhow::anyhow!("Deployment is missing an origin"))?;
-    let imgref = status
-        .image
-        .ok_or_else(|| anyhow::anyhow!("Booted deployment is not container image based"))?;
-    let imgref: OstreeImageReference = imgref.into();
-    if !status.supported {
+    let (_deployments, host) = crate::status::get_status(sysroot, Some(booted_deployment))?;
+    // SAFETY: There must be a status if we have a booted deployment
+    let status = host.status.unwrap();
+    let imgref = host.spec.image.as_ref();
+    // If there's no specified image, let's be nice and check if the booted system is using rpm-ostree
+    if imgref.is_none() && status.booted.map_or(false, |b| b.incompatible) {
         return Err(anyhow::anyhow!(
             "Booted deployment contains local rpm-ostree modifications; cannot upgrade via bootc"
         ));
     }
-    let commit = booted_deployment.csum();
-    let state = ostree_container::store::query_image_commit(repo, &commit)?;
-    let digest = state.manifest_digest.as_str();
-
+    let imgref = imgref.ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
+    // Find the currently queued digest, if any before we pull
+    let queued_digest = status
+        .staged
+        .as_ref()
+        .and_then(|e| e.image.as_ref())
+        .map(|img| img.image_digest.as_str());
     if opts.check {
         // pull the image manifest without the layers
         let config = Default::default();
-        let mut imp = ostree_container::store::ImageImporter::new(repo, &imgref, config).await?;
+        let imgref = &OstreeImageReference::from(imgref.clone());
+        let mut imp =
+            ostree_container::store::ImageImporter::new(&sysroot.repo(), imgref, config).await?;
         match imp.prepare().await? {
             PrepareResult::AlreadyPresent(c) => {
                 println!(
@@ -298,24 +315,27 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
                 );
                 return Ok(());
             }
-            PrepareResult::Ready(p) => {
+            PrepareResult::Ready(r) => {
+                // TODO show a diff
                 println!(
-                    "New manifest available for {}. Digest {}",
-                    imgref, p.manifest_digest
+                    "New image available for {imgref}. Digest {}",
+                    r.manifest_digest
                 );
+                // Note here we'll fall through to handling the --touch-if-changed below
             }
         }
     } else {
-        let fetched = pull(repo, &imgref, opts.quiet).await?;
-
-        if fetched.merge_commit.as_str() == commit.as_str() {
-            println!("Already queued: {digest}");
-            return Ok(());
+        let fetched = pull(&sysroot.repo(), imgref, opts.quiet).await?;
+        if let Some(queued_digest) = queued_digest {
+            if fetched.merge_commit.as_str() == queued_digest {
+                println!("Already queued: {queued_digest}");
+                return Ok(());
+            }
         }
 
-        stage(sysroot, &osname, &imgref, fetched, &origin).await?;
+        let osname = booted_deployment.osname();
+        stage(sysroot, &osname, fetched, &host.spec).await?;
     }
-
     if let Some(path) = opts.touch_if_changed {
         std::fs::write(&path, "").with_context(|| format!("Writing {path}"))?;
     }
@@ -327,14 +347,14 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
 #[context("Switching")]
 async fn switch(opts: SwitchOpts) -> Result<()> {
     prepare_for_write().await?;
-
     let cancellable = gio::Cancellable::NONE;
-    let sysroot = get_locked_sysroot().await?;
-    let booted_deployment = &sysroot.require_booted_deployment()?;
-    let (origin, booted_image) = crate::utils::get_image_origin(booted_deployment)?;
-    let booted_refspec = origin.optional_string("origin", "refspec")?;
-    let osname = booted_deployment.osname();
+
+    let sysroot = &get_locked_sysroot().await?;
     let repo = &sysroot.repo();
+    let booted_deployment = &sysroot.require_booted_deployment()?;
+    let (_deployments, host) = crate::status::get_status(sysroot, Some(booted_deployment))?;
+    // SAFETY: There must be a status if we have a booted deployment
+    let status = host.status.unwrap();
 
     let transport = ostree_container::Transport::try_from(opts.transport.as_str())?;
     let imgref = ostree_container::ImageReference {
@@ -349,30 +369,38 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
         SignatureSource::ContainerPolicy
     };
     let target = ostree_container::OstreeImageReference { sigverify, imgref };
+    let target = ImageReference::from(target);
+
+    let new_spec = {
+        let mut new_spec = host.spec.clone();
+        new_spec.image = Some(target.clone());
+        new_spec
+    };
+
+    if new_spec == host.spec {
+        anyhow::bail!("No changes in current host spec");
+    }
 
     let fetched = pull(repo, &target, opts.quiet).await?;
 
     if !opts.retain {
         // By default, we prune the previous ostree ref or container image
-        if let Some(ostree_ref) = booted_refspec {
-            let (remote, ostree_ref) =
-                ostree::parse_refspec(&ostree_ref).context("Failed to parse ostree ref")?;
-            repo.set_ref_immediate(remote.as_deref(), &ostree_ref, None, cancellable)?;
-            origin.remove_key("origin", "refspec")?;
-        } else if let Some(booted_image) = booted_image.as_ref() {
-            ostree_container::store::remove_image(repo, &booted_image.imgref)?;
-            let _nlayers: u32 = ostree_container::store::gc_image_layers(repo)?;
+        if let Some(booted_origin) = booted_deployment.origin() {
+            if let Some(ostree_ref) = booted_origin.optional_string("origin", "refspec")? {
+                let (remote, ostree_ref) =
+                    ostree::parse_refspec(&ostree_ref).context("Failed to parse ostree ref")?;
+                repo.set_ref_immediate(remote.as_deref(), &ostree_ref, None, cancellable)?;
+            } else if let Some(booted_image) = status.booted.as_ref().and_then(|b| b.image.as_ref())
+            {
+                let imgref = OstreeImageReference::from(booted_image.image.clone());
+                ostree_container::store::remove_image(repo, &imgref.imgref)?;
+                let _nlayers: u32 = ostree_container::store::gc_image_layers(repo)?;
+            }
         }
     }
 
-    // We always make a fresh origin to toss out old state.
-    let origin = glib::KeyFile::new();
-    origin.set_string(
-        "origin",
-        ostree_container::deploy::ORIGIN_CONTAINER,
-        target.to_string().as_str(),
-    );
-    stage(&sysroot, &osname, &target, fetched, &origin).await?;
+    let stateroot = booted_deployment.osname();
+    stage(sysroot, &stateroot, fetched, &new_spec).await?;
 
     Ok(())
 }
diff --git a/lib/src/lib.rs b/lib/src/lib.rs
index d1f031520..58f27a3d3 100644
--- a/lib/src/lib.rs
+++ b/lib/src/lib.rs
@@ -36,6 +36,7 @@ mod install;
 pub(crate) mod mount;
 #[cfg(feature = "install")]
 mod podman;
+pub mod spec;
 #[cfg(feature = "install")]
 mod task;
 
diff --git a/lib/src/privtests.rs b/lib/src/privtests.rs
index 04510a385..d1c996a1f 100644
--- a/lib/src/privtests.rs
+++ b/lib/src/privtests.rs
@@ -7,6 +7,7 @@ use rustix::fd::AsFd;
 use xshell::{cmd, Shell};
 
 use super::cli::TestingOpts;
+use super::spec::Host;
 
 const IMGSIZE: u64 = 20 * 1024 * 1024 * 1024;
 
@@ -101,9 +102,9 @@ pub(crate) fn impl_run_host() -> Result<()> {
 pub(crate) fn impl_run_container() -> Result<()> {
     assert!(ostree_ext::container_utils::is_ostree_container()?);
     let sh = Shell::new()?;
-    let stout = cmd!(sh, "bootc status").read()?;
-    assert!(stout.contains("Running in a container (ostree base)."));
-    drop(stout);
+    let host: Host = serde_yaml::from_str(&cmd!(sh, "bootc status").read()?)?;
+    let status = host.status.unwrap();
+    assert!(status.is_container);
     for c in ["upgrade", "update"] {
         let o = Command::new("bootc").arg(c).output()?;
         let st = o.status;
diff --git a/lib/src/spec.rs b/lib/src/spec.rs
new file mode 100644
index 000000000..e5c40e499
--- /dev/null
+++ b/lib/src/spec.rs
@@ -0,0 +1,98 @@
+//! The definition for host system state.
+
+use kube::CustomResource;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+/// Representation of a bootc host system
+#[derive(
+    CustomResource, Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone, JsonSchema,
+)]
+#[kube(
+    group = "org.containers.bootc",
+    version = "v1alpha1",
+    kind = "BootcHost",
+    struct = "Host",
+    namespaced,
+    status = "HostStatus",
+    derive = "PartialEq",
+    derive = "Default"
+)]
+#[serde(rename_all = "camelCase")]
+pub struct HostSpec {
+    /// The host image
+    pub image: Option<ImageReference>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+/// An image signature
+#[serde(rename_all = "camelCase")]
+pub enum ImageSignature {
+    /// Fetches will use the named ostree remote for signature verification of the ostree commit.
+    OstreeRemote(String),
+    /// Fetches will defer to the `containers-policy.json`, but we make a best effort to reject `default: insecureAcceptAnything` policy.
+    ContainerPolicy,
+    /// No signature verification will be performed
+    Insecure,
+}
+
+/// A container image reference with attached transport and signature verification
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct ImageReference {
+    /// The container image reference
+    pub image: String,
+    /// The container image transport
+    pub transport: String,
+    /// Disable signature verification
+    pub signature: ImageSignature,
+}
+
+/// The status of the booted image
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct ImageStatus {
+    /// The currently booted image
+    pub image: ImageReference,
+    /// The digest of the fetched image (e.g. sha256:a0...);
+    pub image_digest: String,
+}
+
+/// A bootable entry
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct BootEntryOstree {
+    /// The ostree commit checksum
+    pub checksum: String,
+    /// The deployment serial
+    pub deploy_serial: u32,
+}
+
+/// A bootable entry
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct BootEntry {
+    /// The image reference
+    pub image: Option<ImageStatus>,
+    /// Whether this boot entry is not compatible (has origin changes bootc does not understand)
+    pub incompatible: bool,
+    /// Whether this entry will be subject to garbage collection
+    pub pinned: bool,
+    /// If this boot entry is ostree based, the corresponding state
+    pub ostree: Option<BootEntryOstree>,
+}
+
+/// The status of the host system
+#[derive(Debug, Clone, Serialize, Default, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct HostStatus {
+    /// The staged image for the next boot
+    pub staged: Option<BootEntry>,
+    /// The booted image; this will be unset if the host is not bootc compatible.
+    pub booted: Option<BootEntry>,
+    /// The previously booted image
+    pub rollback: Option<BootEntry>,
+
+    /// Whether or not the current system state is an ostree-based container
+    pub is_container: bool,
+}
diff --git a/lib/src/status.rs b/lib/src/status.rs
index 10d9f7f73..31d59ed9b 100644
--- a/lib/src/status.rs
+++ b/lib/src/status.rs
@@ -1,180 +1,219 @@
-use std::borrow::Cow;
+use std::collections::VecDeque;
 
+use crate::spec::{BootEntry, Host, HostSpec, HostStatus, ImageStatus};
+use crate::spec::{ImageReference, ImageSignature};
 use anyhow::{Context, Result};
+use ostree::glib;
 use ostree_container::OstreeImageReference;
 use ostree_ext::container as ostree_container;
+use ostree_ext::keyfileext::KeyFileExt;
 use ostree_ext::ostree;
 use ostree_ext::sysroot::SysrootLock;
 
-use crate::utils::{get_image_origin, ser_with_display};
+const OBJECT_NAME: &str = "host";
 
-/// Representation of a container image reference suitable for serialization to e.g. JSON.
-#[derive(Debug, Clone, serde::Serialize)]
-pub(crate) struct Image {
-    #[serde(serialize_with = "ser_with_display")]
-    pub(crate) verification: ostree_container::SignatureSource,
-    #[serde(serialize_with = "ser_with_display")]
-    pub(crate) transport: ostree_container::Transport,
-    pub(crate) image: String,
+impl From<ostree_container::SignatureSource> for ImageSignature {
+    fn from(sig: ostree_container::SignatureSource) -> Self {
+        use ostree_container::SignatureSource;
+        match sig {
+            SignatureSource::OstreeRemote(r) => Self::OstreeRemote(r),
+            SignatureSource::ContainerPolicy => Self::ContainerPolicy,
+            SignatureSource::ContainerPolicyAllowInsecure => Self::Insecure,
+        }
+    }
+}
+
+impl From<ImageSignature> for ostree_container::SignatureSource {
+    fn from(sig: ImageSignature) -> Self {
+        use ostree_container::SignatureSource;
+        match sig {
+            ImageSignature::OstreeRemote(r) => SignatureSource::OstreeRemote(r),
+            ImageSignature::ContainerPolicy => Self::ContainerPolicy,
+            ImageSignature::Insecure => Self::ContainerPolicyAllowInsecure,
+        }
+    }
+}
+
+/// Fixme lower serializability into ostree-ext
+fn transport_to_string(transport: ostree_container::Transport) -> String {
+    match transport {
+        // Canonicalize to registry for our own use
+        ostree_container::Transport::Registry => "registry".to_string(),
+        o => {
+            let mut s = o.to_string();
+            s.truncate(s.rfind(':').unwrap());
+            s
+        }
+    }
 }
 
-impl From<&OstreeImageReference> for Image {
-    fn from(imgref: &OstreeImageReference) -> Self {
+impl From<OstreeImageReference> for ImageReference {
+    fn from(imgref: OstreeImageReference) -> Self {
         Self {
-            verification: imgref.sigverify.clone(),
-            transport: imgref.imgref.transport,
-            image: imgref.imgref.name.clone(),
+            signature: imgref.sigverify.into(),
+            transport: transport_to_string(imgref.imgref.transport),
+            image: imgref.imgref.name,
         }
     }
 }
 
-impl From<Image> for OstreeImageReference {
-    fn from(img: Image) -> OstreeImageReference {
-        OstreeImageReference {
-            sigverify: img.verification,
+impl From<ImageReference> for OstreeImageReference {
+    fn from(img: ImageReference) -> Self {
+        Self {
+            sigverify: img.signature.into(),
             imgref: ostree_container::ImageReference {
-                transport: img.transport,
+                /// SAFETY: We validated the schema in kube-rs
+                transport: img.transport.as_str().try_into().unwrap(),
                 name: img.image,
             },
         }
     }
 }
 
-/// Representation of a deployment suitable for serialization to e.g. JSON.
-#[derive(serde::Serialize)]
-pub(crate) struct DeploymentStatus {
-    pub(crate) pinned: bool,
-    pub(crate) booted: bool,
-    pub(crate) staged: bool,
-    pub(crate) supported: bool,
-    pub(crate) image: Option<Image>,
-    pub(crate) checksum: String,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub(crate) deploy_serial: Option<u32>,
+/// Parse an ostree origin file (a keyfile) and extract the targeted
+/// container image reference.
+fn get_image_origin(origin: &glib::KeyFile) -> Result<Option<OstreeImageReference>> {
+    origin
+        .optional_string("origin", ostree_container::deploy::ORIGIN_CONTAINER)
+        .context("Failed to load container image from origin")?
+        .map(|v| ostree_container::OstreeImageReference::try_from(v.as_str()))
+        .transpose()
 }
 
-/// This struct is serialized when we're running in a container image.
-#[derive(serde::Serialize)]
-pub(crate) struct StatusInContainer {
-    pub(crate) is_container: bool,
+pub(crate) struct Deployments {
+    pub(crate) staged: Option<ostree::Deployment>,
+    pub(crate) rollback: Option<ostree::Deployment>,
+    #[allow(dead_code)]
+    pub(crate) other: VecDeque<ostree::Deployment>,
 }
 
-impl DeploymentStatus {
-    /// Gather metadata from an ostree deployment into a Rust structure
-    pub(crate) fn from_deployment(deployment: &ostree::Deployment, booted: bool) -> Result<Self> {
-        let staged = deployment.is_staged();
-        let pinned = deployment.is_pinned();
-        let image = get_image_origin(deployment)?.1;
-        let checksum = deployment.csum().to_string();
-        let deploy_serial = (!staged).then(|| deployment.bootserial().try_into().unwrap());
-        let supported = deployment
-            .origin()
-            .map(|o| !crate::utils::origin_has_rpmostree_stuff(&o))
-            .unwrap_or_default();
-
-        Ok(DeploymentStatus {
-            staged,
-            pinned,
-            booted,
-            supported,
-            image: image.as_ref().map(Into::into),
-            checksum,
-            deploy_serial,
-        })
-    }
+fn boot_entry_from_deployment(
+    sysroot: &SysrootLock,
+    deployment: &ostree::Deployment,
+) -> Result<BootEntry> {
+    let repo = &sysroot.repo();
+    let (image, incompatible) = if let Some(origin) = deployment.origin().as_ref() {
+        if let Some(image) = get_image_origin(origin)? {
+            let image = ImageReference::from(image);
+            let csum = deployment.csum();
+            let incompatible = crate::utils::origin_has_rpmostree_stuff(origin);
+            let imgstate = ostree_container::store::query_image_commit(repo, &csum)?;
+            (
+                Some(ImageStatus {
+                    image,
+                    image_digest: imgstate.manifest_digest,
+                }),
+                incompatible,
+            )
+        } else {
+            (None, false)
+        }
+    } else {
+        (None, false)
+    };
+    let r = BootEntry {
+        image,
+        incompatible,
+        pinned: deployment.is_pinned(),
+        ostree: Some(crate::spec::BootEntryOstree {
+            checksum: deployment.csum().into(),
+            // SAFETY: The deployserial is really unsigned
+            deploy_serial: deployment.deployserial().try_into().unwrap(),
+        }),
+    };
+    Ok(r)
 }
 
 /// Gather the ostree deployment objects, but also extract metadata from them into
 /// a more native Rust structure.
-fn get_deployments(
+pub(crate) fn get_status(
     sysroot: &SysrootLock,
     booted_deployment: Option<&ostree::Deployment>,
-    booted_only: bool,
-) -> Result<Vec<(ostree::Deployment, DeploymentStatus)>> {
-    let deployment_is_booted = |d: &ostree::Deployment| -> bool {
-        booted_deployment.as_ref().map_or(false, |b| d.equal(b))
-    };
-    sysroot
+) -> Result<(Deployments, Host)> {
+    let stateroot = booted_deployment.as_ref().map(|d| d.osname());
+    let (mut related_deployments, other_deployments) = sysroot
         .deployments()
         .into_iter()
-        .filter(|deployment| !booted_only || deployment_is_booted(deployment))
-        .map(|deployment| -> Result<_> {
-            let booted = deployment_is_booted(&deployment);
-            let status = DeploymentStatus::from_deployment(&deployment, booted)?;
-            Ok((deployment, status))
+        .partition::<VecDeque<_>, _>(|d| Some(d.osname()) == stateroot);
+    let staged = related_deployments
+        .iter()
+        .position(|d| d.is_staged())
+        .map(|i| related_deployments.remove(i).unwrap());
+    // Filter out the booted, the caller already found that
+    if let Some(booted) = booted_deployment.as_ref() {
+        related_deployments.retain(|f| !f.equal(booted));
+    }
+    let rollback = related_deployments.pop_front();
+    let other = {
+        related_deployments.extend(other_deployments);
+        related_deployments
+    };
+    let deployments = Deployments {
+        staged,
+        rollback,
+        other,
+    };
+
+    let is_container = ostree_ext::container_utils::is_ostree_container()?;
+
+    let staged = deployments
+        .staged
+        .as_ref()
+        .map(|d| boot_entry_from_deployment(sysroot, d))
+        .transpose()?;
+    let booted = booted_deployment
+        .as_ref()
+        .map(|d| boot_entry_from_deployment(sysroot, d))
+        .transpose()?;
+    let rollback = deployments
+        .rollback
+        .as_ref()
+        .map(|d| boot_entry_from_deployment(sysroot, d))
+        .transpose()?;
+    let spec = staged
+        .as_ref()
+        .or(booted.as_ref())
+        .and_then(|entry| entry.image.as_ref())
+        .map(|img| HostSpec {
+            image: Some(img.image.clone()),
         })
-        .collect()
+        .unwrap_or_default();
+    let mut host = Host::new(OBJECT_NAME, spec);
+    host.status = Some(HostStatus {
+        staged,
+        booted,
+        rollback,
+        is_container,
+    });
+    Ok((deployments, host))
 }
 
 /// Implementation of the `bootc status` CLI command.
 pub(crate) async fn status(opts: super::cli::StatusOpts) -> Result<()> {
-    if ostree_ext::container_utils::is_ostree_container()? {
-        if opts.json {
-            let mut stdout = std::io::stdout().lock();
-            serde_json::to_writer(&mut stdout, &StatusInContainer { is_container: true })
-                .context("Serializing status")?;
-        } else {
-            println!("Running in a container (ostree base).");
-        }
-        return Ok(());
-    }
-    let sysroot = super::cli::get_locked_sysroot().await?;
-    let repo = &sysroot.repo();
-    let booted_deployment = sysroot.booted_deployment();
+    let host = if ostree_ext::container_utils::is_ostree_container()? {
+        let status = HostStatus {
+            is_container: true,
+            ..Default::default()
+        };
+        let mut r = Host::new(OBJECT_NAME, HostSpec { image: None });
+        r.status = Some(status);
+        r
+    } else {
+        let sysroot = super::cli::get_locked_sysroot().await?;
+        let booted_deployment = sysroot.booted_deployment();
+        let (_deployments, host) = get_status(&sysroot, booted_deployment.as_ref())?;
+        host
+    };
 
-    let deployments = get_deployments(&sysroot, booted_deployment.as_ref(), opts.booted)?;
     // If we're in JSON mode, then convert the ostree data into Rust-native
     // structures that can be serialized.
+    // Filter to just the serializable status structures.
+    let out = std::io::stdout();
+    let mut out = out.lock();
     if opts.json {
-        // Filter to just the serializable status structures.
-        let deployments = deployments.into_iter().map(|e| e.1).collect::<Vec<_>>();
-        let out = std::io::stdout();
-        let mut out = out.lock();
-        serde_json::to_writer(&mut out, &deployments).context("Writing to stdout")?;
-        return Ok(());
-    }
-
-    // We're not writing to JSON; iterate over and print.
-    for (deployment, info) in deployments {
-        let booted_display = if info.booted { "* " } else { " " };
-        let image: Option<OstreeImageReference> = info.image.as_ref().map(|i| i.clone().into());
-
-        let commit = info.checksum;
-        if let Some(image) = image.as_ref() {
-            println!("{booted_display} {image}");
-            if !info.supported {
-                println!("    Origin contains rpm-ostree machine-local changes");
-            } else {
-                let state = ostree_container::store::query_image_commit(repo, &commit)?;
-                println!("    Digest: {}", state.manifest_digest.as_str());
-                let version = state
-                    .configuration
-                    .as_ref()
-                    .and_then(ostree_container::version_for_config);
-                if let Some(version) = version {
-                    println!("    Version: {version}");
-                }
-            }
-        } else {
-            let deployinfo = if let Some(serial) = info.deploy_serial {
-                Cow::Owned(format!("{commit}.{serial}"))
-            } else {
-                Cow::Borrowed(&commit)
-            };
-            println!("{booted_display} {deployinfo}");
-            println!("    (Non-container origin type)");
-            println!();
-        }
-        println!("    Backend: ostree");
-        if deployment.is_pinned() {
-            println!("    Pinned: yes")
-        }
-        if info.booted {
-            println!("    Booted: yes")
-        } else if deployment.is_staged() {
-            println!("    Staged: yes");
-        }
-        println!();
+        serde_json::to_writer(&mut out, &host).context("Writing to stdout")?;
+    } else {
+        serde_yaml::to_writer(&mut out, &host).context("Writing to stdout")?;
     }
 
     Ok(())
diff --git a/lib/src/utils.rs b/lib/src/utils.rs
index 67ae9f7f1..e0a524aa5 100644
--- a/lib/src/utils.rs
+++ b/lib/src/utils.rs
@@ -1,29 +1,7 @@
-use std::fmt::Display;
 use std::process::Command;
 
-use anyhow::{Context, Result};
 use ostree::glib;
-use ostree_container::OstreeImageReference;
-use ostree_ext::container as ostree_container;
-use ostree_ext::keyfileext::KeyFileExt;
 use ostree_ext::ostree;
-use serde::Serializer;
-
-/// Parse an ostree origin file (a keyfile) and extract the targeted
-/// container image reference.
-pub(crate) fn get_image_origin(
-    deployment: &ostree::Deployment,
-) -> Result<(glib::KeyFile, Option<OstreeImageReference>)> {
-    let origin = deployment
-        .origin()
-        .ok_or_else(|| anyhow::anyhow!("Missing origin"))?;
-    let imgref = origin
-        .optional_string("origin", ostree_container::deploy::ORIGIN_CONTAINER)
-        .context("Failed to load container image from origin")?
-        .map(|v| ostree_container::OstreeImageReference::try_from(v.as_str()))
-        .transpose()?;
-    Ok((origin, imgref))
-}
 
 /// Try to look for keys injected by e.g. rpm-ostree requesting machine-local
 /// changes; if any are present, return `true`.
@@ -38,16 +16,6 @@ pub(crate) fn origin_has_rpmostree_stuff(kf: &glib::KeyFile) -> bool {
     false
 }
 
-/// Implement the `Serialize` trait for types that are `Display`.
-/// https://stackoverflow.com/questions/58103801/serialize-using-the-display-trait
-pub(crate) fn ser_with_display<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
-where
-    T: Display,
-    S: Serializer,
-{
-    serializer.collect_str(value)
-}
-
 /// Run a command in the host mount namespace
 #[allow(dead_code)]
 pub(crate) fn run_in_host_mountns(cmd: &str) -> Command {
diff --git a/tests/kolainst/basic b/tests/kolainst/basic
index ce4469b44..fdf6a9ce7 100755
--- a/tests/kolainst/basic
+++ b/tests/kolainst/basic
@@ -15,7 +15,7 @@ case "${AUTOPKGTEST_REBOOT_MARK:-}" in
     bootc status > status.txt
     grep 'Version:' status.txt
     bootc status --json > status.json
-    image=$(jq -r '.[0].image.image' < status.json)
+    image=$(jq -r '.status.booted.image.image' < status.json)
     echo "booted into $image"
     echo "ok status test"