Auto-Discovery

Automatically discover configuration files in monorepos and multi-application projects.

Confy can automatically find your configuration files by searching the current directory and parent directories. This is especially useful in monorepos where your application may be nested several directories deep.

Quick Start

cfg, err := confy.AutoLoadConfy("myapp", nil)
if err != nil {
    log.Fatal(err)
}

port := cfg.GetInt("server.port", 8080)

AutoLoadConfy uses default settings to search for config.yaml and config.local.yaml starting from the current working directory, walking up parent directories.

Discovery Configuration

For more control, use DiscoverAndLoadConfigs with an AutoDiscoveryConfig:

cfg, result, err := confy.DiscoverAndLoadConfigs(confy.AutoDiscoveryConfig{
    AppName:          "myapp",
    SearchPaths:      []string{".", "./config", "/etc/myapp"},
    ConfigNames:      []string{"config.yaml", "config.yml", "config.json"},
    LocalConfigNames: []string{"config.local.yaml", "config.local.yml"},
    MaxDepth:         5,
    RequireBase:      true,
    EnableEnvSource:  true,
    EnvPrefix:        "MYAPP",
    EnvSeparator:     "_",
    EnvOverridesFile: true,
})

if err != nil {
    log.Fatal(err)
}

// Discovery result contains useful metadata
fmt.Println("Base configs:", result.BaseConfigPaths)
fmt.Println("Local configs:", result.LocalConfigPaths)
fmt.Println("Working dir:", result.WorkingDirectory)
fmt.Println("Monorepo:", result.IsMonorepo)

// Convenience methods return the first path (backward compat)
fmt.Println("First base:", result.BaseConfigPath())
fmt.Println("First local:", result.LocalConfigPath())

Configuration Options

OptionTypeDefaultDescription
AppNamestring""Application name (used for app-scoping)
SearchPaths[]string["."]Directories to search
ConfigNames[]string["config.yaml"]Base config filenames
LocalConfigNames[]string["config.local.yaml"]Local override filenames
MaxDepthint5Max parent directories to search
RequireBaseboolfalseError if base config not found
RequireLocalboolfalseError if local config not found
EnableAppScopingboolfalseExtract app-scoped config
EnableEnvSourceboolfalseAdd environment source
EnvPrefixstring""Env variable prefix
EnvSeparatorstring"_"Env key separator
EnvOverridesFilebooltrueEnv vars override file values

Search Algorithm

The discovery process searches all configured search paths and merges every config it finds:

  1. For each search path, look for any of the ConfigNames files
  2. If not found, walk up to the parent directory (up to MaxDepth levels)
  3. Once found, also look for LocalConfigNames in the same directory
  4. Repeat for every remaining search path — all paths are searched, not just the first match
  5. Duplicate paths are removed automatically
  6. Optionally add an environment source
project-root/
├── config.yaml          ← discovered (base, priority 100)
├── config.local.yaml    ← discovered (local, priority 200)
├── services/
│   └── api/
│       └── main.go      ← working directory (cwd)

Multi-path merging

When multiple search paths each contain config files, they are all loaded with incrementing priorities so later paths layer on top of earlier ones:

SearchPaths: ["./config", "/etc/myapp"]

./config/config.yaml       → base priority 100
/etc/myapp/config.yaml     → base priority 101 (overrides ./config values)
./config/config.local.yaml → local priority 200
/etc/myapp/config.local.yaml → local priority 201 (overrides ./config local)

All local configs override all base configs, and environment variables (priority 300 by default) override everything.

App-Scoped Configuration

In monorepos with multiple applications sharing a single config file, use app-scoping to extract the relevant section:

# config.yaml (shared)
database:
  host: shared-db.internal

apps:
  api:
    port: 8080
    database:
      host: api-db.internal

  worker:
    concurrency: 10
    database:
      host: worker-db.internal
cfg, err := confy.LoadConfigWithAppScope("api", logger, nil)
if err != nil {
    log.Fatal(err)
}

// Gets "api-db.internal" — app config overrides shared config
host := cfg.GetString("database.host")

// Gets 8080 — from the api app section
port := cfg.GetInt("port")

How App-Scoping Works

  1. Load the full configuration file
  2. Find the apps.<appName> section
  3. Merge the app-specific config over the global config
  4. App keys override global keys at the same path

Priority Resolution

When using app-scoping with multiple sources:

# config.yaml (priority 100)
database:
  host: base-host
apps:
  api:
    database:
      host: app-host

# config.local.yaml (priority 200)
database:
  host: local-host

For app api, the resolution is:

  1. base-host from base config (priority 100)
  2. app-host from app-scoped base config
  3. local-host from local config (priority 200) — wins

Local config globals always override app-scoped base config.

Loading from Explicit Paths

When you know exactly where your config files are:

cfg, err := confy.LoadConfigFromPaths(
    "/etc/myapp/config.yaml",       // base path
    "/etc/myapp/config.local.yaml", // local path
    "api",                          // app name (optional)
    logger,
)

Debugging Discovery

Use GetConfigSearchInfo to understand where confy would look for configs:

info := confy.GetConfigSearchInfo("myapp")
fmt.Println(info)
// Outputs search paths, filenames, and priority information

Default Discovery Configuration

DefaultAutoDiscoveryConfig() returns sensible defaults:

defaultCfg := confy.DefaultAutoDiscoveryConfig()
// SearchPaths: ["."]
// ConfigNames: ["config.yaml", "config.yml"]
// LocalConfigNames: ["config.local.yaml", "config.local.yml"]
// MaxDepth: 5
// EnvSeparator: "_"
// EnvOverridesFile: true

The config.local.yaml file is typically added to .gitignore so each developer can maintain their own overrides without affecting the shared configuration.

How is this guide?

On this page