config

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 30, 2026 License: AGPL-3.0 Imports: 11 Imported by: 0

Documentation

Overview

Package config loads and validates runtime configuration from the environment, one sub-config at a time. Configuration is read exclusively from environment variables so secrets are never committed; an optional .env file is honored in development only, via LoadDotenv.

Each sub-config has its own loader and its own validator: LoadServer, LoadDB, LoadSession, and so on, each paired with a Validate method (or a free function taking the sub-config, for the two that also need the deployment environment). Every loader that can fail returns the parsed value alongside an aggregated slice of errors rather than stopping at the first problem, and every fallback is still applied even when a value fails to parse, so the caller always has something usable to work with while it reports every problem found in one pass.

This package holds only sub-configs shared across more than one application. An application's own root configuration struct — and any config that is specific to its own domain — lives in that application's own module and is composed from the loaders here.

Index

Constants

View Source
const (
	EnvDev  = "dev"
	EnvTest = "test"
	EnvProd = "prod"
)

Deployment environments. A caller's own AppEnv value is expected to be one of these three.

View Source
const DefaultHSTSMaxAge = 180 * 24 * time.Hour

DefaultHSTSMaxAge is the HSTS max-age applied when HSTS is enabled without an explicit HSTS_MAX_AGE (~180 days) — long enough to be effective, short of the 1-year preload-list minimum so it stays low-risk.

View Source
const DevEncryptionKey = "00000000000000000000000000000000000000000000000000000000deadbeef"

DevEncryptionKey is a known, insecure 32-byte (64-hex) default used only in development so a caller starts without configuration. It is rejected in prod (see Validate), forcing a real key (generated with `openssl rand -hex 32`) there.

View Source
const DevSessionSecret = "dev-only-insecure-session-secret-change-me"

DevSessionSecret is a known, insecure default used only in development. It satisfies the length check in dev but is rejected in prod (see Validate), forcing a real secret in production.

Variables

This section is empty.

Functions

func AppEnv

func AppEnv() string

AppEnv returns the deployment environment from APP_ENV, defaulting to EnvDev.

func Bool

func Bool(key string, fallback bool) (bool, error)

Bool parses a boolean environment variable via strconv.ParseBool, which accepts 1/t/T/TRUE/true/True and 0/f/F/FALSE/false/False (not an arbitrary mixed case like "tRuE"), returning fallback when unset or empty and an error when present but invalid.

func Duration

func Duration(key string, fallback time.Duration) (time.Duration, error)

Duration parses a duration environment variable (e.g. "30s", "5m"), returning fallback when unset or empty and an error when present but invalid.

func Int32

func Int32(key string, fallback int32) (int32, error)

Int32 parses an int32 environment variable, returning fallback when unset or empty and an error when present but not a valid integer.

func Int64

func Int64(key string, fallback int64) (int64, error)

Int64 parses an int64 environment variable, returning fallback when unset or empty and an error when present but not a valid integer.

func LoadDotenv

func LoadDotenv() []error

LoadDotenv loads an optional .env file from the current working directory into the process environment. godotenv.Load never overwrites a variable that is already set, so the real environment always takes precedence over .env. A missing .env file is expected and not an error; a permission or I/O error, or a malformed file, is returned so a caller that intended the file to be read finds out at startup rather than silently running without it.

Loading .env at all is a development convenience, not a runtime property of this function: a caller wires it in only for its dev environment (e.g. `if AppEnv() == EnvDev { errs = append(errs, LoadDotenv()...) }`), and should re-read AppEnv afterward, since .env may itself set APP_ENV.

func ServerAddrFromEnv

func ServerAddrFromEnv() string

ServerAddrFromEnv returns the HTTP listen address derived from PORT, using the same parsing LoadServer uses (a leading colon is tolerated), without requiring a full, validated ServerConfig. It backs any first-run setup mode that must serve HTTP before the rest of configuration (notably a database DSN) exists and so cannot call the full loader chain.

