Putnami
DocsGitHub

Licensed under FSL-1.1-MIT

Getting Started
Concepts
How To
Build A Web App
Build An Api Service
Share Code Between Projects
Configure Your App
Add Persistence
Add Authentication
Add Background Jobs
Principles
Tooling & Workspace
Workspace Overview
Cli
Jobs & Commands
SDK
Error Handling
Extensions
Typescript
Go
Python
Docker
Ci
Frameworks
Typescript
OverviewWebReact RoutingForms And ActionsStatic FilesApiErrors And ResponsesConfigurationLoggingHttp And MiddlewareDependency InjectionPlugins And LifecycleSessionsAuthPersistenceEventsStorageCachingWebsocketsTestingHealth ChecksTelemetryProto GrpcSmart Client
Go
OverviewHttpDependency InjectionPlugins And LifecycleConfigurationSecurityPersistenceErrorsEventsStorageCachingLoggingTelemetryGrpcService ClientsValidationOpenapiTesting
Platform
  1. DocsSeparator
  2. FrameworksSeparator
  3. GoSeparator
  4. Configuration

Configuration

go.putnami.dev/config provides multi-source configuration loading with struct-tag-driven mapping, environment variable overrides, and DI integration.

Defining configuration

Define a typed configuration at a path in the config tree:

import "go.putnami.dev/config"

type ServerConfig struct {
    Host    string `json:"host" default:"localhost"`
    Port    int    `json:"port" default:"8080" env:"PORT"`
    Debug   bool   `json:"debug" default:"false" env:"DEBUG"`
    Timeout int64  `json:"timeout" default:"30" env:"TIMEOUT"`
}

var serverDef = config.Config[ServerConfig]("server")

The path uses dot-notation for nested structures:

// Navigates to root["services"]["api"]["server"]
var apiServerDef = config.Config[ServerConfig]("services.api.server")

Struct tags

Tag Description Example
json:"name" Field name in the config tree json:"port"
default:"value" Default value when no source provides one default:"8080"
env:"VAR" Environment variable override (highest priority) env:"PORT"

Priority order (highest to lowest):

  1. Environment variables (via env tag)
  2. Explicit source data
  3. Default values (via default tag)

Loading configuration

cfg, err := config.Load(serverDef,
    config.NewMapSource("file", 50, map[string]any{
        "server": map[string]any{
            "host": "0.0.0.0",
            "port": 3000,
        },
    }),
)
// cfg.Host == "0.0.0.0"    (from source)
// cfg.Port == 3000          (from source, unless PORT env var is set)
// cfg.Debug == false        (from default tag)

Multiple sources are merged in priority order (higher priority wins):

cfg, err := config.Load(serverDef,
    config.NewMapSource("defaults", 10, defaultData),
    config.NewMapSource("file", 50, fileData),
    config.NewMapSource("overrides", 90, overrideData),
)

Sources

MapSource

Provides configuration from a static map:

source := config.NewMapSource("file", 50, map[string]any{
    "server": map[string]any{
        "host": "0.0.0.0",
        "port": 3000,
    },
    "database": map[string]any{
        "dsn": "postgres://localhost/mydb",
    },
})

EnvSource

Reads from environment variables. Individual fields opt in via the env struct tag:

source := config.NewEnvSource("APP") // prefix: APP_
// "server.port" with env:"PORT" → checks APP_PORT

source := config.NewEnvSource("") // no prefix
// "server.port" with env:"PORT" → checks PORT

The EnvSource has priority 80 by default, so environment variables take precedence over most file-based sources.

Custom sources

Implement the Source interface:

type Source interface {
    Name() string
    Priority() int
    Load() (map[string]any, error)
}

Type conversion

Fields are automatically converted from source values:

Target type Conversion
string Direct assignment or fmt.Sprint
int, int64 Parsed from string or numeric value
float64 Parsed from string or numeric value
bool "true", "1", "yes" → true; others → false

DI integration

Config tokens

Create a DI token from a config definition:

token := config.Token(serverDef)
// token = inject.Named[ServerConfig]("config:server")

Config provider

Register a config definition as a DI provider:

import (
    "go.putnami.dev/app"
    "go.putnami.dev/config"
)

a := app.New("my-service")
a.Module.Provide(config.Provide(serverDef,
    config.NewMapSource("file", 50, yamlData),
))

The config is loaded when the DI container starts. Other providers can depend on the config type:

// Resolved automatically via DI
func NewHTTPServer(cfg ServerConfig) *fhttp.ServerPlugin {
    return fhttp.NewServerPlugin(fhttp.ServerConfig{Port: cfg.Port})
}

a.ProvideFunc(NewHTTPServer)

Nested configuration

type AppConfig struct {
    Server   ServerConfig   `json:"server"`
    Database DatabaseConfig `json:"database"`
    Features FeatureFlags   `json:"features"`
}

type DatabaseConfig struct {
    DSN         string `json:"dsn" env:"DATABASE_URL"`
    MaxConns    int    `json:"maxConns" default:"10"`
    MaxIdleTime string `json:"maxIdleTime" default:"30m"`
}

type FeatureFlags struct {
    NewUI      bool `json:"newUI" default:"false" env:"FEATURE_NEW_UI"`
    BetaAccess bool `json:"betaAccess" default:"false"`
}

var appDef = config.Config[AppConfig]("app")

Error codes

Code Description
config.source Source Load() failed
config.path Path does not resolve to an object
config.mapping Struct mapping failed (type conversion error)

Related guides

  • Dependency Injection — using config with DI
  • Plugins & Lifecycle — application setup

On this page

  • Configuration
  • Defining configuration
  • Struct tags
  • Loading configuration
  • Sources
  • MapSource
  • EnvSource
  • Custom sources
  • Type conversion
  • DI integration
  • Config tokens
  • Config provider
  • Nested configuration
  • Error codes
  • Related guides