Skip to content

Downstream Project Setup

This guide covers how to set up a downstream project that builds on the Ghaf Framework, enabling full customization while staying updated with upstream changes.

A downstream project typically:

  1. Imports Ghaf as a flake input
  2. Extends or replaces VM configurations
  3. Adds custom applications and services
  4. Creates product-specific targets

Example downstream projects:

  • ghaf-fmo-laptop - Field Mission Operations variant
  • Custom product builds based on Ghaf

Recommended directory structure for a downstream project:

my-ghaf-product/
├── flake.nix # Main flake with ghaf input
├── flake.lock # Locked dependencies
├── lib/
│ └── profiles.nix # Custom profiles
├── modules/
│ ├── custom-vm/ # Custom VM definitions
│ │ └── my-vm-base.nix
│ ├── services/ # Product-specific services
│ │ └── my-service.nix
│ └── hardware/ # Custom hardware support
│ └── my-device.nix
├── targets/
│ └── my-product/
│ └── flake-module.nix # Product configurations
└── README.md
flake.nix
{
description = "My Ghaf-based Product";
inputs = {
# Pin to specific Ghaf version or branch
ghaf = {
url = "github:tiiuae/ghaf";
# Or pin to specific commit:
# url = "github:tiiuae/ghaf/abc123";
};
# Follow Ghaf's nixpkgs
nixpkgs.follows = "ghaf/nixpkgs";
# Flake-parts for module organization
flake-parts.follows = "ghaf/flake-parts";
};
outputs = inputs@{ flake-parts, ghaf, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [ "x86_64-linux" "aarch64-linux" ];
imports = [
# Import Ghaf's flake modules
ghaf.flakeModules.targets
# Your target configurations
./targets/my-product/flake-module.nix
];
# Expose custom modules
flake.nixosModules = {
my-service = ./modules/services/my-service.nix;
my-vm-base = ./modules/custom-vm/my-vm-base.nix;
my-hardware = ./modules/hardware/my-device.nix;
};
};
}
targets/my-product/flake-module.nix
{ inputs, lib, ... }:
let
# Access Ghaf's builders
mkGhafConfiguration = inputs.ghaf.lib.ghaf.builders.mkGhafConfiguration;
mkGhafInstaller = inputs.ghaf.lib.ghaf.builders.mkGhafInstaller;
in
{
flake.nixosConfigurations = {
# Debug variant
my-product-debug = mkGhafConfiguration {
name = "my-product";
system = "x86_64-linux";
profile = "laptop-x86";
# Use Ghaf's hardware definition or your own
hardwareModule = inputs.ghaf.nixosModules.hardware-lenovo-x1-carbon-gen11;
# Debug variant auto-applies lib.ghaf.profiles.debug via mkDefault
variant = "debug";
# Override specific global config values
extraConfig = {
global-config.features.bluetooth.enable = false;
};
# Additional modules
extraModules = [
inputs.self.nixosModules.my-service
{
# Inline configuration
environment.systemPackages = [ inputs.nixpkgs.vim ];
}
];
};
# Release variant
my-product-release = mkGhafConfiguration {
name = "my-product";
system = "x86_64-linux";
profile = "laptop-x86";
hardwareModule = inputs.ghaf.nixosModules.hardware-lenovo-x1-carbon-gen11;
variant = "release";
};
};
# Installer
flake.packages.x86_64-linux.my-product-installer = mkGhafInstaller {
name = "my-product";
configuration = inputs.self.nixosConfigurations.my-product-release;
};
}

# In your target configuration
{ config, lib, inputs, ... }:
let
# Get base GUI VM from profile
baseGuivm = config.ghaf.profiles.laptop-x86.guivmBase;
# Extend with your customizations
myGuivm = baseGuivm.extendModules {
modules = [
# Add your applications
({ pkgs, ... }: {
environment.systemPackages = with pkgs; [
my-custom-app
my-monitoring-tool
];
})
# Configure services
{
services.myProductService = {
enable = true;
apiEndpoint = "https://my-api.example.com";
};
}
# Desktop customization
{
services.xserver.desktopManager.gnome.enable = lib.mkForce false;
services.xserver.desktopManager.plasma5.enable = true;
}
];
};
in
{
# Use your extended GUI VM (pass the full system, not .config)
ghaf.virtualization.microvm.guivm.evaluatedConfig = myGuivm;
}
modules/custom-vm/gcs-vm-base.nix
{ config, lib, pkgs, globalConfig, hostConfig, inputs, ... }:
{
_file = ./gcs-vm-base.nix;
imports = [
# Import Ghaf's base VM modules
inputs.ghaf.nixosModules.vm-config
inputs.ghaf.nixosModules.givc
];
# Standard VM setup
networking.hostName = hostConfig.vmName;
ghaf = {
profiles.debug.enable = globalConfig.debug.enable;
givc.enable = globalConfig.givc.enable;
};
# Your GCS-specific configuration
services.gcsClient = {
enable = true;
serverAddress = "gcs.example.com";
};
environment.systemPackages = with pkgs; [
gcs-tools
mission-planner
];
}
{ config, ... }:
let
mkAppVm = config.ghaf.profiles.laptop-x86.mkAppVm;
in
{
ghaf.virtualization.microvm.appvm.vms = {
# Custom productivity app
my-productivity = mkAppVm {
name = "my-productivity";
applications = [{
name = "My Productivity Suite";
description = "Company productivity tools";
packages = [ pkgs.my-productivity ];
icon = "my-productivity";
command = "my-productivity";
}];
extraModules = [{
networking.proxy.default = "http://proxy.example.com:8080";
}];
};
# Secure browser
secure-browser = mkAppVm {
name = "secure-browser";
applications = [{
name = "Secure Browser";
description = "Isolated web browsing";
packages = [ pkgs.firefox ];
icon = "firefox";
command = "firefox --private-window";
}];
};
};
}

