An external orchestration program (written in Gleam) that manages Emacs configuration through bidirectional JSON-RPC over stdio, providing deterministic, dependency-aware package installation and configuration management inspired by NixOS principles.
After over a decade of using Emacs, I grew frustrated with a persistent problem: my configuration would break unexpectedly, and I'd have to debug it before I could actually get work done.
The root cause: no true dependency resolution
Traditional Emacs configuration tools don't actually resolve dependencies. with-eval-after-load is just a convenience macro that defers code execution until a library loads - it doesn't guarantee execution order across multiple deferred blocks. Similarly, use-package's :after keyword delays loading until dependencies are available, but doesn't provide deterministic ordering. There's no built-in way to declare "configure B only after A is fully ready" and have the system guarantee it.
This leads to non-deterministic behavior where the same configuration could behave differently across restarts or machines. Race conditions and timing issues create bugs that are nearly impossible to track down.
The lazy loading dilemma
I've been a Doom Emacs user for years with heavily customized config. Doom's lazy loading is excellent for fast startup, but I kept hitting the same frustrating pattern:
- Make a mistake in my config or custom elisp
- Error only appears when that feature loads - could be hours into my session
- Hard to debug because the context is lost
The problem isn't lazy loading itself - it's that errors hide until runtime. Traditional approaches optimize for startup speed but defer error discovery.
The fail-fast philosophy
That's why Emacs Backbone takes the opposite approach: load everything at startup in deterministic order and encounter all errors immediately. Fix them right away while the context is fresh. Once Emacs starts successfully, you can be confident nothing will break hours into your session. If you comment out a package, its config-unit automatically skips - no manual cleanup needed.
This trades a few extra seconds at startup for reliability. Since Emacs is a long-lived process that runs for days or weeks, I'd rather fail fast at startup than risk hours of lost work from a mid-session crash.
What Emacs Backbone provides
Inspired by NixOS principles, Emacs Backbone treats configuration as a dependency graph that's resolved deterministically using topological sorting:
- True dependency resolution - Declare dependencies explicitly, the system figures out the order
- Deterministic execution - Same declarations always produce the same order
- Immediate feedback - All configuration errors surface at startup
- Reproducible behavior - Your config works the same way every time, on every machine
Every package and configuration unit declares its dependencies explicitly, and the system ensures they execute in the correct order - every single time.
Why Gleam?
I happened to be learning Gleam when this idea crystallized, and it turned out to be the perfect choice. Gleam's strong static type system and compiler give me confidence that once it compiles, it will work. The compiler catches potential issues before runtime, which is exactly the kind of reliability I was seeking for my Emacs configuration. This compile-time safety complements the deterministic runtime behavior - together they ensure that configuration problems are caught early and behavior remains consistent.
Why not just `use-package :after`?
A common question is: If use-package has the :after keyword, why do we need config-unit! and an external orchestrator?
The short answer is that use-package is an excellent package loader, but a very limited state manager. When your configuration scales, relying solely on :after for dependency management breaks down in a few critical ways.
The most glaring limitation of use-package :after is handling configurations that depend on multiple packages being loaded and configured first.
Take the evil-org package as an example. To configure it properly, you need both evil and org to be fully initialized. Where does the integration code go? With use-package, you are forced into awkward, tightly coupled nesting:
;; The use-package way: Awkward and conceptually messy
(use-package org
:config
;; Do org stuff...
(use-package evil-org
:after evil
:config
;; Wait, does this belong in the 'org' block or the 'evil' block?
(evil-org-set-key-theme)))This forces your integration logic to live inside the namespace of a single, arbitrary package.
With emacs-backbone and config-unit!, you decouple the integration entirely. You create a distinct, independent unit that explicitly declares its upstream dependencies. It sits perfectly at the intersection of the two, keeping your base package configs clean:
;; The emacs-backbone way: Clean, decoupled dependency declaration
(config-unit! evil-org-integration
:requires (evil-org)
:after (org-base evil-base)
:config
(evil-org-set-key-theme))Under the hood, use-package :after is just syntactic sugar for Emacs's built-in with-eval-after-load. This is a reactive system. It tells Emacs: "Keep an eye out, and if package X ever happens to load, run this code."
Because it is reactive, your configuration's execution order is highly vulnerable to the physical order of your init.el file, autoload triggers, or user actions. This is a common source of silent failures, race conditions, and infinite loading loops.
emacs-backbone is deterministic. The Gleam backend reads your config-unit! declarations, parses them into a strict Directed Acyclic Graph (DAG), and uses topological sorting to guarantee mathematically perfect execution order over JSON-RPC. The physical location of the code in your files no longer matters; the dependencies dictate the flow.
use-package operates strictly at the package level—it waits for a package to call (provide 'some-package).
However, in advanced Emacs setups, you often need to depend on a logical state, not just a package. For example, you might want your customized modeline to load only after your entire "UI Framework" (fonts, themes, and fringes) is fully configured. config-unit! allows you to depend on arbitrary blocks of configuration state to resolve, giving you orchestration granularity that use-package simply cannot provide.
Emacs Backbone separates the configuration framework from user configuration, similar to Doom Emacs:
- Core System (
~/.config/emacs): Gleam backend, Emacs Lisp framework, macros, and orchestration logic - User Configuration (
~/.config/backbone): Your personalconfig.org, package declarations, and config units
This separation allows you to:
- Version control your user config independently from the core
- Receive core updates without affecting your personal configuration
- Share your configuration with others easily
The heart of Emacs Backbone lies in two declarative macros that replace traditional Emacs configuration patterns:
The package! macro declares packages with explicit dependencies, eliminating load-order guesswork:
;; Simple package declaration
(package! magit)
;; Package from a specific repository
(package! evil
:repo "emacs-evil/evil"
:host "github")
;; Package with dependencies - Emacs Backbone ensures correct installation order
(package! evil-collection
:repo "emacs-evil/evil-collection"
:deps (evil))
;; Local package with subdirectories
(package! lsp-bridge
:local "~/projects/lsp-bridge"
:files ("*.el" "*.py" "acm" "core" "langserver")
:no-compilation t
:deps (markdown-mode yasnippet))Key features:
- Dependency resolution: Use
:depsto declare package dependencies - the system guarantees they install first - Repository flexibility: Install from GitHub, GitLab, or any git host with
:repoand:host - Local packages: Use
:localfor packages from local filesystem, with:filesto include subdirectories - Version pinning: Use
:branch,:tag, or:reffor reproducible builds - No manual ordering needed: Declare packages anywhere in your config - dependencies are resolved automatically
The config-unit! macro replaces use-package, with-eval-after-load, and scattered configuration blocks with explicit, ordered units:
;; Basic configuration unit
(config-unit! evil-config
:requires (evil) ; Require evil before running this unit
:config
(evil-mode 1)
(setq evil-want-C-u-scroll t))
;; Configuration with ordering dependencies
(config-unit! evil-collection-config
:requires (evil-collection)
:after (evil-config) ; Guaranteed to run after evil-config
:config
(evil-collection-init))
;; Complex dependency graph - automatically resolved
(config-unit! my-keybindings
:after (evil-config evil-collection-config which-key-config)
:config
(global-set-key (kbd "C-c g") 'magit-status))
;; Runtime prerequisites: require env vars and executables before running
(config-unit! llm-tools
:requires (gptel)
:env (OPENAI_API_KEY ANTHROPIC_API_KEY)
:executable (rg git)
:config
(message "LLM tooling is ready"))Key features:
:requires- Require listed features before running the unit body:after- Execution ordering betweenconfig-unit!names (notpackage!names); unlikeeval-after-load, this is guaranteed:env- Require environment variables to be set before the unit runs:executable- Require external binaries to exist onPATHbefore the unit runs:config- Configuration body that runs in deterministic order- Topological sorting - Complex dependency graphs are automatically resolved
- Circular dependency detection - Fails fast if your configuration has cycles
- Declaration phase: You declare
package!andconfig-unit!in any order in yourconfig.org - Dependency resolution: Gleam backend extracts all declarations and builds a dependency graph
- Package installation: Packages install in dependency order (packages with no deps first)
- Configuration execution: After all packages install, config units execute in topologically sorted order
- Guaranteed determinism: Same declarations always produce the same execution order
This replaces the traditional Emacs configuration chaos with a predictable, reproducible system.
- Emacs 29+ (with native compilation support recommended)
- Bun (runtime for
generated/emacs-backbone.mjs)
-
Clone the core repository:
git clone git@github.com:nohzafk/emacs-backbone.git ~/.config/emacs cd ~/.config/emacs
-
Create your user configuration directory:
mkdir -p ~/.config/backbone -
Create your configuration:
Create
~/.config/backbone/config.orgwith your Emacs configuration usingpackage!andconfig-unit!macros. For example:* Package Declarations #+begin_src emacs-lisp (package! evil :repo "emacs-evil/evil") (package! which-key :repo "justbur/emacs-which-key") #+end_src * Configuration Units #+begin_src emacs-lisp (config-unit! evil-setup :requires (evil) :config (evil-mode 1)) (config-unit! which-key-setup :requires (which-key) :after (evil-setup) :config (which-key-mode 1)) (config-unit! ai-setup :requires (gptel) :after (which-key-setup) :env OPENAI_API_KEY :executable (git rg) :config (message "AI tooling enabled")) #+end_src -
Start Emacs:
emacs
-
Tangle and reload from inside Emacs:
C-h r eto tangle~/.config/backbone/config.orgintoconfig.elC-h r rto reload the freshly tangled config
On first startup, Emacs Backbone may warn that
config.eldoes not exist yet. That is expected until you tangle it for the first time.
For a complete, real-world example of an Emacs Backbone configuration, see:
nohzafk's literate-darwin config
Default: ~/.config/backbone
Directory containing your user configuration. Customize if you want to use a different location:
(setq emacs-backbone-user-directory "~/my-custom-config")Default: Auto-detected (/opt/homebrew/bin/bun or from PATH)
Path to the Bun executable used for generated/emacs-backbone.mjs:
(setq emacs-backbone-bun-executable "/opt/homebrew/bin/bun")Default: nil
Enable verbose logging and debugging output:
(setq emacs-backbone-enable-debug t)M-x emacs-backbone-start- Initialize Emacs Backbone (auto-invoked in init.el)M-x emacs-backbone-tangle-config- Tangleconfig.orgtoconfig.elM-x emacs-backbone-reload-config- Full reload: tangle + reinitialize
The Gleam backend is managed over stdio, so it exits with Emacs. There is no separate manual shutdown command.
C-h r e- Tangle configurationC-h r r- Reload configuration
bun run build # Regenerate generated/emacs-backbone.mjs
gleam test # Run testsThe repo ships a committed Bun bundle so end users do not need Gleam installed at runtime. Emacs always starts Backbone with:
(list emacs-backbone-bun-executable
(expand-file-name "generated/emacs-backbone.mjs" user-emacs-directory))Regenerate that bundle during development with:
bun run buildThis script:
- Runs
gleam build - Bundles the generated Gleam JavaScript into
generated/emacs-backbone.mjs - Produces the stable backend entrypoint Emacs runs with Bun
Run the committed bundle directly with:
bun generated/emacs-backbone.mjsIf Emacs cannot find Bun in its environment, set
emacs-backbone-bun-executable to an absolute path.
Developers need both Gleam and Bun.
When you change backend code under src/:
- Edit the Gleam source
- Run
bun run build - Run
gleam test - Commit both the source changes and the updated
generated/emacs-backbone.mjs
When you change only the Emacs Lisp side under lisp/, you usually do not need
to rebuild the generated backend bundle.
Normal users need only Bun.
- Install Bun
- Clone this repo to
~/.config/emacs - Keep
generated/emacs-backbone.mjsfrom the repo checkout - Start Emacs
No Gleam installation is required unless the user intends to modify the Gleam backend and regenerate the committed bundle.
~/.config/emacs/ # Core system (this repository)
├── src/ # Gleam backend
│ ├── emacs_backbone.gleam # Entry point
│ ├── jsonrpc.gleam # JSON-RPC communication over stdio
│ ├── resolver.gleam # Dependency resolution
│ └── ...
├── lisp/ # Core Emacs Lisp framework
│ ├── backbone.el # JSON-RPC client & lifecycle
│ ├── macro-package.el # package! macro
│ ├── macro-config-unit.el # config-unit! macro
│ └── ...
├── init.el # Core initialization
├── early-init.el # Early initialization
├── gleam.toml # Gleam project config
└── package.json # JavaScript module metadata
~/.config/backbone/ # User configuration (you create this)
├── early-init.el # Your early initialization (optional)
├── config.org # Your literate configuration
├── config.el # Generated from config.org
├── config/ # Your helper utilities
└── clis/ # Your CLI configurations
Emacs Backbone currently bootstraps against Elpaca installer version 0.12 in
lisp/package-manager.el.
This integration currently depends on some Elpaca implementation details, not
just its public user-facing configuration surface. In particular, Backbone has
workarounds for split packages in a shared monorepo checkout, such as
magit-section living inside the shared magit repository checkout, and
packages like embark-consult living inside the shared embark checkout.
That means Elpaca upgrades should be treated as compatibility work: if Elpaca's shared-source, clone-skip, or dependency-detection internals change, Backbone's integration may need to be updated as well.
src/- Gleam backend source codelisp/- Core Emacs Lisp frameworktest/- Gleam testsbin/- Utility scripts
If you see "Emacs Backbone user config not found", ensure:
~/.config/backbone/config.elexists- You've tangled your
config.orgfile - The
emacs-backbone-user-directoryvariable points to the correct location
Check:
- Bun is installed:
which bun - The
emacs-backbone-bun-executablevariable is correct generated/emacs-backbone.mjsexists in the repo- If you changed Gleam source, you rebuilt the bundle with
bun run build
- Check the
*emacs-backbone*buffer for errors - Enable debug mode:
(setq emacs-backbone-enable-debug t) - Verify your package declarations syntax in
config.el