Profiles & Modules

Profiles and modules give you a single repo that applies the right config to each machine — work laptop, personal Mac, minimal server.

The model

Modules control packages (Homebrew and mise). A module is a TOML file at modules/<name>.toml that points to a Brewfile and/or a mise config.

Profiles control which modules are active on a given machine. Profiles are declared in haven.toml.

Files are not scoped to modules. Every file in source/ is applied on every haven apply, regardless of module or profile. Profiles only affect which packages get installed.

Modules

Creating a module

# modules/shell.toml
[homebrew]
brewfile = "brew/Brewfile.shell"

[mise]
config = "source/mise.toml"
# modules/work.toml
[homebrew]
brewfile = "brew/Brewfile.work"
# modules/secrets.toml
requires_op = true    # skip if 1Password isn't available

Module fields

Field Description
[homebrew] brewfile Path to Brewfile, relative to repo root
[mise] config Path to mise config file, relative to repo root
requires_op If true, skip brew/mise if op is not installed/signed in

Applying a single module

haven apply --module shell      # brew + mise for the shell module only
haven apply --brews --module work  # Homebrew only, work module

Note

--module scopes brew and mise operations only. Dotfiles in source/ are always applied globally.

Profiles

Declaring profiles

# haven.toml

[profile.default]
modules = ["shell", "git", "packages"]

[profile.work]
extends = "default"           # inherits all modules from default
modules = ["work", "secrets"] # then adds these

[profile.personal]
extends = "default"
modules = ["personal"]

[profile.minimal]
modules = ["shell"]           # just the essentials

extends gives you single-level inheritance: the parent's modules come first, then the child's are appended with duplicates removed.

Applying profiles

haven apply --profile work
haven apply --profile minimal
haven status --profile work
haven diff --profile personal

The last-used profile is saved in ~/.haven/state.json and reused automatically on subsequent commands.

Practical patterns

Work vs. personal

[profile.default]
modules = ["shell", "git", "editor"]

[profile.work]
extends = "default"
modules = ["work-tools", "vpn-config", "secrets"]

[profile.personal]
extends = "default"
modules = ["games", "media"]

Server profile

[profile.server]
modules = ["shell"]    # no GUI apps, no casks, minimal tooling

Secrets isolated in a module

# modules/secrets.toml
requires_op = true

[homebrew]
brewfile = "brew/Brewfile.secrets"

The requires_op = true flag means this module is silently skipped on machines where you're not signed into 1Password.

Custom data per profile

You can't currently set different [data] values per profile — [data] in haven.toml is global. Use template conditionals on the profile variable instead:

# source/dot_gitconfig.tmpl
[user]
{% if profile == "work" %}
  email = alice@corp.example
{% else %}
  email = alice@personal.example
{% endif %}

Committing profiles

Commit haven.toml with all your profiles. On a new machine:

haven init gh:yourname/haven --apply --profile work

This clones the repo and applies the right profile in one step — no manual configuration needed.