{
ghaf.global-config.features = {
# Move fingerprint to admin VM for enrollment only
fprint.targetVms = [ "admin-vm" ];
# Enable YubiKey in custom VM
yubikey.targetVms = [ "gui-vm" "gcs-vm" ];
# Disable unused features
bluetooth.enable = false;
};
}

Define new features in your downstream project:

modules/features/gcs-radio.nix
{ config, lib, globalConfig, ... }:
let
enabled = globalConfig.features.gcsRadio.enable or false;
in
{
_file = ./gcs-radio.nix;
config = lib.mkIf enabled {
services.gcsRadio = {
enable = true;
frequency = globalConfig.features.gcsRadio.frequency or 915;
};
hardware.firmware = [ pkgs.gcs-radio-firmware ];
};
}

Terminal window
# Update to latest
nix flake update ghaf
# Or update to specific commit
nix flake lock --update-input ghaf --override-input ghaf github:tiiuae/ghaf/abc123
  1. Read Ghaf release notes
  2. Check for API changes in lib.ghaf.*
  3. Test builds before deploying:
Terminal window
nix build .#my-product-debug --dry-run
nix build .#my-product-release

If upstream changes break your configuration:

# Temporarily override in flake.nix
inputs.ghaf = {
url = "github:tiiuae/ghaf";
# Pin to last working version
# url = "github:tiiuae/ghaf/known-good-commit";
};

Prefer extending over replacing:

# Good - extend base
baseGuivm.extendModules { modules = [ ... ]; }
# Avoid - replace entirely (loses upstream updates)
lib.nixosSystem { modules = [ my-complete-guivm ]; }
# Good - use variant parameter
mkGhafConfiguration {
variant = "debug";
# Overrides go in extraConfig
extraConfig = {
global-config.features.bluetooth.enable = false;
};
};
# Avoid - define everything manually
extraConfig = {
global-config = {
debug.enable = true;
development.ssh.daemon.enable = true;
# ... copying all profile values
};
};
# Good - hardware in separate module
hardwareModule = inputs.self.nixosModules.my-hardware;
# Avoid - inline hardware in target
extraModules = [{ hardware.cpu.intel.enable = true; /* ... */ }];
modules/services/my-service.nix
# SPDX-FileCopyrightText: 2026 My Company
# SPDX-License-Identifier: Apache-2.0
#
# My Product Service
#
# This service provides XYZ functionality for our product.
# It depends on Ghaf's GIVC for inter-VM communication.
#
{ config, lib, ... }: ...

Terminal window
# Show detailed trace
nix build .#my-product-debug --show-trace
# Check specific option
nix eval .#nixosConfigurations.my-product-debug.config.ghaf.global-config --json | jq
Terminal window
# Check VM configuration
nix eval .#nixosConfigurations.my-product-debug.config.microvm.vms.gui-vm.config.config.networking.hostName
# Build VM system
nix build .#nixosConfigurations.my-product-debug.config.microvm.vms.gui-vm.config.config.system.build.toplevel
Terminal window
# Verify feature assignment
nix eval .#nixosConfigurations.my-product-debug.config.ghaf.global-config.features.myFeature
# Check if service enabled in VM
nix eval .#nixosConfigurations.my-product-debug.config.microvm.vms.gui-vm.config.config.services.myService.enable

  • Create flake.nix with ghaf input
  • Follow ghaf/nixpkgs to avoid conflicts
  • Create target configuration using mkGhafConfiguration
  • Use extendModules for VM customization
  • Keep hardware definitions separate
  • Document customizations
  • Test both debug and release variants
  • Set up CI for build testing
  • Plan update strategy for upstream changes