Configuration
> Manage application configuration with environment variables and the structured velocity.Config struct in Velocity.
Velocity provides a simple yet powerful configuration system that reads from environment variables and a .env file, then exposes a single strongly-typed velocity.Config struct that every framework package consumes.
Quick Start
.env file, following the twelve-factor app methodology.import "github.com/velocitykode/velocity"
func main() {
// velocity.New loads ConfigFromEnv() by default:
// it reads .env (if present) and the process environment.
app, err := velocity.New()
if err != nil {
panic(err)
}
if err := app.Serve(); err != nil {
panic(err)
}
}# .env file
APP_ENV=production
APP_DEBUG=false
APP_PORT=4000
# Database
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=myapp
DB_USERNAME=root
DB_PASSWORD=secret
# Cache
CACHE_DRIVER=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379import "github.com/velocitykode/velocity"
func main() {
// Build the config explicitly, then override New's env-loaded default.
cfg := velocity.ConfigFromEnv()
cfg.Port = "8080"
app, err := velocity.New(velocity.WithConfig(cfg))
if err != nil {
panic(err)
}
_ = app
}How Configuration Loads
velocity.New() calls ConfigFromEnv() automatically, so a default app reads its
configuration from the environment with no extra wiring. The loader:
- Calls
godotenv.Load()to read a.envfile from the working directory (if present). A.envthat exists but fails to parse is logged as a warning, not a fatal error. - Reads every documented environment variable, applying defaults for unset values.
- Returns a
velocity.Configwhose typed sub-structs (DB,Cache,Queue,Storage,Session,Auth,CSRF,Crypto,Mail,View,Log) are consumed by the matching framework packages.
import "github.com/velocitykode/velocity"
func main() {
cfg := velocity.ConfigFromEnv()
// cfg.Env is the normalized (lowercased, trimmed) APP_ENV value.
if cfg.Env == "production" && cfg.Debug {
panic("APP_DEBUG must be false in production")
}
}You rarely need to call ConfigFromEnv() yourself. Use it only when you want to
inspect or mutate the config before passing it to velocity.New via WithConfig.
The Config Struct
velocity.Config is the single source of truth for application configuration. The
top-level fields and their backing environment variables are:
type Config struct {
// App
Env string // APP_ENV, empty when unset
Debug bool // APP_DEBUG, default false
Port string // APP_PORT, default "4000"
Key string // APP_KEY (used for crypto)
DB DBConfig // DB_*
Auth auth.Config // AUTH_*
Cache CacheConfig // CACHE_*, REDIS_*
Log log.LogConfig // LOG_*
Queue QueueConfig // QUEUE_*
Storage StorageConfig // STORAGE_*, FILESYSTEM_*, AWS_*
CSRF csrf.Config // CSRF_*
Session auth.SessionConfig // SESSION_*
View view.Config // VIEW_SSR_*
Crypto crypto.Config // CRYPTO_*
Mail mail.MailConfig // MAIL_*
// Server timeouts
ReadTimeout time.Duration // SERVER_READ_TIMEOUT, default 30s
WriteTimeout time.Duration // SERVER_WRITE_TIMEOUT, default 30s
IdleTimeout time.Duration // SERVER_IDLE_TIMEOUT, default 120s
ReadHeaderTimeout time.Duration // SERVER_READ_HEADER_TIMEOUT, default 10s
// FileRoot bounds Context.File / Context.Download / Context.SaveFile.
// Sourced from FILE_ROOT; defaults to the process working directory.
FileRoot string
}Database Configuration
type DBConfig struct {
Connection string // DB_CONNECTION: sqlite, postgres, mysql
Host string // DB_HOST, default "127.0.0.1"
Port string // DB_PORT, default per driver (mysql 3306, postgres 5432)
Database string // DB_DATABASE
Username string // DB_USERNAME
Password string // DB_PASSWORD
Charset string // DB_CHARSET
SSLMode string // DB_SSL_MODE (postgres)
TLS string // DB_MYSQL_TLS (true/false/skip-verify/preferred)
MaxIdleConns int // DB_MAX_IDLE_CONNS, default 10
MaxOpenConns int // DB_MAX_OPEN_CONNS, default 100
ConnMaxLifetime time.Duration // DB_CONN_MAX_LIFETIME (seconds), default 3600
LogQueries bool // DB_LOG_QUERIES
SlowThreshold time.Duration // DB_SLOW_QUERY_THRESHOLD
}Cache Configuration
type CacheConfig struct {
Driver string // CACHE_DRIVER: memory, file, redis, database (default "memory")
Prefix string // CACHE_PREFIX, default "velocity_cache"
Path string // CACHE_PATH (required when CACHE_DRIVER=file)
MemoryMaxEntries int // CACHE_MEMORY_MAX_ENTRIES (0 = 1,000,000, negative = unlimited)
MaxValueBytes int64 // CACHE_MAX_VALUE_BYTES (0 = unlimited)
RedisHost string // REDIS_HOST, default "127.0.0.1"
RedisPort int // REDIS_PORT, default 6379
RedisPassword string // REDIS_PASSWORD
RedisDatabase int // REDIS_DATABASE, default 0
RedisTLS bool // REDIS_TLS
}Queue Configuration
type QueueConfig struct {
Driver string // QUEUE_DRIVER: memory, redis, database (default "memory")
RedisHost string // QUEUE_REDIS_HOST, default "localhost"
RedisPort string // QUEUE_REDIS_PORT, default "6379"
RedisPassword string // QUEUE_REDIS_PASSWORD
RedisDB string // QUEUE_REDIS_DB, default "0"
RedisTLS bool // REDIS_TLS
SigningKey string // QUEUE_SIGNING_KEY: HMAC key for payload signing
Encrypt bool // QUEUE_ENCRYPT: encrypt job payloads at rest
}Storage Configuration
type StorageConfig struct {
Default string // STORAGE_DRIVER, default "local"
Disks map[string]DiskConfig // configured disks
}
type DiskConfig struct {
Driver string // "local", "s3", "memory"
Root string // root path for the local driver
URL string // base URL for file access
Visibility string // default visibility (public/private)
Bucket string // s3
Region string // s3
Key string // s3
Secret string // s3
MaxSize int64 // memory driver max bytes
}A local disk is always configured (root from FILESYSTEM_LOCAL_ROOT, default
./storage/app). An s3 disk is added automatically when AWS_BUCKET is set, reading
AWS_DEFAULT_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_URL.
Configuration Options
Pass velocity.Option functions to velocity.New to override the env-loaded config.
import (
"time"
"github.com/velocitykode/velocity"
)
app, err := velocity.New(
velocity.WithPort("8080"),
velocity.WithReadTimeout(15*time.Second),
velocity.WithWriteTimeout(15*time.Second),
velocity.WithIdleTimeout(60*time.Second),
)Available options include:
| Option | Effect |
|---|---|
WithConfig(cfg Config) | Replace the entire configuration. |
WithPort(port string) | Set the HTTP server port. |
WithReadTimeout(d time.Duration) | Set the HTTP read timeout. |
WithWriteTimeout(d time.Duration) | Set the HTTP write timeout. |
WithIdleTimeout(d time.Duration) | Set the HTTP idle timeout. |
WithProviders(providers ...app.ServiceProvider) | Append service providers. |
WithoutEvents() | Disable the event dispatcher entirely. |
WithFakeEvents(fake *events.FakeDispatcher) | Record dispatched events for assertions. |
WithSchedulerInProcess() | Run the scheduler loop in the same process as Serve(). |
Validating Configuration
velocity.New calls Config.Validate() before allocating any resources, so malformed
values (an invalid APP_PORT, a negative timeout, an unknown SESSION_SAME_SITE, a
file cache driver without CACHE_PATH) fail fast with a clear error. You can also call
it yourself:
import (
"errors"
"github.com/velocitykode/velocity"
)
cfg := velocity.ConfigFromEnv()
if err := cfg.Validate(); err != nil {
if errors.Is(err, velocity.ErrInvalidConfig) {
// configuration is structurally invalid
}
return err
}Validate() performs structural checks only (port is numeric, timeouts are
non-negative, SESSION_SAME_SITE / CSRF_SAME_SITE are one of strict|lax|none, and
each sub-config’s Validate() passes). It deliberately does not check driver names
against an allowlist: an unknown driver surfaces as a typed registry error when the
relevant subsystem resolves it. Every failure wraps the velocity.ErrInvalidConfig
sentinel so callers can branch with errors.Is.
Logging Configuration
The root config stores logging settings in Config.Log, a log.LogConfig:
import "github.com/velocitykode/velocity/log"
type LogConfig struct {
// Driver: "console", "file", "stack", "null", or a registered driver.
Driver string
// Config holds driver-specific options, e.g. "path" for file,
// "level" for any driver, "stack" with a []string of channel names.
Config map[string]any
}ConfigFromEnv() populates it from LOG_DRIVER (default console), LOG_PATH,
LOG_LEVEL (default debug), LOG_DAYS (default 14), and LOG_STACK (a
comma-separated list of channel names for the stack driver).
The log package also exposes a multi-channel LoggingConfig for applications that
define named channels:
import "github.com/velocitykode/velocity/log"
cfg := log.LoggingConfig{
Default: "stack",
Channels: map[string]log.ChannelConfig{
"daily": {Driver: "file", Level: "debug", Path: "./storage/logs", MaxAge: 14},
},
}
if channel, ok := cfg.GetChannel("daily"); ok {
_ = channel.Driver
}
if channel, ok := cfg.GetDefaultChannel(); ok {
_ = channel.Driver
}type ChannelConfig struct {
Driver string // file, console, syslog, null
Level string // debug, info, warn, error
Path string // file path (for file driver)
MaxAge int // max age in days
Options map[string]any // driver-specific options
}Environment Variables Reference
A representative .env covering the most common settings:
# Application
APP_ENV=production
APP_DEBUG=false
APP_PORT=4000
APP_KEY=base64:your-32-byte-base64-encoded-key
# Crypto
CRYPTO_KEY=base64:your-32-byte-base64-encoded-key
CRYPTO_CIPHER=AES-256-GCM
# Database
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=velocity
DB_USERNAME=root
DB_PASSWORD=
# Cache
CACHE_DRIVER=redis
CACHE_PREFIX=velocity_cache
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DATABASE=0
# Logging
LOG_DRIVER=file
LOG_PATH=./storage/logs
LOG_LEVEL=debug
LOG_DAYS=14
# Queue
QUEUE_DRIVER=memory
QUEUE_REDIS_HOST=localhost
QUEUE_REDIS_PORT=6379
# Mail
MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=noreply@example.com
MAIL_FROM_NAME="${APP_NAME}"
# Session
SESSION_NAME=velocity_session
SESSION_LIFETIME=120
SESSION_SECURE=true
SESSION_HTTP_ONLY=true
SESSION_SAME_SITE=lax
# Server timeouts (accepts Go duration syntax, e.g. 30s)
SERVER_READ_TIMEOUT=30s
SERVER_WRITE_TIMEOUT=30s
SERVER_IDLE_TIMEOUT=120sMAIL_DRIVER defaults to log
(captured, not sent), CRYPTO_CIPHER defaults to AES-256-GCM, CACHE_DRIVER and
QUEUE_DRIVER default to memory, SESSION_SECURE defaults to true (only the literal
false disables it), and DB_HOST / REDIS_HOST default to 127.0.0.1.Environment-Specific Configuration
Velocity classifies APP_ENV through the canonical reader, so security gates relax only
when you explicitly opt into a non-production profile. An unset APP_ENV is treated as
production (fail-secure).
Development
APP_ENV=development
APP_DEBUG=true
APP_PORT=4000
LOG_LEVEL=debug
LOG_DRIVER=console
CACHE_DRIVER=memory
QUEUE_DRIVER=memory
DB_HOST=127.0.0.1
DB_DATABASE=myapp_devProduction
APP_ENV=production
APP_DEBUG=false
LOG_LEVEL=info
LOG_DRIVER=file
CACHE_DRIVER=redis
QUEUE_DRIVER=redis
DB_HOST=db.example.com
DB_DATABASE=myapp_prodTesting
APP_ENV=testing
APP_DEBUG=true
LOG_LEVEL=error
LOG_DRIVER=null
CACHE_DRIVER=memory
QUEUE_DRIVER=memory
DB_CONNECTION=sqlite
DB_DATABASE=:memory:Security Best Practices
Sensitive Data
Never commit secrets to version control:
# .gitignore
.env
.env.local
.env.production
.env.*.localEnvironment Template
Provide a template for required variables:
# .env.example
APP_ENV=development
APP_DEBUG=true
APP_PORT=4000
APP_KEY=
# Database (required)
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
# Crypto (required for production)
CRYPTO_KEY=Testing Configuration
In tests, build a velocity.Config directly and pass it with WithConfig rather than
relying on the environment:
import (
"testing"
"github.com/velocitykode/velocity"
"github.com/velocitykode/velocity/log"
"github.com/velocitykode/velocity/mail"
)
func TestApp(t *testing.T) {
app, err := velocity.New(velocity.WithConfig(velocity.Config{
Env: "testing",
Debug: true,
Port: "0",
Cache: velocity.CacheConfig{Driver: "memory", Prefix: "test_cache"},
Log: log.LogConfig{Driver: "null", Config: make(map[string]any)},
Queue: velocity.QueueConfig{Driver: "memory"},
Mail: mail.MailConfig{Driver: "log"},
}))
if err != nil {
t.Fatalf("New() error: %v", err)
}
_ = app
}You can also point the loader at a dedicated env file before constructing the app:
func TestMain(m *testing.M) {
_ = godotenv.Load(".env.testing")
os.Exit(m.Run())
}Docker Integration
Docker Compose
# docker-compose.yml
services:
app:
build: .
ports:
- "4000:4000"
environment:
- APP_ENV=production
- APP_DEBUG=false
- APP_PORT=4000
- DB_HOST=db
- DB_DATABASE=myapp
- DB_USERNAME=root
- DB_PASSWORD=secret
- REDIS_HOST=redis
depends_on:
- db
- redis
db:
image: mysql:8
environment:
- MYSQL_ROOT_PASSWORD=secret
- MYSQL_DATABASE=myapp
redis:
image: redis:alpineBest Practices
- Let
New()load config: a plainvelocity.New()reads.envand the environment for you. - Override via options: use
WithConfig/WithPort/WithReadTimeoutinstead of mutating globals. - Validate early:
Config.Validate()runs insideNew(); call it yourself when building config manually. - Provide defaults: keep an
.env.exampledocumenting every variable. - Environment-specific files: use
.env,.env.testing, etc., for different profiles. - Security: never commit
.envfiles; setAPP_KEY/CRYPTO_KEYin production. - Fail-secure env: leave
APP_ENVunset only when you intend production-grade defaults.