func String

func String(key, fallback string) string

String returns the value of the environment variable named key, or fallback when the variable is unset or empty.

func ValidateAppEnv

func ValidateAppEnv(env string) []error

ValidateAppEnv reports whether env is one of EnvDev, EnvTest, or EnvProd.

Types

type CacheConfig

type CacheConfig struct {
	// Dir is the directory the cache opens its store under.
	Dir string
}

CacheConfig configures an on-disk cache for data that is derived, re-computable, or externally sourced. Mirrors the same safe-local-default-in-every-environment shape most path-valued sub-configs in this package share: Validate only checks Dir is non-empty, not that it is absolute. A relative CACHE_DIR resolves against the caller's working directory at the time it was launched, which can vary by how the process is started (systemd unit, an ad hoc shell, a container WORKDIR); best-effort local storage is intentional, not a startup invariant this package enforces. Production deployments should set CACHE_DIR to an absolute path so its location does not depend on how the process happens to be launched.

func LoadCache

func LoadCache() CacheConfig

LoadCache reads CacheConfig from CACHE_DIR, defaulting to devCacheDir.

func (CacheConfig) Validate

func (c CacheConfig) Validate() []error

Validate returns every CacheConfig problem found, so callers can surface them together.

type CryptoConfig

type CryptoConfig struct {
	EncryptionKey string
}

CryptoConfig holds an at-rest encryption key for protecting stored secrets. EncryptionKey is a 64-character hex string (32 bytes), produced by `openssl rand -hex 32`. When set in any environment it must be valid; Validate additionally requires it in prod. Key decodes and validates it.

func LoadCrypto

func LoadCrypto() CryptoConfig

LoadCrypto reads CryptoConfig from ENCRYPTION_KEY, falling back to DevEncryptionKey when unset.

func (CryptoConfig) Key

func (c CryptoConfig) Key() ([]byte, error)

Key decodes the configured hex EncryptionKey into its 32 raw bytes, returning an error when it is unset or not exactly 32 bytes of hex.

func (CryptoConfig) Validate

func (c CryptoConfig) Validate(env string) []error

Validate returns every CryptoConfig problem found, so callers can surface them together. env additionally gates the prod-only requirement that a key be set and non-default: a malformed or default key must fail fast at startup rather than at the first encrypt.

type DBConfig

type DBConfig struct {
	// DSN is the Postgres connection string. LoadDB reads it verbatim, with
	// no environment-specific default: a caller that wants a development
	// convenience DSN applies its own fallback before calling Validate.
	DSN string
	// MaxConns caps the connection pool. Zero means "let the pool decide".
	MaxConns int32
	// ConnTimeout bounds the initial connectivity check at startup.
	ConnTimeout time.Duration
	// Provider selects the database backend (default DBProviderPostgres).
	// The Postgres path is byte-for-byte identical to before this field
	// existed.
	Provider DBProvider
	// PoolMode declares the Supabase pooler endpoint the DSN targets;
	// consulted only when Provider is DBProviderSupabase (default
	// DBPoolModeSession).
	PoolMode DBPoolMode
	// SSLRootCert is an optional path to a CA bundle. When set, the
	// connection upgrades to sslmode=verify-full and verifies the server
	// certificate against this CA.
	SSLRootCert string
	// MigrateDSN is an optional override (MIGRATE_DATABASE_URL) for the
	// connection a migration tool uses; empty means "use DSN". This lets an
	// operator point migrations at a Supabase direct/session connection
	// (port 5432) so DDL and version bookkeeping run on a session
	// connection while the app server uses the transaction pooler (port
	// 6543).
	MigrateDSN string
}

DBConfig configures Postgres connectivity.

func LoadDB

func LoadDB() (DBConfig, []error)

