Skip to content

Packaging Decisions ​

Four decisions that make Nexus packaging unlike anything in the Linux/BSD world.

P1: Six Package Types ​

Status: Accepted

Context ​

Docker uses one model (container) for everything — databases, web servers, CLI tools, drivers. Debian uses one model (.deb) with global /usr dependencies. Neither accounts for the vast difference between a temperature sensor driver (10KB, direct MMIO) and a web browser (500MB, full sandbox).

Decision ​

Six package archetypes, each optimized for its role:

TypeNamePurposeIsolationSize
NPSNip SensorTelemetry agentsDirect MMIOKB
NPLNip LibraryDrivers, filtersOne hop from kernel (9P)KB-MB
NPXNip eXtensionHot-loadable hooks (eBPF replacement)Trap dispatchKB
NPMNip ModulePure logic librariesNo hardware accessKB-MB
NPKNip PackageEnd-user applicationsFull sandboxMB
NPINip InterfaceAPI bridges, service discovery/Bus/ symlinkKB

Each type has different:

  • Startup time (NPS: µs, NPK: ms)
  • Memory overhead (NPS: none, NPK: full Membrane)
  • Security boundary (NPS: PMP only, NPK: pledge + capability + cell)
  • Build pipeline (NPS: bare binary, NPK: full quarantine pipeline)

Alternatives Rejected ​

OptionWhy Not
Docker containers for everythingOne-size-fits-all wastes memory; 200MB per container on MCU is absurd
Debian .deb for everythingNo isolation, shared /usr pollutes global namespace
Flatpak/SnapEach package bundles own libc; 200MB per desktop app
Static binary onlyWorks but no code sharing, harder to patch shared library CVEs

Consequences ​

  • Drivers (NPL) don't need full POSIX stack (saves 50MB per driver)
  • Sensors (NPS) start in microseconds (no libc initialization)
  • Clear intent: package type communicates isolation requirements at a glance
  • Isolation tuned per type (NPL may share kernel memory; NPK gets full sandbox)
  • Developers must choose correct type (higher initial learning curve)
  • Migration between types requires repackaging

P2: Graft → Evolve → Sovereignize ​

Status: Accepted

Context ​

Building everything from scratch takes years and produces immature code. Forking upstream creates divergent codebases that can't merge. The Linux and BSD ecosystems contain decades of battle-tested code. The question isn't "build vs. buy" — it's "borrow now, replace later."

Decision ​

Three-phase component lifecycle:

Phase 1: Graft ​

Use upstream artifacts as-is. No modifications.

Example: LwIP TCP/IP stack from git submodule. BSD-licensed. Works.

Phase 2: Evolve ​

Replace components incrementally where Nexus needs differ from upstream.

Example: Standard TCP for Internet traffic, UTCP wrapper for cluster traffic. Both coexist.

Phase 3: Sovereignize ​

Full native reimplementation. Zero external dependencies.

Example: Native Nim TCP stack replacing LwIP (post-MVP, when edge cases are understood).

Current State ​

ComponentPhaseSource
TCP/IPGraftLwIP (BSD)
ShellGraftmksh (MirBSD)
Filesystem (NexFS)SovereignNative Zig
Transport (UTCP)SovereignNative Zig
Package Manager (nip)SovereignNative Nim
Build ToolkitSovereignNative Nim
Kernel (Rumpk)SovereignNative Nim + Zig
Init/BootSovereignNative

Alternatives Rejected ​

OptionWhy Not
All from scratch10x development time, high bug risk, reinventing solved problems
All from upstreamForever dependent, can't optimize for Nexus, can't remove POSIX bloat
Fork and divergeUnmaintainable; merge conflicts with upstream destroy velocity

Consequences ​

  • Rapid MVP using proven code (LwIP, mksh work today)
  • Gradual technical debt payoff (replace when you understand the problem)
  • Can optimize critical path first (Evolve only hot components)
  • Intermediate versions have mixed code quality
  • Grafted code must not leak into architecture (clean boundaries required)

P3: GoboLinux Hierarchy ​

Status: Accepted

Context ​

The Filesystem Hierarchy Standard (FHS) puts all binaries in /usr/bin, all libraries in /usr/lib. One broken package update can corrupt shared libraries system-wide. Nix solves this with /nix/store/hash-name/ but the paths are cryptic and undiscoverable.

Decision ​

Content-addressed per-version directories, inspired by GoboLinux:

/Cas/blake3:a1b2c3.../
  bin/
  lib/
  share/

/Cell/App/firefox/
  fs/ → /Cas/blake3:a1b2c3.../    (symlink to active version)
  • Each package version is content-addressed (hash includes all transitive dependencies)
  • Old versions remain in /Cas/ — instant rollback by relinking
  • Multiple versions coexist (Python 3.9 and 3.10, same system, zero conflict)
  • Deduplication at block level via NexFS (identical binaries share storage)

Alternatives Rejected ​

OptionWhy Not
Traditional FHSapt upgrade breaks fragile dependency chains; shared /usr is a liability
Nix store/nix/store/hash-name/ is awkward, non-discoverable, hard to debug
Statically linked monolithsWorks but code duplication; can't patch shared library CVEs efficiently
Containers (Docker volumes)Opaque layers, hard to inspect, storage inefficient

Consequences ​

  • Downgrades are atomic (relink one symlink)
  • Multiple versions coexist without conflict
  • NexFS deduplication means shared binaries stored once on disk
  • Apps must not hardcode absolute paths (use relative or /Cell/ paths)
  • Legacy tools expecting /usr/bin/app need Membrane path translation

P4: KDL Over YAML and TOML ​

Status: Accepted

Context ​

YAML is whitespace-significant (a tab vs. spaces can break your deployment). TOML is flat (nested configurations become awkward). JSON has no comments. HCL (Terraform) is verbose. Nexus needs a configuration language for package manifests, cell blueprints, build recipes, and system policies — all deeply nested, all declarative.

Decision ​

KDL (KDL Document Language) for all configuration:

kdl
graft "networking" {
    origin {
        system "alpine"
        package "networkmanager"
        version "1.44.0"
    }
    pledge {
        allow "net-admin"
        deny "ptrace"
    }
}

cell "web-server" {
    cores 2
    memory "512MB"
    pledge "net" "stdio" "rw"
    mount "/data" from="/Cas/blake3:..." readonly=true
}
  • S-expression inspired, whitespace-insensitive
  • Comments with // and /* */
  • Not Turing-complete (pure declaration, no computation)
  • KDL compiler targets JSON, CBOR, and binary formats

Alternatives Rejected ​

OptionWhy Not
YAMLWhitespace-significant parsing is fragile; !! escape sequences are cryptic
TOMLFlat structure makes nested configs awkward; table syntax is verbose
JSONNo comments, verbose syntax, no types beyond string/number/bool
HCL (Terraform)JSON-like verbosity; Terraform ecosystem coupling
DhallTuring-complete (enables arbitrary computation, reduces predictability)
Nix languagePowerful but complex; Nix's learning curve is legendary

Consequences ​

  • Declarative syntax prevents Turing-complete escape (policies are predictable)
  • Nested structure maps naturally to object hierarchy (cells contain mounts contain paths)
  • Comments are first-class (unlike JSON)
  • Not Turing-complete means no conditional logic in configs (this is intentional)
  • KDL is a niche language (limited tooling, IDE support developing)
  • Developers must learn new syntax (though simpler than most alternatives)