Configuration

Nimbus uses environment-driven configuration inspired by Laravel conventions. Each subsystem has its own config file that reads from .env via godotenv.

How it works

When the application boots, bin/server.go calls config.Load() as the very first step. This:

  1. Reads the .env file using godotenv.Load().
  2. Calls each config loader (loadApp(), loadDatabase(), etc.).
  3. Populates typed Go structs that you access via config.App, config.Database, etc.
// config/config.go
package config

import "github.com/joho/godotenv"

func Load() {
    _ = godotenv.Load()
    loadApp()
    loadDatabase()
}

Environment variables

Create a .env file in your project root:

PORT=3333
APP_ENV=development
APP_NAME=myapp
DB_DRIVER=sqlite
DB_DSN=database.sqlite

The .env.example file (committed to version control) documents all available variables. Copy it to .env and fill in your values. Never commit .env itself.

Environment helpers

config/env.go provides helper functions to safely read environment variables with fallback values:

// config/env.go
func env(key, fallback string) string      // string with default
func envInt(key string, fallback int) int   // int with default
func envBool(key string, fallback bool) bool // bool with default

Config files

Each subsystem has its own file inside config/. Here is the pattern:

config/app.go

package config

type AppConfig struct {
    Name string
    Env  string
    Port int
}

var App AppConfig

func loadApp() {
    App = AppConfig{
        Name: env("APP_NAME", "nimbus"),
        Env:  env("APP_ENV", "development"),
        Port: envInt("PORT", 3333),
    }
}

config/database.go

package config

type DatabaseConfig struct {
    Driver string
    DSN    string
}

var Database DatabaseConfig

func loadDatabase() {
    Database = DatabaseConfig{
        Driver: env("DB_DRIVER", "sqlite"),
        DSN:    env("DB_DSN", "database.sqlite"),
    }
}

config/auth.go

package config

type AuthConfig struct {
    DefaultGuard string
    Stateless    StatelessTokenConfig
}

type StatelessTokenConfig struct {
    Driver    string
    Secret    string
    ExpiresIn time.Duration
}

var Auth AuthConfig

func loadAuth() {
    Auth = AuthConfig{
        DefaultGuard: env("AUTH_GUARD", "session"),
        Stateless: StatelessTokenConfig{
            Driver:    env("AUTH_TOKEN_DRIVER", "jwt"),
            Secret:    env("AUTH_TOKEN_SECRET", ""),
            ExpiresIn: envDuration("AUTH_TOKEN_EXPIRES_IN", 24 * time.Hour),
        },
    }
}

Accessing config

Import the config package and access the typed struct fields:

import "myapp/config"

fmt.Println(config.App.Name)        // "myapp"
fmt.Println(config.App.Port)        // 3333
fmt.Println(config.Database.Driver) // "sqlite"

Adding a new config

To add configuration for a new subsystem (e.g. mail):

  1. Create config/mail.go with a struct, a package-level var, and a loadMail() function.
  2. Add loadMail() to the Load() function in config/config.go.
  3. Add the environment variables to .env.example.
// config/mail.go
package config

type MailConfig struct {
    Driver   string
    SMTPHost string
    SMTPPort int
    From     string
}

var Mail MailConfig

func loadMail() {
    Mail = MailConfig{
        Driver:   env("MAIL_DRIVER", "smtp"),
        SMTPHost: env("SMTP_HOST", "localhost"),
        SMTPPort: envInt("SMTP_PORT", 587),
        From:     env("MAIL_FROM", "noreply@example.com"),
    }
}

Environment validation

Validate that required environment variables are set at boot time. The app panics with a clear message if any required variable is missing.

import "github.com/CodeSyncr/nimbus/config"

func init() {
    config.ValidateEnv(
        config.Required("APP_KEY"),
        config.Required("DB_DSN"),
        config.Required("REDIS_URL"),
    )
}

// If DB_DSN is not set:
// panic: "environment validation failed: DB_DSN is required"

Use EnvRule for custom validation beyond presence checks:

config.ValidateEnv(
    config.Required("PORT"),
    config.EnvRule{
        Key:     "APP_ENV",
        Message: "APP_ENV must be development, staging, or production",
        Validate: func(val string) bool {
            return val == "development" || val == "staging" || val == "production"
        },
    },
)