LoadDB reads DBConfig from DATABASE_URL, DB_MAX_CONNS, DB_CONNECT_TIMEOUT, DB_PROVIDER, DB_POOL_MODE, DB_SSL_ROOT_CERT, and MIGRATE_DATABASE_URL. DATABASE_URL is read verbatim: LoadDB applies no development default, so a caller that wants one must apply it before calling Validate — see DSN's own doc for why.

func (DBConfig) Validate

func (d DBConfig) Validate() []error

Validate returns every DBConfig problem found, so callers can surface them together.

The empty-DSN check is load-bearing, not cosmetic: pgxpool.ParseConfig("") does not error on an empty DSN — it resolves to libpq defaults (host=/tmp, port=5432, database="", user=$USER) and silently attempts a local Unix-socket connection as the invoking OS user. Since LoadDB applies no development fallback, this check is the only thing standing between an unset DATABASE_URL and a connection to some unrelated local Postgres.

type DBPoolMode

type DBPoolMode string

DBPoolMode declares which Supabase pooler endpoint the DSN targets. It is consulted only when Provider is DBProviderSupabase.

const (
	// DBPoolModeSession targets the session pooler or a direct connection,
	// where a backend connection is not multiplexed mid-transaction, so
	// pgx's default cached server-side prepared statements are safe.
	DBPoolModeSession DBPoolMode = "session"
	// DBPoolModeTransaction targets the transaction pooler (Supavisor port
	// 6543), which multiplexes a backend connection per transaction and is
	// incompatible with cached server-side prepared statements.
	DBPoolModeTransaction DBPoolMode = "transaction"
)

type DBProvider

type DBProvider string

DBProvider selects the database backend. Both are Postgres; the provider only changes connectivity (TLS and pooler-safe statement handling), never the schema or queries.

const (
	// DBProviderPostgres is the default self-hosted Postgres backend.
	DBProviderPostgres DBProvider = "postgres"
	// DBProviderSupabase targets Supabase: Postgres reached through the
	// Supavisor connection pooler, requiring TLS and pooler-safe statement
	// handling.
	DBProviderSupabase DBProvider = "supabase"
)

type EmailConfig

type EmailConfig struct {
	// Enabled turns the email channel on.
	Enabled bool
	// FromAddress is the verified sending address messages are sent from.
	// Required when Enabled.
	FromAddress string
	// Region is passed to every email API request. Required when Enabled.
	Region string
	// AccessKeyID / SecretAccessKey are optional static credentials. When
	// BOTH are blank, the AWS SDK's default credential chain supplies
	// credentials instead — mirrors SMSConfig's identical field pair.
	AccessKeyID     string
	SecretAccessKey string
}

EmailConfig configures an optional email notification channel. It is only consulted when Enabled is true; every other field is otherwise ignored (and unvalidated), mirroring SMSConfig's identical enabled-gates-required-fields pattern — a deployment with email disabled (the default) never has to set any of it.

func LoadEmail

func LoadEmail() (EmailConfig, []error)

LoadEmail reads EmailConfig from NOTIFY_EMAIL_ENABLED, SES_FROM_ADDRESS, SES_REGION, SES_ACCESS_KEY_ID, and SES_SECRET_ACCESS_KEY.

func (EmailConfig) Validate

func (e EmailConfig) Validate() []error

Validate returns every EmailConfig problem found, so callers can surface them together. Every check below runs only when Enabled is true, so a deployment with email disabled never fails validation on a stray or partial SES_* value it will never use.

type HSTSConfig

type HSTSConfig struct {
	// Enabled turns the Strict-Transport-Security header on.
	Enabled bool
	// MaxAge is the max-age directive (emitted as whole seconds). It is
	// only meaningful when MaxAgeSet is true; see EffectiveMaxAge.
	MaxAge time.Duration
	// MaxAgeSet records whether HSTS_MAX_AGE was explicitly provided. It
	// lets an explicit max-age=0 (which clears a previously-sent HSTS
	// policy in browsers) be distinguished from "unset" (apply
	// DefaultHSTSMaxAge). A negative explicit value is invalid.
	MaxAgeSet bool
	// IncludeSubdomains adds the includeSubDomains directive.
	IncludeSubdomains bool
	// Preload adds the preload directive (requires includeSubDomains +
	// max-age >= 1y).
	Preload bool
}

