Anatomy of an Atom
A Solid & Natural Foundation
This post is a deep technical walkthrough of the Atom format: a packaging API designed to fix how Nix code is distributed and scaled. If you want the political and philosophical backdrop, see my pieces on the erosion of open-source values and the broader vision driving this work. Here, we're all technical. Fair warning, this is a long one...
A note on project evolution: This piece captures the Atom design as of mid-2025, during the proof-of-concept phase. That phase was a success. Since then, some details (particularly the manifest sketch) have evolved considerably. We are now working on a formal specification to nail down semantics and address production concerns not covered in the prototype: key management, ownership, claim provenance, and more. Consider this a snapshot of the thinking that got us here, not the final word.
Atom: A Review¶
If youâve followed my previous writings or poked around in my code, you might already have a rough sense of the foundational format Iâm championing: the Atom. To keep this piece standalone, though, letâs recap its high-level design and the driving force behind it before we go deep. The silver lining to the long gap between iterations? My ability to explain my designs after months of stewing has, hopefully, gotten a lot sharper.
The Runaway Train: A Motivation¶
Conventional wisdom in tech projects says that once you hit a certain scale, foundational overhauls are a bad idea: iterative tweaks are the safer play. But every now and then, the existing setup is so broken that it starts threatening the projectâs very existence. When that happens, a radical rethink isnât just an option; itâs a necessity.
Iâve spent nearly a decade with Nix, half of that in professional gigs, and Iâve watched the same problems rear their heads as organizations scale up their Nix usage. I wonât bore you with the gory details; anyone whoâs made a non-trivial contribution to nixpkgs knows the pain all too well, and if you are really curious, there is plenty of evidence all over the internet, by now. The real kicker? These issues donât seem fixable without rethinking the core idioms we use to write and, especially, organize Nix code.
As projects like nixpkgs balloon to massive scale, the cracks only get worse. Long-standing social drama has some folks burying their heads in the sand, or dipping out entirely. Others might lack the experience to see the train wreck coming. Some are too tied to the status quo to budge, while others, like the teams behind snix, the promising early-stage cab language, and our own ekapkgs, are stepping up with bold efforts to tackle the mess.
Iâm rooting for those projects to succeed; their technical vision lines up closely with my own take on the challenges. My original plan was to pitch in and support them, aiming to complement their work rather than reinvent the wheel. But along the way, I stumbled onto what I now see as a glaring gap in the ecosystem: one that has to be filled if weâre going to solve these scaling issues at their root.
The Missing Link: Language-Level Packages¶
Thereâs an irony in Nix. Itâs a domain-specific language (DSL) meticulously crafted to deliver binary software packages with precision and discipline, yet it barely considers packaging its own expressions in a similar way. To avoid confusionâsince âpackageâ is a heavily overloaded termâwe're referring here to source code distribution packages. Think package.json or Cargo.toml: formats that bundle source code into clean, discrete units for easy distribution and downstream use.
Since Iâm a Rust enthusiast, letâs use it to illustrate. In Rust, a repository might house a workspace with dozens, maybe even hundreds, of crates: self-contained package units. When itâs time to publish, each crate gets neatly bundled and shipped to a central registry. If I need crate a from a larger workspace P, I can grab just a from this registry, no extra baggage from P included. Later, if I need a newer version of a, itâs simply another pull from the registry; only the files for a, nothing more.
Now contrast that with nixpkgs. Want package a? Youâre stuck pulling the entire repository just to evaluate it. Sure, aâs dependencies get fetched in the process, but most of the code youâre downloading has nothing to do with a. Need a different version of a down the line? Youâre fetching another full nixpkgs checkout, with another chunk of totally irrelevant code. Itâs not hard to see how this spirals out of control. Itâs not sustainable.
Like any well-designed language ecosystem, we should have a straightforward way to grab only the Nix expressions we need, with their dependencies pulled in piecemealâno more, no less. Itâs not just about efficiency; itâs just as much about maintaining sanity.
The Unique Challenge of Packaging Nix Code¶
Of course, this feels like it should be obvious, especially after years in the trenches. When flakes came along, I was hopeful theyâd crack this nut. Spoiler: they didnât. In fact, they sometimes make it worse, though I wonât dive too deep into that here. The core issue is that flakes still require you to fetch the full repository context to evaluate them, which kills any chance of packaging smaller expressions. Even if you split your repo into multiple flakesânot a trivial taskâyouâre still dragging in the whole repo for each subflake. Itâs the same mess, just rearranged.
The real problem, delivering small, versioned units of Nix code efficiently, has barely been touched. Some folks dedicate tiny repos to a single package, but thatâs rare. Flakes encourage tying Nix to project source, and nixpkgs itself is a sprawling monolith. The repository boundary is just too coarse. We need something finer-grained, like a single Rust crate in a workspace, to have any hope of taming the challenge of distributing only the Nix expressions needed for building binaries.
This isnât a simple fix, though. Nix is source-first by design; it needs access to source code to evaluate expressions and build packages. That tight, cryptographically secure link between expressions and their source repo is one of Nixâs biggest strengths. Slapping on a centralized registry model, like other languages use, would shred that advantage.
So, we need a novel approach; one that doesnât sacrifice what makes Nix powerful. I experimented with tools like josh proxy, which seemed promising but couldnât handle nixpkgsâ scale. It became clear thereâs no off-the-shelf solution for this, not at the size weâre dealing with. What I needed was a system that:
- Preserves the cryptographic tie between Nix expressions and their source.
- Distributes directly from the source, staying true to Nixâs ethos.
- Adds no runtime overhead to enumerating available atoms and their versions, ensuring trivial scalability.
- Scales efficiently across repositories of any size, letting users organize their projects based on preference, not constraints.
- Delivers only relevant, versioned code in a way thatâs simple to understand and use.
Atomic Anatomy¶
In the last section, I introduced the motivation behind atomâa foundational format to rethink how we package and distribute Nix expressions. Driven by the escalating complexity of nixpkgs and the Nix ecosystemâs scaling woes, I argued that a first-principles overhaul is critical to avoid a maintenance nightmare. While I respect efforts like snix and cab, Iâve identified a unique gap in the ecosystem that the atom aims to fill, complementing those projects with a format they could adopt in the future. Now, letâs unpack the technical anatomy of an atom and see how it tackles the problem head-on.
After years with my hands deep in the code, stepping back to explain the big picture to newcomers can be tough. But to build support and drive adoption, Iâve realized I need to double down on describing my work as simply as possible. So, letâs start from the ground up and build from there.
A Packaging API¶
The Atom API is deliberately generic, unbound from any specific language, ecosystem, or storage system. Think of it as a source code packaging API: a frontend defines how to package code for a given language, and a backend, termed an Ekala store, specifies where and how those atoms are stored. This flexibility isnât just elegant designâitâs practical, letting atom adapt to the diverse needs of different organizations.
Why such an open approach? A clear, high-level API for the atomic universe is good design, but itâs also about real-world utility. The Git storage backend, which Iâll cover soon, aligns perfectly with the open-source ethos of transparency and redistribution. Yet some organizations prioritize privacy and security over source availabilityâan S3 backend, for example, could offer a centralized solution to meet those needs. This versatility ensures atom supports varied use cases while maintaining a unified user-facing API, without locking anyone into a single mold.
This openness also future-proofs the design. If atom gains traction, it could support new frontends like Guix or even Cab, or integrate with existing packaging formats. Picture âatomicâ Cargo crates distributed from an Ekala Git Storeâa concept Iâll clarify in the next segment. While supporting existing formats isnât my focus now, it underscores the designâs potential.
To ground things, letâs dive into the Atom Nix frontend and Git storage backend, which are tightly linked to the motivating use case outlined earlier and the heart of current development efforts. Weâll begin with the latter, the lower-level storage foundation, and build up from there.
Atomic Git¶
As Iâve outlined earlier, Nixâs current code distribution mechanism has a glaring flaw. To reference a package at a specific version, you must first identify the nixpkgs checkout containing that versionâa process thatâs neither obvious nor trivial. Need another version? Find another nixpkgs checkout. Need both simultaneously? Youâre stuck fetching all of nixpkgsâ unrelated code twice. Anyone whoâs wrestled with a bloated flake.lock file has felt this pain, as Iâve previously noted, so I wonât belabor it here.
If youâre familiar with Gitâs internal object format, though, you might wonder why this is even necessary. Every file and directory in Git is a content-addressed object, which, in theory, should be independently referenceable and fetchable. The issue isnât that Git canât handle thisâitâs that Gitâs conventional linear history model obscures a more elegant solution.
As mentioned, this led me to explore tools like josh proxy, hoping to filter nixpkgsâ history and extract specific package definitions without fetching the entire monorepo. But nixpkgsâ massive history overwhelmed even joshâs impressive speed, and it required a non-standard Git proxy thatâd need ongoing maintenance. Worse, Nix code lacks inherent boundaries, so fetched objects might reference unrelated code from elsewhere in the repo, breaking the isolation we need.
Weâll tackle Nixâs code boundary issue when we discuss the Atom Nix frontend. For now, letâs focus on leveraging Gitâs object structure to solve our storage woes. Git doesnât offer a straightforward API to fetch individual objects, and even if you resort to the lower level plumbing, youâd need their IDs upfrontârequiring a costly search through the projectâs history, which is essentially what tools like josh do.
For the uninitiated, Gitâs high-level entry point is typically a reference (e.g., a branch under refs/heads or a tag under refs/tags). References usually point to a commit or tag object, and users can list them on a Git server with a quick, lightweight requestâno need to fetch object data or sift through history. The reference points to a commitâs hash, letting the client fetch specific objects directly. Pause for a second: this is exactly the behavior we need to fix our problem.
If we could cheaply list server-side references pointing to specific history subsectionsâsay, a Git tree object (a directory)âwithout pulling the entire repo or filtering its history, weâd be golden. If those references had a clear, versioned format, weâd have it all: ping the server, see all available package versions, and fetch only the relevant code, no matter the repoâs size or history.
Thatâs precisely what the Ekala Git storage backend does, at a high level, but since this is a technical deep dive, letâs go a little further.
# demonstration of querying a remote for atom refs with `git` cli
⯠git ls-remote origin 'refs/atoms/*'
62e1b358b25f22e970d6eecd0d6c8d06fad380a7 refs/atoms/core/0.3.0
c85014bb462e55cc185853c791b8946734fd09bf refs/atoms/std/0.2.0
An Atomic Reference¶
The Atom Git Store, as described, uses references to isolate specific repository subsectionsâboth spatially (subdirectories) and temporally (points in history). To make this work seamlessly with Nix, though, we need to address some key details.
Git treats tree and blob objects as low-level implementation details, with no high-level âporcelainâ commands to fetch or manipulate them. Most user-facing tools, including Nix, only understand commit or tag objects. For example, passing a tree object reference to Nixâs builtins.fetchGit function will fail, as it expects a commit, not a tree.
To bridge this gap, we wrap atomic Git trees in orphaned commit objectsâdetached from history, carrying no baggage on fetch. This lets Git-aware tools, like the Git CLI, treat atoms like branches or tags (e.g., for checkout). This detachment, however, risks breaking our requirement to preserve the cryptographic tie between Nix expressions and their source. Fortunately, we can leverage cryptographic primitives to link the atom to its original history rigorously.
How? The implementation offers a peek, but hereâs the gist: we ensure the orphaned commitâs hash is fully reproducible for sanity and hygiene, using a fixed author and timestamp (Unix epoch). To tie it to the source, we embed metadata in the commit objectâs header, which influences its final hash. Specifically, we include:
- The commit SHA from the source history where the atom was copied.
- The relative path from the repository root to the atomâs root.
These, combined with the commitâs reproducibility, yield powerful properties:
- Source Verification: Users can verify the atom by checking the embedded SHA and ensuring the tree object ID at the specified path matches the source commitâs. Since tree objects are content-addressed, this guarantees the atomâs source hasnât been altered.
- Trust and Signing: A verified, reproducible atom commit can be signed with a standard Git tag object. Organizations can use a trusted signing key for added security, ensuring downstream users who trust the key can rely on the atomâs integrity. Since the commit is reproducible, a verified SHA remains trustworthy indefinitely. If a key is compromised, the tag can be revoked and re-signed with a new keyâno need to alter the commit.
- Low Overhead: The atom adds minimal load to the Git server. Using low-level operations via gitoxide, it references existing Git trees and blobs (the actual files). This is like a shallow copy in Rust or Câa new pointer to pre-existing dataâmaking the operation fast and lightweight.
Isotopic Versioning¶
Weâve built a solid foundation for publishing and referencing Nix code (and potentially other languages) with the Atom Git Store. But one critical piece, which Iâve stressed before, deserves its own spotlight: versioning. Itâs the linchpin of the atom scheme and warrants a dedicated section.
Every atom must be versioned, currently using semantic versioning, though we could support other schemes later to accommodate diverse software naturally. As shown earlier, each atomâs Git reference lives at refs/atoms/<atom-id>/<atom-version>. This structure is key for efficient discovery. Querying references from a Git server is lightweight, with filtering done on the server sideâno heavy object fetching required. A single request made with a simple glob pattern can list all atoms and their versions in a repository. Try that with nixpkgs todayâitâs a slog, requiring costly history traversal and git log parsing, with no guarantee of accuracy if the log format hiccups; not to mention you'll have to have the whole history available locally to be exhaustive.
By contrast, the atom format is standardized (though evolving), efficient, and well-typed. When published using the official atom crate library, atoms are guaranteed to conform to spec. We even embed the format version in the atomâs Git commit header, ensuring tools can easily handle future backward-incompatible changes by identifying the format version upfront.
Versioning also enables disciplined dependency management. Dependencies can be locked to simple semantic version constraints (e.g., ^1). Down the line, a version resolver could traverse the dependency tree to minimize the closure while leveraging Nixâs ability to handle multiple software versions seamlessly. This will ensure the smallest possible dependency set, even when different versions are needed in the chain.
Equally critical is the user experience (UX). Versioning as the primary abstraction lowers the barrier to entry for Nix newcomers. Users can fetch, use, or build software without grappling with concepts like âderivations.â Only package maintainers and developers need to dive into Nixâs internalsâevaluation, dependency closures, and the like. Regular users get a smoother, less daunting onboarding while still reaping Nixâs powerful benefits.
Atomic Numbers: A Rigorous Identity¶
This leads us to a critical aspect of atoms: their machine identity. As weâve hinted in the reference and versioning scheme, each atom has a human-readable, Unicode ID specified in its manifest alongside its version. This ID, shown in the Git reference before the version (i.e., refs/atoms/<atom-id>/<atom-version>), uniquely identifies the atom within a repository. To keep things hygienic, we enforce sanity rules: no two atoms in the same repository can share the same Unicode ID in the same commit. For example, you canât have atom âfooâ under both bar/baz and baz/buz simultaneously, but you can move âfooâ between paths across commits.
With thousands or millions of atoms across multiple repositories, Unicode IDs alone become ambiguousâname collisions are inevitable. We need a robust, cryptographic identity to uniquely and efficiently identify atoms. A GitHub discussion (which Iâve tried, and unfortunately failed, to track down for reference here) once highlighted a gap in Nix: it lacks a high-level package abstraction to distinguish âpackagesâ from other derivations. A Nix derivation can represent inputs (sources, patches, build scripts) or outputs (packages, systems, JSON files), yet Nix, despite billing itself as a package manager, offers no unified way to identify a package derivation as distinct among these.
Why does this matter? Try tracking a packageâs evolution in nixpkgs. You might lean on its name or path, but those can shift. Same source, same project, but a tiny tweak changes the derivation hash, and poofâcontinuityâs gone. Without rigor, youâre stuck guessing if itâs the same package across time. Atoms fix this with a machine ID thatâs logical, rigorous, and ties a package to its versions or even dev builds (like their derivation hashes) with mathematical precision.
So, how do we pull this off? We need to disambiguate atoms with the same Unicode ID across repositories. I wrestled with ideasâmaybe the repoâs URL? But URLs shift without touching the projectâs core (name, maintainers, versions). After banging my head on it, the answer hit me: the initial commit hash of the repository. Think about it: a repoâs history flows from one unique starting point: that first "seed" commit. Itâs set in stoneârewrite it, and youâve got a whole new beast. Itâs the perfect, unchanging marker for a repository, no matter where itâs hosted or how it evolves.
From there, we derive the atomâs machine ID using a keyed BLAKE3 hash over the repositoryâs initial commit hash, a constant for key derivation, and the atomâs Unicode ID. BLAKE3âs speed and vast collision space let us index trillions of atoms with negligible risk of collisions. This hash then becomes our bridge, linking the gritty world of derivations to the human world of versions, pulling software distribution idioms cleanly into Nixâs rigorous realm of closures.
And whatâs it good for? A ton. It can power optimizations like bulletproof evaluation and build caches. Picture a backend that spots a userâs requested atom and version, verifies its pinned commit, and checks the organizationâs work history. Been built before? Boomâit skips the work and hands over the artifact. Thatâs not just faster; it splits concerns cleanly. A userâs client doesnât need to touch a Nix evaluatorâjust parse the atom API and ping the backend. If evaluation or buildingâs needed, the backend handles it quietly; if not, you get results instantly.
This opens up a lot of possibilities. Beyond speed, the machine ID boosts provenance tracking, record-keepingâeverything a big outfit might need to manage its atoms or meet compliance standards. And it's important to note: the source identity (that initial commit hash) is an abstraction, so future storage backends can pick their own hash keys, keeping Atom flexible for the future.
Now with atom identities locked in, weâre ready to tackle how non-package content fits into the mix, especially in those sprawling monorepos.
Subatomics¶
Weâre nearly ready to climb the abstraction ladder and explore the Atom Nix frontend. But first, we need to cover one more critical piece planned for the Git store before it hits 1.0. Many organizations rely on large monorepos, blending source code with configurationâthink package descriptions, CI workflows, and more. A single monorepo might house hundreds or thousands of software projects. As Iâve noted, a key goal for the atom format is to work seamlessly across diverse project structures, from sprawling monorepos to small, focused repositories.
If we stopped here, monorepos could still be a pain. Referencing source code from different places and points in history would mean fetching the entire monorepo each timeâechoing the nixpkgs dilemma we outlined earlier. To ensure a consistent, pleasant user experience, we need a way to reference repository subsections that arenât full atom packages, with the same efficiency as atoms.
Enter subatomics, the working title for these lightweight âlensesâ into a monorepoâs vast history, much like atoms but for non-package content. Their format is slightly tweaked to handle less structured data. Instead of named, versioned references, subatomics use a flat, content-addressed form: refs/subs/<git-tree-id>. The Git tree object ID, already a content-addressed identifier, acts as a simple, self-verifying reference for the subsection. For compatibility with Git tooling, each reference points to a reproducible, orphaned commit object, carrying all the same benefits as atoms: reproducibility, verifiability, and optional signing.
Weâll explore how users define subatomics when we move up the abstraction chain, but itâs worth noting that theyâre created only when atoms reference other repository segments (e.g. a source tree for a build) as dependencies, ensuring their existence during the atom publishing phase.
User Entry URIs¶
Weâve thoroughly covered the Ekala Git store, the atom formatâs first storage backend, crafted to tackle Nixâs scaling woes while staying intuitive for newcomers and veterans alike. It leans on, perhaps, the most uncontroversial abstraction in software: the version. With subatomics now in the mix to handle non-package content, weâre ready to shift gears toward the Atom Nix language APIâbut first, letâs talk about user interface, specifically how we reference atoms.
Even the slickest tooling can flop with clunky UX. The eka CLI is still a work in progress, and not all its features tie directly to atoms, but one piece, the atom URI, is already implemented and worth a look. Itâs how we address atoms, and itâs a game-changer for usability.
Now, Iâve had a love-hate relationship with flakes. I went from preaching their gospel in the early days, to groaning every time I deal with them. Yet one thing I always liked was the flake URI. Itâs handy, but not without its flaws. The âshortcutsâ arenât short enoughâIâm still typing most of github.com. Worse, those shortcodes are hardwired into the Nix binary, so if your favorite Git host isnât listed, youâre out of luck. And donât get me started on how flake URIs, embedded in flake.nix, can confuse newcomers and break clickability in editors or IDEs. I wanted to keep what works, fix what doesnât, and add support for explicit atom versions. After a couple of intense hacking weekends, the atom URI was born, and itâs pretty much feature-complete.
The syntax is dead simple. Hereâs the schematic:
[scheme://][[user[:pass]@][url-alias:][url-fragment::]atom-id[@version]
The scheme (e.g., https://, ssh://) is usually omitted, with smart heuristics picking a sane default. The user:pass bit is there for completenessâs sake but rarely needed. The real magic is in user-defined aliasesâthink URL shorteners for common paths:
# eka.toml: client config file
[aliases]
# predefined for convenience
gh = "github.com"
# can build on other aliases
work = "gh:my-verbose-work-org"
cool = "work:our-cool-project"
org = "gitlab.com/some-org"
This lets you write commands like:
⯠eka do org:project::the-atom@^1
⯠eka get work:repo::a-pkg@0.2
⯠eka add cool::cool-atom@^3
When adding an atom as a dependency (like that last command), the manifest stores the full URLâe.g., https://github.com/my-verbose-work-org/our-cool-projectâmaking it readable and clickable. This is crucial: embedding aliases in the manifest would break for downstream users without the same aliases, so we expand them to keep things sane.
Additionally, as a core library component, any tool interacting with atoms can tap this URI format to reference them effortlessly. Itâs a small but mighty piece of the puzzle, making atoms as easy to use as they are powerful.
Now, letâs dive into the Atom Nix language API and explore how it harnesses this foundation to help deliver a more disciplined, scalable Nix experience.
Atomic Nix¶
With the atom URI paving the way for user-friendly access, weâre ready to explore the high-level Atom Nix language frontend. As Iâve said, Atom is fundamentally a packaging API. Weâve dissected the Ekala Git store as a storage backend; now itâs time to unpack what a language frontend needs to mesh with the atom protocol. This depends heavily on the languageâs built-in facilitiesâor lack thereof. Take Rust: integrating Cargo crates with atom would be a breeze, since Cargo already provides a slick, consistent frontend. Itâd likely just need atom as a dependency in the cargo binary and some glue code to tie it together.
Weâre not rushing to support existing formats like Cargo while atomâs still young, but I bring it up to contrast with Nix. Unlike Rust, Nix has almost no native tools for neatly packaging or isolating its code. Building an atom frontend for Nix means crafting core pieces from scratch to make it work.
Hereâs the rub: pairing the atomâs storage format with Nixâs current idioms reveals a glaring issueâNixâs total lack of enforceable code boundaries. If you tried bundling raw nixpkgs code into atoms as-is, youâd get a mess. Itâd be near impossible to untangle, let alone fix.
Why? Nix code can reference anything, anywhere in a repositoryâor even outside it in impure setups. If we naively carve out subdirectories to isolate as atoms, weâd end up with a tangle of broken references and unusable code. Itâs a challenge, but also a chance to tame some of Nixâs wilder complexities. Done right, we could craft an API for Nix thatâs leagues better than the patchwork mess of flakes, et al. Letâs start with the Atom Nix library, the heart of this frontend.
Actual Encapsulation: What a Concept đ€ŻÂ¶
Atom Nix is, at its core, a lean Nix library with a clean API for injecting values into a pure Nix evaluation in a type-safe way. That purity piece deserves its own deep dive, so weâll save it for later and focus on the libraryâs heart: actual encapsulation.
The meat of Atom Nix lives in a single function that delivers what Nix folks toss around loosely: a âmodule system.â But letâs be realâNixâs so-called âmodule systemâ is a far cry from what that term means in any other language. As Iâve ranted before, the NixOS module system falls flat on delivering the containment and consistency youâd expect. Our compose function fixes that, offering true module boundaries with zero bloat, spitting in the face of Nixâs sprawling complexity.
If youâre steeped in Nixâs quirks, you might be clutching your pearls, brainwashed by years of overengineered anti-patterns. No shameâStockholm syndromeâs real. Newcomers, youâve got the edge, unburdened by Nixâs baggage. To my friends who love those idioms: I get it. When youâre dying of thirst, even rancid water looks tempting. But Atom Nix isnât here to coddle complexityâitâs the antidote, ruthlessly focused on delivering real boundaries and isolation, like any decent module system should. Fear not, thoughâbeyond that, it stays out of your way, letting you revel in as much complexity as you like.
Howâs it done? Simple in principle: stop letting Nix reference code willy-nilly. Instead, enforce strict rules on how modules access other code. The secret sauce? A little-known, often-slammed Nix feature: builtins.scopedImport. Iâll nod to the hatersâcareless use of scopedImport is a nightmare, making code untraceable. But we use it internally, and hereâs the kicker: we rig it so itâs literally impossible to call from an Atom Nix module. Take that, chaos.
Hereâs how it works. scopedImport lets us import a Nix file with a custom context injected. We leverage that, plus its ability to override Nixâs default prelude, to make rogue calls to import or scopedImport trigger hard evaluation errors. That means modules can only reference code from our controlled global context. Nix veterans hooked on its prototypical styleâfunctions churning out resultsâmight squirm. But ditching prototypes for an implicit global context, where modules are defined in their final form, is a game-changer.
Why? For one, it makes code introspectable. Prototypes hide their guts until evaluatedâfunction, set, list? Who knows without running it, maybe at a steep cost. With Atom Nix, you see what you get upfront. Plus, rigid boundaries unlock tooling superpowers. A language server could pinpoint code locations and typesâyours or upstream atomsâwithout touching a Nix evaluator. Good luck doing that with Nixâs free-for-all status quo.
Atomic Scopes¶
Though Atom Nix is pre-stable and its scope may evolve, the current pieces are likely here to stay. Every Atom moduleâs evaluation context includes a top-level atom reference, exposing your atomâs public API. The mod scope offers a recursive reference to the current module, including private members.
And yes, Atom modules feature public and private membersâbecause this is, again, a real module system. Access rules mirror Rust: child modules can tap their parentâs private members via the pre scope, which links to the parent module (and its pre.pre for the grandparent, and so on). Public members are declared with a capitalized first letter but accessed externally in lowercase to nod to Nix idioms. We might ditch this convention and fully break from Nixâs normsâstay tuned.
External dependencies split into two scopes. The from scope holds evaluation-time (Nix code) dependencies listed in the manifest. The get scope, kept separate, covers build-time dependencies (like source trees), fetched only during the build phase to avoid blocking evaluation. Unlike flakes, which carelessly fetch everything at eval timeâneeded or notâAtom Nix enforces this split to keep things sane.
Lastly, the std scope holds a built-in standard library of functions, itself an atom, always available in any contextâno need to haul in heavy dependencies like nixpkgs just for basic utilities.
# A concise example of a module nested a few levels deep in an atom
let
inherit (from) pkgs;
in
{
PublicFunc = std.fix (x: { inherit x; });
privateFunc = x: x + 2;
Six = mod.privateFunc 4;
accessParent = pre.pre.privateValue + atom.path.to.this.module.Six;
Package = pkgs.stdenv.mkDerivation {
inherit (get.package) src;
# ...
};
}
Lazy Purity¶
Atom Nix salutes the purity goals flakes introduced years ago, but letâs be real: Nixâs approach is absurdly heavy-handed when the languageâs core features already hand us nearly everything we need on a silver platter.
Take the PR to make flakes fetch inputs lazily. Three years to slap a VFS layer onto the evaluation context? Cool. Atom Nix does it right now though, leaning on Nixâs built-in laziness. đ€Ż
Flakes also love copying everythingâpre-lazy trees VFS, at leastâstraight into the /nix/store like eager beavers. Kudos to the upstream fix (coming⊠someday), but itâs wild that nobody paused to say, âUh, guys, this language is already lazy.â Atom Nix imports expressions into the store for isolation and boundary enforcement, sure, but we do it with the inherent laziness of Nix. No bloat, no wait... Try to hold on. đ€Ż
Each module and expression lands in the store only when accessed, blocking sneaky filesystem references. But sometimes, Nix packaging or config legit needs a local file. Atom Nix has a clean API for that. Relative paths (./.)? Hard noâthey fail, since each lazily imported Nix fileâs working directory is the /nix/store root. Want a file like my-config.toml in your module for a NixOS service? Just use string interpolation: "${mod}/my-config.toml". Itâs lazily imported, disciplined, and keeps your scope tight.
This setup ensures we only touch files in our own module, never rummaging through parentsâ or childrenâs directories. Filtering out parents and children makes lazy store copying dirt cheapâwe copy only the current moduleâs files, lazily, skipping duplicates. No redundant store bloat here.
Now, runtime purity. Nix, outside flakesâ pure eval or a nix.conf toggle, canât fully lock down impurities like absolute path access using just language tricks. We could cave, enable pure eval, and drown in the copying and complexity weâve dodged. Orâhear me outâwe sandbox the evaluation runtime like Nix does for builds. What?! đ€Ż
We start by disabling impure builtins with our scopedImport tactic, the same one that bans random imports. For absolute paths, early tests with a cross-platform sandbox library look promising. The eka CLI or other tools can easily tap this, ensuring the eval runtime sandbox sees nothing but the atom itself. No disk, no nonsense.
And there it is: flake-level purity, no VFS, no three-year wait. Using only the features we already have, and the isolation principles Nix is literally built on đ€Żđ„đ€Ż
Atomic Files¶
Got any brains left? đ
Iâll cop to it: the last segment was dripping with sarcasm. Iâve ranted before about how a well-aimed jab can vaccinate against half-baked ideasâall in good fun, of course. Now, letâs wrap up our tour of the Atom Nix module system with the dead-simple file structure of a Nix atom.
The rules are straightforward: a top-level module is marked by a mod.nix file, and any directory with its own mod.nix is a submodule. For consistency, thereâs no skipping layersâeach module must be a direct child of its parent in the filesystem.
As a bonus, any *.nix file in your moduleâs root (besides mod.nix) gets auto-imported as a member. This keeps long or complex Nix expressions tidy in their own files with zero boilerplate fuss.
# Example: structure of the WIP `std` atom
atom-nix/std
âââ file
â âââ mod.nix
â âââ parse.nix
âââ fix.nix
âââ list
â âââ imap.nix
â âââ mod.nix
â âââ sublist.nix
âââ mod.nix
âââ path
â âââ make.nix
â âââ mod.nix
âââ set
â âââ filterMap.nix
â âââ inject.nix
â âââ merge.nix
â âââ mergeUntil.nix
â âââ mod.nix
â âââ when.nix
âââ string
âââ mod.nix
âââ toLowerCase.nix
# file/mod.nix
{
# Re-export the auto-imported private member from `parse.nix` as public
Parse = mod.parse;
}
Easy enough, right? Now letâs dive into the pulsing core of an atomâthe manifest formatâa make-or-break piece for long-term success, as users will either wrestle or rejoice with it daily.
Static Configuration: An Antidote to Complexity¶
Weâre wrapping up this piece by digging into the manifest format and lock fileâthe heart of atomâs design. Most of what weâve covered so far (barring the explicitly future stuff) is already implemented or proto-typed, but Iâve deliberately held off on the manifest for months. Why? To avoid painting myself into a corner like flakes did. Iâve ranted before about keeping crucial metadata static for better separation of concerns and performance, but this is the deep dive youâve been waiting forâso letâs go all in.
The manifest splits into three clear categories: dependencies, configuration, and metadata. Here are the high-level goals Iâm chasing:
- Totally static, human-editable format: TOML, hands down.
- Intuitive, exhaustive system handling: No weird parsing or Nix code tricksâjust a clear, upfront list of supported systems and cross-configurations.
- Distinct dependency groups: Eval-time vs. build-time dependencies should be crystal-clear, both for performance and sanity.
- Exhaustive package variations: Static vs. dynamic linking, musl vs. glibc, etc., declared upfront to keep Nix code lean and mean.
- Type-checked configuration: After minimal frontend processing, the config gets injected into Nix, purity intact.
Hitting these goals unlocks a ton of goodness:
- Static queries for package variations, systems, and defaults.
- Static schema validation for Nix inputs.
- Static access to metadata without spinning up Nix.
- Static build matrices for CI and caching.
See the theme? We want an exhaustive high-level view of our packageâsystems, variants, metadataâwithout touching Nix evaluation. Clients can serve up package info fast, even without a local Nix install. Users get quicker feedback, fewer âwhy is this so slow?â moments, and a cleaner experience. Itâs a smarter way to tame the chaos of package permutations in nixpkgsâlike pkgsCross or pkgsStaticâwhich are neither obvious nor newbie-friendly. Plus, it beats the shotgun approach of generating every possible variant, whether it works or not. Letâs track what actually builds and make it dead simple for users and CI to grok.
The payoff? Less Nix code complexity, a snappier user-facing API, and smarter build scheduling. Who knew searching the problem space before charging in could work so well?
Iâm hammering out an Ekala Enhancement Proposal (EEP) to lock in a release candidateâcheck the rough draft at ekala-project/atom#51. For completeness's sake, let's just take a quick peek at the TOML and lock format in the next segment.
Atomic Manifest: A Sketch¶
Letâs riff off the draft in ekala-project/atom#51. This will, therefore, be the latest snapshot until the Ekala Enhancement Proposal is finalized. This is the manifestâs current vibe, and itâs shaping up to be the user-friendly core of atom.
# Package identity and metadata
[atom]
id = "mine"
version = "0.1.0"
# Type determines the configuration schema
type = "nix:package" # Or nix:config, nix:deployment, etc.
[atom.meta]
# Similar to pkg.meta in current Nix packages
description = "A cool package doing cool things"
license = "MIT"
maintainers = ["alice <aliceiscool@duh.io>", "bob <bobsalright@fine.com>"]
## Dependencies: eval-time (Nix code) and build-time (sources, tools)
### Eval-time Atom dependencies
[deps.atom] # Available at `from.atom`
url = "https://github.com/ekala-project/atom"
version = "^1"
[deps.my-lib] # e.g., eka add work:mono@^2
url = "https://github.com/org/mono"
version = "^2"
[deps.local] # Local atom in the same repo
path = "../../path/to/other/atom" # locked in lock file
### Eval-time legacy Nix libraries
[pins.pkgs] # Available at `from.pkgs`
git = "https://github.com/NixOS/nixpkgs"
ref = "nixos-25.05"
# Expression to import, since we canât do it ourselves
entry = "pkgs/top-level/impure.nix"
## Build-time sources: tarballs, git repos, subatomics, lock files
### Tarball source
[srcs.src] # Available at `get.src`
url = "https://example.com/v${major}/${version}/pkg.src.tar.xz"
# Version for URL string interpolation
version = "${atom.version}"
### Git source
[srcs.repo]
git = "https://github.com/owner/repo"
ref = "v1"
### Subatomic reference
[srcs.pkg] # Locked as git tree-id in lock file
path = "../../my/source/tree"
# No URL; assumed to be in the same repo
### Lock file for builders
[srcs.cargo] # For builder libs or plugins
path = "../Cargo.lock"
## Build configuration: platforms, variants, and distribution formats
### Supported/tested/cached cross-compilation matrix
[platform]
# BUILD:HOST:TARGET, with shell-style expansion (< = previous value)
supported = [
"riscv64-linux",
"x86_64-linux:{<,aarch64-linux}",
"{aarch64-darwin,x86_64-darwin}:{<,aarch64-linux,x86_64-linux}"
]
### Abstract packages for variants
[provide] # e.g., eka do --cc=clang --host=aarch64-linux <uri>
ld = ["binutils", "mold"] # From deps, default: first
cc = ["gcc", "clang"]
libc = ["glibc", "musl"]
### Dependency-free build variations
[support]
# Flags injected into build command if requested; off by default
my-feature-flag = ["MY_FEATURE=1"]
# Boolean toggle, overridable by client
static = false
### Distribution formats, e.g., `eka get --oci` for OCI container
[dist]
formats = ["deb", "oci"]
The lock fileâs a snooze compared to the manifestâjust a list of hashes to lock in reproducibility. Its schemaâs still in flux, so weâll skip the details for now, but hereâs the key bit: local path dependencies (like [deps.local] or [srcs.pkg]) get pinned in the lock file with both their git tree IDs and reproducible âatomicâ commit hashes for sanity. Before publishing, the publish logic double-checks the lockâs accuracyâmessed up? It bails.
The [provide] and [support] keys both define build configurations, but hereâs the difference: [provide] expects extra dependencies from nix:package-type atoms (e.g., picking clang or gcc), while [support] handles dependency-free tweaks like flags or toggles (e.g., static = true). This keeps variants clear and Nix code lean.
Future backends, like the proposed Eos API, will cryptographically track built variant combinations to skip redundant builds and turbocharge cachingâas we alluded to earlier.
With that, weâve unpacked every major piece of the atom format in gritty detail. The brave can dive into the code or contribute, but for now, letâs wrap it all up.
Forging the Future: A Call to Rethink Nix¶
Wow, props to you for slogging through this beast of a piece, dense with technical grit. I wouldnât blame you if it took a few sittings to digestâIâve spent a year wrestling words to explain it half-decently. Atomâs design tackles Nixâs scaling woes head-on: a Git store for lightweight versioning, URIs for snappy user access, lazy purity to ditch flakesâ bloat, module boundaries to tame code chaos, and a static manifest to make daily use a breeze. Letâs revisit our core motivation with this full picture in hand.
The atom format is bold, aiming to be a long-term packaging API and a rock-solid replacement for Nix idioms buckling under scale. But is it worth it? Iâm no zealotâIâll admit defeat if itâs time. Yet, from my years in the Nix trenches, Iâm convinced itâs a thundering yes. Skeptics might cling to flakesâ familiarity, but atomâs rigor, built on 20 years of Nix lessons, offers stability, not chaos. We could keep patching flakesâ half-baked API or stretch nixpkgsâ creaky architecture until it snaps. Or we can honor the grind that got us here and see this as a new beginning.
Many Nix abstractions will stick around, atom or no atomâIâm sure of it. But their shape could shift dramatically. I respect the magic thatâs carried Nix for 20 years, but weâve mostly been tweaking old idioms. With two decades of global-scale lessons, weâve got the perspective to ask, âWhatâs next?â Imagine a Nix ecosystem where builds are fast, configs are intuitive, and scaleâs no issueâAtom just might be the spark to get us there.
Look, if youâve read this far, you clearly care about Nix and its innovation. You've also seen that Iâve got strong opinionsâmy ramblings prove itâbut they've been forged iteratively, over a long timespan, from questioning my own assumptions and ditching what doesnât work. Atomâs not my pet project; itâs a community effort, and your ideas will shape its path. So, join us on Discord and share your take. Be brutally honest or wildly supportiveâjust bring your real thoughts. Whatever comes next, thanks for diving deep into my ideas. Catch you soon! And...
Viva Rebellion!