← Back to Blog

Introducing Urd

urddevtoolsconfigurationcli

Urd is a configuration tool built for small teams or solo developers. It was born out of solving my own configuration management needs. I have multiple personal projects as well as my "commercial" projects under Cone Crows. Each has its own configuration needs, ranging from storing secrets and api keys to access various services, to enabling feature flags in different environments.

When I first started Builds, the surface area for configuration and secrets was relatively small: config was stored in .env files scattered across component directories. When my personal workflow expanded to include three different development workstations, all of a sudden I needed to synchronize that config. My first attempt was a hacked combination of shell scripts and consul/consul-template.

It was a dirty hack, but it worked ... for one project. I leveraged the same system for my next few projects, but it just didn't scale. So this problem space - managing configuration and secrets across multiple environments and projects, has been on my mind for a while. I recently decided to spend the time to properly fix this problem, and I hope it can be valuable for others.

But What Is It?

Urd is not configuration management in a traditional enterprise sense. Urd does not push config to a fleet of servers. Nor does Urd provision resources in an arbitrary environment. Those spaces have mature tools. Instead, Urd is about managing the configuration for a single project, supporting multiple environments, and to be safely managed by source control.

Urd is based around three main axes: a catalog of configuration items, templates, and topologies. The catalog is a simple yaml file that contains the configuration items, their metadata, and their values. Templates are a means of outputting filtered config values, and topologies map templates to components and environments.

More on templates: these support handlebars-style expressions {{ item.id }} that resolve against the store — they preserve the structure of your target file (comments, hardcoded values, ordering) while filling in the managed values.

DX is important. So Urd includes a rich CLI as well as a TUI. The CLI is full featured and is equally suitable for direct human consumption, scripting, or even AI use. The TUI in comparison provides a deliberately more interactive experience, including undo and redo.

Source Control Friendly

The catalog is a single yaml file, which means project configuration lives in the same repo. Furthermore, it's human readable and easily diffable. Urd supports multiple sensitivity levels (plain, sensitive, and secret). Items declared as sensitive or secret are encrypted, so even secrets live safely in your repo.

Encryption requires no infrastructure or servers. Values are encrypted via AES-256-GCM, and your encryption keys are local to your environment. Distributing/synchronizing these keys across multiple workstations/development environments is an exercise for the developer. I personally scp them. Encrypted values appear as ENC[aes:sensitive,...] in the catalog, maintaining human readability and diffability.

So What Does Using Urd Look Like?

Let's walk through setting up Urd for a typical project — say a web app with an API server and a Next.js frontend.

Bootstrap the project:

urd init

Init walks you through three steps: generating an encryption key (stored locally, never in the repo), choosing your default environments, and creating a starter topologies.yaml. If any step is already configured, it skips ahead.

Add configuration values:

urd set supabase.url --env dev http://localhost:54321
urd set supabase.url --env prod https://myproject.supabase.co
urd set stripe.secret_key --env dev --secret sk_test_abc123

Or just run urd set with no arguments and it walks you through each field interactively — ID, environment, sensitivity, value, and optional metadata like description and origin. You can also run urd with no arguments to launch the TUI for a more visual experience.

Values flagged as sensitive or secret are encrypted automatically. Your store ends up looking something like this:

items:
  supabase.url:
    description: Supabase API URL
    sensitivity: plaintext
    origin: Supabase dashboard
    environments: [dev, prod]
    dev: http://localhost:54321
    prod: https://myproject.supabase.co

  stripe.secret_key:
    description: Stripe secret key
    sensitivity: secret
    origin: Stripe dashboard > Developers > API keys
    environments: [dev, prod]
    dev: ENC[aes:secret,YWJjMTIzNDU2Nzg5...]
    prod: ENC[aes:secret,eHl6OTg3NjU0MzIx...]

Human-readable, diffable, and safe to commit.

Define templates:

Templates live alongside the components that consume them. For the API, you might create api/.env.template:

# target: .env
NODE_ENV=dev
PORT=3002
DATABASE_URL={{ supabase.database_url }}
SUPABASE_URL={{ supabase.url }}
STRIPE_SECRET_KEY={{ stripe.secret_key }}

Hardcoded values pass through unchanged. {{ }} expressions resolve against the store. The # target: line tells Urd where to write the output.

Wire it together with topologies:

topologies.yaml maps components to environments:

all-dev:
  api: dev
  web: dev

all-prod:
  api: prod
  web: prod

hybrid:
  api: dev
  web: dev
  overrides:
    api:
      supabase.*: prod

That hybrid topology is where it gets interesting — run your API locally but pointed at your production Supabase instance. The override uses glob matching, so supabase.* catches supabase.url, supabase.database_url, and any other supabase. prefixed items.

Assemble:

urd assemble --topology all-dev

Urd resolves every template against the store, decrypts any encrypted values, and writes the resulting .env files. Want to preview before writing?

urd assemble --topology hybrid --dry-run

That's it. Your config is cataloged, encrypted at rest, and assembled on demand. Add .env to your .gitignore and commit the .urd/ directory and templates instead.

Urd is open source — check out the repo at github.com/thutch-conecrow/urd.