HSTSConfig configures the HTTP Strict-Transport-Security response header. HSTS is opt-in because it is sticky and hard to undo, so it should only be enabled on a stable HTTPS hostname, and emitted only over HTTPS.

func LoadHSTS

func LoadHSTS() (HSTSConfig, []error)

LoadHSTS reads HSTSConfig from HSTS_ENABLED, HSTS_MAX_AGE, HSTS_INCLUDE_SUBDOMAINS, and HSTS_PRELOAD.

func (HSTSConfig) EffectiveMaxAge

func (h HSTSConfig) EffectiveMaxAge() time.Duration

EffectiveMaxAge returns the max-age the header should carry: the explicit value when HSTS_MAX_AGE was set (including 0 to clear HSTS), otherwise DefaultHSTSMaxAge.

func (HSTSConfig) Validate

func (h HSTSConfig) Validate() []error

Validate returns every HSTSConfig problem found, so callers can surface them together.

type S3Config

type S3Config struct {
	// Endpoint is the S3-compatible API's base URL. Blank targets real AWS
	// S3 (the SDK's regional default endpoint); a custom endpoint (MinIO,
	// Garage, Cloudflare R2, ...) is a first-class target, not an
	// afterthought.
	Endpoint string
	// Region is passed to every S3 request. AWS S3 requires a real region;
	// most S3-compatible servers (MinIO, Garage) accept any non-empty value
	// since they do not partition by region.
	Region string
	// Bucket is the bucket objects are stored under.
	Bucket string
	// AccessKeyID / SecretAccessKey are optional static credentials. When
	// BOTH are blank, the AWS SDK's default credential chain (environment,
	// shared config/credentials file, EC2/ECS instance role, etc.) supplies
	// credentials instead, so a deployment that already provisions
	// credentials another way (e.g. an IAM role) never needs to duplicate
	// them here. Validate enforces both-or-neither, mirroring TLSConfig's
	// CertFile/KeyFile pairing.
	AccessKeyID     string
	SecretAccessKey string
	// UsePathStyle forces path-style bucket addressing
	// (https://endpoint/bucket/key instead of https://bucket.endpoint/key).
	// MinIO and most self-hosted S3-compatible servers require this; real
	// AWS S3 does not.
	UsePathStyle bool
	// PresignTTL is how long a presigned GET URL stays valid when the
	// caller passes a non-positive ttl of its own — the applied default.
	// Kept short: a presigned URL is a bearer credential for as long as it
	// is valid, so the default favors a tight window over convenience.
	PresignTTL time.Duration
}

S3Config configures an optional S3-compatible object storage backend. LoadS3 always parses every field; whether it applies to a given deployment is the caller's own decision (e.g. a storage-backend selector that only exists in the caller's own domain config), so Validate is caller-gated rather than self-gating on an Enabled field the way SMSConfig and EmailConfig do — see Validate's own doc.

func LoadS3

func LoadS3() (S3Config, []error)

LoadS3 reads S3Config from S3_ENDPOINT, S3_REGION, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_USE_PATH_STYLE, and S3_PRESIGN_TTL. It always parses every field, regardless of whether the caller's deployment actually selected an S3 backend; the caller decides whether the returned errors and Validate's findings count — see S3Config's own doc.

func (S3Config) Validate

func (s S3Config) Validate() []error

Validate returns every S3Config problem found, so callers can surface them together. Unlike SMSConfig.Validate and EmailConfig.Validate, which self-gate on their own Enabled field, Validate here is caller-gated: it always checks the required fields, and it is the caller's responsibility to call it (and to append LoadS3's own errors) only when its own storage-backend selection actually opted into S3.

type SMSConfig

type SMSConfig struct {
	// Enabled turns the SMS channel on.
	Enabled bool
	// OriginationIdentity is the verified sending number (or its ARN, or a
	// pool id/ARN) messages are sent from. Required when Enabled.
	OriginationIdentity string
	// Region is passed to every SMS API request. Required when Enabled.
	Region string
	// AccessKeyID / SecretAccessKey are optional static credentials. When
	// BOTH are blank, the AWS SDK's default credential chain supplies
	// credentials instead — mirrors S3Config's identical field pair.
	AccessKeyID     string
	SecretAccessKey string
	// RetryMaxAttempts caps the AWS SDK's own built-in retryer.
	RetryMaxAttempts int
}

SMSConfig configures an optional SMS notification channel. It is only consulted when Enabled is true; every other field is otherwise ignored (and unvalidated) — a deployment with SMS disabled (the default) never has to set any of it.

func LoadSMS

func LoadSMS() (SMSConfig, []error)

LoadSMS reads SMSConfig from NOTIFY_SMS_ENABLED, SMS_ORIGINATION_IDENTITY, SMS_REGION, SMS_ACCESS_KEY_ID, SMS_SECRET_ACCESS_KEY, and SMS_RETRY_MAX_ATTEMPTS. NOTIFY_SMS_ENABLED gates every other SMS_* setting: SMS_RETRY_MAX_ATTEMPTS is parsed (and its parse error collected) only when SMS is enabled, so a deployment with SMS disabled (the default) never fails to load on a stray or malformed value it will never use.

func (SMSConfig) Validate

func (s SMSConfig) Validate() []error

Validate returns every SMSConfig problem found, so callers can surface them together. Every check below runs only when Enabled is true, so a deployment with SMS disabled never fails validation on a stray or partial SMS_* value it will never use.

type SchemaConfig added in v0.2.0

type SchemaConfig struct {
	// Identity is the shared schema nestcore's identity package owns:
	// household, member, credentials, and sessions.
	Identity string
	// Nestova is Nestova's own schema for its non-identity tables.
	Nestova string
	// Nestorage is Nestorage's own schema for its non-identity tables.
	Nestorage string
}

SchemaConfig names the three Postgres schemas the shared "nest" database holds: Identity (owned by nestcore, shared by every app), and one schema per app for its own tables. Every field defaults to the canonical install's name, so a single-database, single-app deployment needs none of this section's environment variables set.

Identity's default ("identity") is the only value nestcore/identity/migrate actually consumes today — that package's schema name is still a compile-time constant, not wired to this config. An operator who overrides DB_SCHEMA_IDENTITY changes what this config reports, but not what identity/migrate migrates against, until a future ticket parameterizes it.

func LoadSchemas added in v0.2.0

func LoadSchemas() SchemaConfig

LoadSchemas reads SchemaConfig from DB_SCHEMA_IDENTITY, DB_SCHEMA_NESTOVA, and DB_SCHEMA_NESTORAGE, defaulting to identity, nestova, and nestorage.

func (SchemaConfig) Validate added in v0.2.0

func (s SchemaConfig) Validate() []error

Validate returns every SchemaConfig problem found, so callers can surface them together: each name must be a valid, unquoted Postgres identifier of no more than 63 bytes, and the three names must be pairwise distinct.

type ServerConfig

type ServerConfig struct {
	// Addr is the TCP address the HTTP server listens on, e.g. ":8080".
	Addr string
	// TrustedProxies is the raw, comma-separated CIDR list (from
	// TRUSTED_PROXIES) of reverse-proxy source networks whose
	// X-Forwarded-* headers are trusted. It is validated at Validate; call
	// TrustedProxyPrefixes for the parsed form. Forwarded headers should be
	// honored only when the immediate peer falls inside one of these
	// networks, so an external client cannot spoof a secure context. An
	// empty value trusts no proxy.
	TrustedProxies string
	// RequestTimeout bounds how long the server allows a single request to
	// take end to end — both the connection-level ReadTimeout/WriteTimeout
	// and (minus a small margin) the per-request context deadline applied
	// to every handler.
	RequestTimeout time.Duration
	// PublicBaseURL is the externally-reachable origin (scheme + host, no
	// trailing slash, e.g. "https://app.tailxxxx.ts.net") a caller builds
	// absolute links against. Empty (the default) means "derive it from the
	// incoming request" instead.
	//
	// A caller that pins a fixed Relying Party ID for WebAuthn, or anything
	// else that cannot tolerate a per-request derived origin, additionally
	// REQUIRES this to be set: changing PublicBaseURL's host after such an
	// identity has been registered against it breaks every credential
	// registered under the old value, since the value is baked in at
	// registration time, not re-derived per request.
	PublicBaseURL string
}

ServerConfig configures the HTTP listener.

func LoadServer

func LoadServer() (ServerConfig, []error)

LoadServer reads ServerConfig from PORT, TRUSTED_PROXIES, SERVER_REQUEST_TIMEOUT, and PUBLIC_BASE_URL.

func (ServerConfig) TrustedProxyPrefixes

func (s ServerConfig) TrustedProxyPrefixes() []netip.Prefix

TrustedProxyPrefixes parses TrustedProxies into netip prefixes for a forwarded-headers middleware. TrustedProxies is validated during Validate, so any malformed entry would already have failed startup; this drops such entries defensively and never returns an error.

func (ServerConfig) Validate

func (s ServerConfig) Validate() []error

Validate returns every ServerConfig problem found, so callers can surface them together.

type SessionConfig

type SessionConfig struct {
	// Secret is a high-entropy key reserved for cryptographic signing; it
	// must be at least minSecretLen bytes.
	Secret string
	// Secure marks the session cookie Secure (HTTPS-only). It is resolved
	// from SESSION_COOKIE_SECURE: auto (the default) keeps Secure only when
	// the deployment environment is EnvProd, while true/false force it —
	// letting a TLS-terminated deployment emit Secure cookies regardless of
	// environment.
	Secure bool
	// Lifetime is the maximum session duration.
	Lifetime time.Duration
}

SessionConfig configures sessions.

func LoadSession

func LoadSession(env string) (SessionConfig, []error)

LoadSession reads SessionConfig from SESSION_SECRET, SESSION_LIFETIME, and SESSION_COOKIE_SECURE. env resolves SESSION_COOKIE_SECURE's auto setting against the deployment environment (EnvDev, EnvTest, or EnvProd).

func (SessionConfig) Validate

func (s SessionConfig) Validate(env string) []error

Validate returns every SessionConfig problem found, so callers can surface them together. env additionally gates the prod-only rejection of the development default secret.

type TLSConfig

type TLSConfig struct {
	// CertFile is the path to the PEM server certificate (chain).
	CertFile string
	// KeyFile is the path to the PEM private key for CertFile.
	KeyFile string
}

TLSConfig configures optional app-terminated TLS. When both files are set, the caller's server listens with TLS (ListenAndServeTLS); otherwise it serves plain HTTP and relies on a reverse proxy for TLS. Both-or-neither is enforced by Validate.

func LoadTLS

func LoadTLS() TLSConfig

LoadTLS reads TLSConfig from TLS_CERT_FILE and TLS_KEY_FILE.

func (TLSConfig) Enabled

func (t TLSConfig) Enabled() bool

Enabled reports whether app-terminated TLS is configured (both files present).

func (TLSConfig) Validate

func (t TLSConfig) Validate() []error

Validate returns every TLSConfig problem found, so callers can surface them together. This is the same both-or-neither shape as validateCredentialPair, but is not built on it: unlike S3/SMS/Email's credential pairs, an unset TLS pair has no fallback credential chain to mention, so the message differs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL