Help us shape the future of Go! Take the 2026 Go Developer Survey

config

package
v1.4.14 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package config — user-level telemetry preference management.

Telemetry preference is stored in ~/.nself/config.toml under [telemetry]. This file does NOT ship a telemetry client at v1.0.9. The preference is persisted now so the v1.1.0 client can respect choices made today.

Env var NSELF_TELEMETRY_OPT_OUT=1 always takes precedence over the file.

Index

Constants

View Source
const EnvBackupHeartbeatRemote = "NSELF_BACKUP_HEARTBEAT_REMOTE"

EnvBackupHeartbeatRemote names the variable that selects the heartbeat remote.

View Source
const LegacyEnvOrderVar = "NSELF_LEGACY_ENV_ORDER"

LegacyEnvOrderVar is the escape-hatch environment variable that restores the pre-CLI-R18 cascade order for exactly one minor version after the reorder ships. Every use prints a warning naming the variable and the version it will stop being honored in — see warnLegacyEnvOrder in loader.go.

View Source
const URLPasswordEnvSuffix = "_URLENC"

URLPasswordEnvSuffix is appended to a password variable name to name its URL-encoded twin (POSTGRES_PASSWORD -> POSTGRES_PASSWORD_URLENC).

Variables

View Source
var AllCascadeFilenames = []string{
	".env",
	".env.dev",
	".env.staging",
	".env.prod",
	".env.secrets",
	".env.local",
	".env.ai",
}

AllCascadeFilenames is the fixed superset of every filename that appears in either cascade order, across every environment name. Used by the migration shim to read every candidate file once regardless of which order or env is being analyzed.

Functions

func BuildJWTSecret

func BuildJWTSecret(cfg *Config) (string, error)

BuildJWTSecret constructs the HASURA_GRAPHQL_JWT_SECRET JSON string. If the environment variable HASURA_GRAPHQL_JWT_SECRET is already set, it is returned directly. Otherwise the secret is constructed from cfg.Hasura.JWTKey and cfg.Hasura.JWTType, which are the single source of truth also used by buildAuthService for AUTH_JWT_SECRET/AUTH_JWT_TYPE (JWT-ALGO-01) — this guarantees Hasura verifies tokens with the exact algorithm+key auth signed them with. A missing JWTKey is auto-generated (matching ApplyDefaults' HS256-default behavior); an empty JWTType defaults to HS256 for the same reason: no RSA keypair generator exists in this codebase, so RS256 can only be produced correctly when the caller already went through ApplyDefaults (which rejects RS256 without a supplied key).

func BuildServiceURL

func BuildServiceURL(subdomain, baseDomain string) string

BuildServiceURL constructs a full HTTPS URL for a service subdomain against the given baseDomain. If baseDomain already starts with "subdomain.", that prefix is stripped first to avoid double-prefixing (e.g. "auth.auth.example.com").

Examples:

BuildServiceURL("auth", "auth.example.com") → "https://auth.example.com"
BuildServiceURL("auth", "example.com")      → "https://auth.example.com"

func DefaultFor

func DefaultFor(key string) string

DefaultFor returns the documented default value for an env var, as a human-readable string. Returns "" when the var has no static default (e.g. generated secrets, or dynamic per-env defaults).

func DetectMonorepoRoot

func DetectMonorepoRoot(dir string) string

DetectMonorepoRoot checks whether dir is a monorepo root by looking for an nself backend sub-directory containing a .env file. It probes, in order:

  • <dir>/backend/.env
  • <dir>/.backend/.env

Returns the full path to the backend directory if found, or "" if dir does not appear to be a monorepo root. The function never returns an error: a missing or unreadable path is treated as "not found".

func EnvCascadeOrder added in v1.3.0

func EnvCascadeOrder(envName string, legacy bool) []string

EnvCascadeOrder returns the ordered list of filenames (lowest precedence first; later entries win) consulted to resolve the given environment name.

Canonical order (legacy=false), approved 2026-08-23 GATE B:

.env → .env.{dev|staging|prod} → .env.secrets → .env.local

.env is the shared, committed base. Exactly one of .env.dev/.env.staging/ .env.prod loads, matching envName ("local" loads .env.dev). A custom envName (qa, live, ...) loads .env.<envName> and never .env.dev (P7-DEPL-12). .env.secrets never ships in git. .env.local is the personal override and always wins. .env.ai no longer exists as a cascade layer — its content is folded into .env.secrets at init/upgrade (see internal/setup/envai.go and internal/migrate/env_order.go).

Legacy order (legacy=true), restored only via NSELF_LEGACY_ENV_ORDER for exactly one minor version:

.env.dev → .env.{staging|prod} → .env.secrets → .env.local → .env → .env.ai

.env.dev always loaded as a base layer regardless of envName (a quirk the canonical order removes), with .env.staging/.env.prod layered on top only for those two envs, and bare .env / .env.ai winning last.

func FindDependsCycle added in v1.4.13

func FindDependsCycle(edges map[string][]string, roots []string) []string

FindDependsCycle returns the first dependency cycle reachable from roots in edges (name -> dependencies) as "a -> b -> a", or nil. Each node is visited once (depth-first, memoised), so a diamond is not a cycle and a cycle among services reached through a root is reported even when the root is not on it. Shared by compose generation (custom services only) and build post-validation (the whole compose set).

func FindNSelfRoot

func FindNSelfRoot(startDir string) (string, error)

FindNSelfRoot walks up from startDir looking for a nself project root. It checks, at each directory level:

  1. startDir/.backend/.env → returns startDir/.backend (monorepo case)
  2. startDir/.env → returns startDir (already in backend dir)

Walking stops at $HOME, at /, or after 10 levels — whichever comes first. Returns an error if no project root is found.

func IsSecretKey added in v1.4.13

func IsSecretKey(key string) bool

IsSecretKey reports whether key names a secret value.

func KnownEnvVars

func KnownEnvVars() []string

KnownEnvVars returns the list of environment variable names that the CLI loader reads. Used by commands like `nself config list` to display known vars with their current values.

func LegacyOrderActive added in v1.3.0

func LegacyOrderActive() bool

LegacyOrderActive reports whether the NSELF_LEGACY_ENV_ORDER escape hatch is set to a truthy value in the current process environment.

func MaskURLCredentials added in v1.4.14

func MaskURLCredentials(v string) string

MaskURLCredentials masks the credentials of a value that holds a URL. For `scheme://` followed later by `@`, everything between `://` and the LAST `@` becomes `****` (user and password), so a password that contains `@`, `/`, `:`, `+` or spaces never leaks and the rule does not depend on net/url accepting the string. Query, matrix (`;`) and fragment (`#`) parameters whose name is a credential (isCredentialName, any case, spaces around the name allowed) are masked too; the name is percent-decoded first, so `access%5Ftoken=`, `access_token%3D` and `#access_token=` are caught, and a parameter nested in another one's value is found (D-0430). A value whose percent-decoded form holds a credential parameter (an encoded redirect such as https%3A%2F%2Fx%2F%3Ftoken%3D...) is masked whole. A credential value runs to the next `&` or whitespace (a credential holding a raw `&` or space keeps the part after it); a value masked whole for an encoded credential runs to the next `&`, `;`, `#` or whitespace. Time is linear in len(v). A value without `://` is returned unchanged. The rule over-masks a path that holds an `@`; that is the safe side.

func QuoteComposeEnvValue added in v1.4.14

func QuoteComposeEnvValue(v string) string

QuoteComposeEnvValue renders v as the value half of a docker compose --env-file line so that compose's dotenv parser reads back exactly v (D-0333). Unquoted compose dotenv values are trimmed, cut at " #", expanded on "$" and opened as quoted strings by a leading quote, so any value holding whitespace, '#', '$', a quote or a backtick is quoted. A value whose only special character is a backslash stays unquoted: every compose reads unquoted backslashes verbatim, while compose before 2.20 rejects the whole file when a quoted value ends in a backslash.

  • Single quotes when v has no ' and no \: compose reads the bytes between them verbatim.
  • Double quotes otherwise: \ → \\, " → \", $ → $$. Compose unescapes \\ and \" and turns $$ into a literal $. Escaping ' inside single quotes is not portable: compose before 2.24 (compose-go v1 dotenv) keeps the backslash of \', and treats any backslash before the closing quote as an escape.

Verified byte for byte with `docker compose config` on 2.20.3, 2.24.0, 2.27.0 and 5.6.0. Older compose (2.17 and before) still cannot read a quoted value with a backslash before its closing quote. Unlike QuoteEnvValue it is not idempotent: a value that is already wrapped in quotes is a value with quotes in it and is quoted again.

Example:

QuoteComposeEnvValue("plain-value") → plain-value
QuoteComposeEnvValue("$2a$10$abc")  → '$2a$10$abc'
QuoteComposeEnvValue("it's $5")     → "it's $$5"
QuoteComposeEnvValue(`a\b`)         → a\b
QuoteComposeEnvValue(`a\b"c`)       → "a\\b\"c"

func QuoteEnvValue added in v1.1.1

func QuoteEnvValue(v string) string

QuoteEnvValue wraps v in single quotes when v is a JSON object or array value. Non-JSON values are returned unchanged. Single-quoted values are also returned unchanged (idempotent).

Example:

QuoteEnvValue(`{"type":"HS256","key":"abc"}`) → `'{"type":"HS256","key":"abc"}'`
QuoteEnvValue("plain-value")                  → "plain-value"
QuoteEnvValue(`'already-quoted'`)             → `'already-quoted'`

func ResolveEnv added in v1.4.4

func ResolveEnv(projectDir string) (value string, source string)

ResolveEnv determines which environment name selects the .env.{ENV} cascade layer, and where that value came from (surfaced via slog and by `nself env explain`).

Order: the process environment wins outright — an operator or CI that already exported ENV=... is always honored. Otherwise the ENV= key inside the project's own .env file (the shared, committed base present in every clone, including a bare production checkout with no shell env set) is read directly. Otherwise "dev".

The .env fallback is read with a single targeted key lookup rather than running the full cascade, because the full cascade's file selection itself depends on the answer.

func RouteToFQDN

func RouteToFQDN(route, baseDomain string) (string, error)

RouteToFQDN constructs a valid FQDN from a route segment and base domain. It trims whitespace, removes leading/trailing dots and slashes from both inputs, lowercases both, and returns "route.domain". Returns error if either input is empty after normalization.

func SanitizeDBName added in v1.3.6

func SanitizeDBName(input string) (string, error)

SanitizeDBName converts a Docker-compatible project name (as produced by SanitizeName: lowercase alphanumeric + hyphens) into a value that satisfies the stricter PostgreSQL unquoted-identifier syntax enforced by database.SanitizeIdentifier (start with a letter or underscore; only letters, digits, and underscores after that; max 63 bytes).

PROJECT_NAME and POSTGRES_DB have different validity domains — Docker container/network names allow hyphens and a leading digit, SQL identifiers allow neither — so the raw project name (e.g. from a hyphenated directory like "rls-pentest-project") cannot be reused verbatim as the database name. This normalizes it instead of rejecting it, so init always produces a working POSTGRES_DB regardless of the source directory name.

Returns ("", err) only if nothing usable remains after normalization (e.g. an input made entirely of characters outside [a-z0-9_-. ]).

func SanitizeDomain

func SanitizeDomain(input string) (string, error)

SanitizeDomain lowercases, trims whitespace, trims trailing dots, and validates that only hostname-safe characters remain: letters, digits, dots, and hyphens ([a-zA-Z0-9.-]). Any other character (semicolons, spaces, asterisks, dollar signs, newlines, etc.) causes an error to prevent nginx config injection and similar attacks. Returns ("", err) if the result is empty or contains invalid characters.

func SanitizeName

func SanitizeName(input string) (string, error)

SanitizeName lowercases, trims whitespace, replaces spaces/underscores with hyphens, removes all non-alphanumeric-hyphen characters, collapses consecutive hyphens, and trims leading/trailing hyphens. Returns ("", err) if the result is empty after normalization.

func SetTelemetryEnabled added in v1.0.11

func SetTelemetryEnabled(enabled bool) error

SetTelemetryEnabled persists the telemetry preference to ~/.nself/config.toml. Creates the file and directory if they do not exist.

func URLPassword added in v1.4.13

func URLPassword(password string) string

URLPassword returns password percent-encoded for the password position of a connection URL's userinfo (RFC 3986 §3.2.1): the exact bytes URLUserInfo emits after the colon, so "redis://:" + URLPassword(p) + "@host" and URLUserInfo("", p) agree.

Purpose: docker compose can interpolate a variable into a URL but cannot encode it, so a password with "/", "@", ":", "?", "#" or "%" breaks every compose fragment that writes ":${POSTGRES_PASSWORD}@" (P7-PROD-31). nself build writes this value as POSTGRES_PASSWORD_URLENC / REDIS_PASSWORD_URLENC and fragments reference it. Encode once, at the source: the result must be substituted verbatim and never encoded again (a second pass turns "%40" into "%2540"). Inputs: any password, possibly empty. Outputs: the encoded password, or "" for an empty password (callers omit the variable so the compose fallback applies).

func URLUserInfo added in v1.4.10

func URLUserInfo(user, password string) string

URLUserInfo returns the "user:password" part of a connection URL with both halves percent-encoded for the userinfo component (RFC 3986 §3.2.1).

Purpose: every DSN the CLI builds (DATABASE_URL, REDIS_URL, pgbouncer and restore URLs) embeds a credential. A password containing "/", "@", ":", "?" or "#" must be escaped there, or the URL parses with the wrong host and clients fail with "Invalid URL" (nself-web prod, 2026-09-23: a base64 REDIS_PASSWORD with "/" left auth-server rate limiting, revocation and lockout without Redis). url.PathEscape is not a substitute: it leaves "@" and ":" unescaped. Inputs: user may be empty (Redis uses ":password"); password may be empty. Outputs: "user", "user:pw" or ":pw", escaped; "" when both are empty.

func UnquoteEnvValue added in v1.1.1

func UnquoteEnvValue(v string) string

UnquoteEnvValue strips a single layer of wrapping single quotes from v when the entire value is single-quoted. It is the inverse of QuoteEnvValue.

Example:

UnquoteEnvValue(`'{"type":"HS256","key":"abc"}'`) → `{"type":"HS256","key":"abc"}`
UnquoteEnvValue("plain-value")                    → "plain-value"

func UseRandSource added in v1.4.13

func UseRandSource(r io.Reader) (restore func())

UseRandSource makes every secret generated by this package (JWT key, admin and plugin secrets) read from r until the returned restore func runs; a nil r keeps crypto/rand. It is for the build orchestrator (build.Options.Rand), so a plan and an apply of one fresh project can generate identical secrets. The scope holds a package lock: concurrent scopes run one after another, and each restores crypto/rand on exit. Callers must call restore exactly once and must not nest scopes. Direct config.Load callers outside a scope always get crypto/rand unless a seeded scope is open in the same process; the seed is a test and plan-fidelity facility, never set it in production.

func Validate

func Validate(cfg *Config) error

Validate checks cfg for security issues, port conflicts, and route collisions by running all registered validators. All failures are collected and returned together so the caller sees the full picture.

Password and JWT validations are only enforced when Env is "staging" or "prod". In dev mode, weak passwords are acceptable (the caller is expected to auto-generate strong ones before reaching validation).

func ValidateBuildContext added in v1.4.13

func ValidateBuildContext(key, projectDir, p, dockerfile string) error

ValidateBuildContext enforces E528 on an ancestor CS_N_PATH (key names the variable in the message). projectDir is the project root ("" means the current directory); dockerfile is CS_N_DOCKERFILE ("" means Dockerfile). A path with no ".." segment is inside the project and passes. An ancestor context may reach at most the nearest ancestor of the project that holds .git (the project itself when none does), and the ignore file BuildKit will use must exclude the .env* and .secrets files at the context root, in every directory down to the project, and below it, so the monorepo's secrets are never sent to the image builder. A bare ".env*" is not enough: Docker matches it at the context root only, so proj/.env would still be sent; "**/.env*" and "**/.secrets" are the patterns that cover nested paths.

func ValidateHasuraDevMode

func ValidateHasuraDevMode(cfg *Config) error

ValidateHasuraDevMode returns an error if HASURA_GRAPHQL_DEV_MODE=true in prod. When the block fires it emits a structured slog.Error so that operators with centralised log aggregation (Loki/Datadog) can alert on accidental dev-mode exposure. The error is always returned; the log is a side effect.

func ValidateMinioCredentials added in v1.1.8

func ValidateMinioCredentials(cfg *Config) error

ValidateMinioCredentials enforces strong MinIO credentials in staging and production. In dev, minioadmin defaults are accepted without error.

Rules (staging/prod only):

  • MINIO_ROOT_USER must not be empty or "minioadmin".
  • MINIO_ROOT_PASSWORD must not be "minioadmin" and must be >=16 chars.

Returns nil for dev environments unconditionally.

func ValidateNginxInputs

func ValidateNginxInputs(cfg *Config) error

ValidateNginxInputs validates raw nginx template inputs that are interpolated without escaping. Prevents nginx directive injection via env vars.

func ValidateRedisPassword

func ValidateRedisPassword(cfg *Config) error

ValidateRedisPassword returns an error if Redis is enabled in staging/prod without a password.

func WithoutLocalOverride added in v1.4.13

func WithoutLocalOverride(order []string) []string

WithoutLocalOverride returns order minus .env.local. The remote deploy cascade is EnvCascadeOrder with this applied, in the build and in the env snapshot pushed to the host alike.

Types

type AdminConfig

type AdminConfig struct {
	Enabled      bool   `env:"NSELF_ADMIN_ENABLED"`
	Version      string `env:"NSELF_ADMIN_VERSION"`  // latest
	Port         int    `env:"NSELF_ADMIN_PORT"`     // 3021
	Route        string `env:"NSELF_ADMIN_ROUTE"`    // admin.{BD}
	DevMode      bool   `env:"NSELF_ADMIN_DEV"`      // false
	DevPort      int    `env:"NSELF_ADMIN_DEV_PORT"` // 3000
	SecretKey    string `env:"ADMIN_SECRET_KEY"`
	PasswordHash string `env:"ADMIN_PASSWORD_HASH"`
}

AdminConfig holds nSelf Admin GUI configuration.

type ApiDocsConfig added in v1.0.12

type ApiDocsConfig struct {
	Enabled         bool     `env:"API_DOCS_ENABLED"`      // default: true
	Path            string   `env:"API_DOCS_PATH"`         // serve path, default: /docs
	Title           string   `env:"API_DOCS_TITLE"`        // defaults to "<ProjectName> API"
	Theme           string   `env:"API_DOCS_THEME"`        // default | moon | purple | solarized
	AuthEnvVar      string   `env:"API_DOCS_AUTH_ENV_VAR"` // env var with bearer token for try-out
	HideEndpoints   []string // paths to exclude from the spec
	GraphQLEnabled  bool     `env:"API_DOCS_GRAPHQL_ENABLED"`  // default: true
	GraphQLEndpoint string   `env:"API_DOCS_GRAPHQL_ENDPOINT"` // default: /v1/graphql
}

ApiDocsConfig holds the api_docs section from nself.yaml. The fields are filled from the API_DOCS_* env vars in loader_parse_env_apidocs.go. Controls generation of the OpenAPI 3.1 spec and Scalar interactive docs page.

type AuthConfig

type AuthConfig struct {
	Version            string `env:"AUTH_VERSION"` // 0.36.0
	Port               int    `env:"AUTH_PORT"`    // 4000
	ClientURL          string `env:"AUTH_CLIENT_URL"`
	AccessTokenExpiry  int    `env:"AUTH_ACCESS_TOKEN_EXPIRES_IN"`  // seconds
	RefreshTokenExpiry int    `env:"AUTH_REFRESH_TOKEN_EXPIRES_IN"` // seconds
	Route              string `env:"AUTH_ROUTE"`
	SMTPHost           string `env:"AUTH_SMTP_HOST"`
	SMTPPort           int    `env:"AUTH_SMTP_PORT"`
	SMTPUser           string `env:"AUTH_SMTP_USER"`
	SMTPPass           string `env:"AUTH_SMTP_PASS"`
	SMTPSecure         bool   `env:"AUTH_SMTP_SECURE"`
	SMTPSender         string `env:"AUTH_SMTP_SENDER"`
	MemLimit           string `env:"AUTH_MEM_LIMIT"`           // 256m
	CPULimit           string `env:"AUTH_CPU_LIMIT"`           // 0.25
	ExtraRedirectURLs  string `env:"AUTH_EXTRA_REDIRECT_URLS"` // comma-separated extra redirect URLs
	WebAuthnEnabled    bool   `env:"AUTH_WEBAUTHN_ENABLED"`
	MFATOTPEnabled     bool   `env:"AUTH_MFA_TOTP_ENABLED"` // false
	LogLevel           string `env:"AUTH_LOG_LEVEL"`        // info
}

AuthConfig holds authentication service configuration.

type BackupConfig

type BackupConfig struct {
	Dir           string `env:"BACKUP_DIR"` // ./backups — read by database/backup.go and restore.go
	Enabled       bool   `env:"BACKUP_ENABLED"`
	Schedule      string `env:"BACKUP_SCHEDULE"`       // legacy alias for BACKUP_SCHEDULE_FULL
	RetentionDays int    `env:"BACKUP_RETENTION_DAYS"` // legacy — use Daily/Weekly/Monthly instead
	CloudProvider string `env:"BACKUP_CLOUD_PROVIDER"` // legacy — use Remote instead

	// Cloud/remote storage
	Remote              string `env:"BACKUP_REMOTE"`                // rclone remote path, e.g. s3://bucket/path
	Encryption          bool   `env:"BACKUP_ENCRYPTION"`            // enable age encryption
	AgeRecipients       string `env:"BACKUP_AGE_RECIPIENTS"`        // age public key for encryption
	ScheduleFull        string `env:"BACKUP_SCHEDULE_FULL"`         // cron expr for full backups (default: 0 3 * * *)
	WALInterval         int    `env:"BACKUP_WAL_INTERVAL_SECONDS"`  // WAL archive interval (default: 60)
	RetentionDaily      int    `env:"BACKUP_RETENTION_DAILY"`       // keep last N daily backups (default: 7)
	RetentionWeekly     int    `env:"BACKUP_RETENTION_WEEKLY"`      // keep last N weekly backups (default: 4)
	RetentionMonthly    int    `env:"BACKUP_RETENTION_MONTHLY"`     // keep last N monthly backups (default: 12)
	RestoreTestSchedule string `env:"BACKUP_RESTORE_TEST_SCHEDULE"` // cron for restore tests (default: 0 5 * * 0)
	AlertOnFailure      bool   `env:"BACKUP_ALERT_ON_FAILURE"`      // send alert on backup failure
	S3AccessKeyID       string `env:"BACKUP_S3_ACCESS_KEY_ID"`
	S3SecretAccessKey   string `env:"BACKUP_S3_SECRET_ACCESS_KEY"`
	S3Region            string `env:"BACKUP_S3_REGION"`
	S3Endpoint          string `env:"BACKUP_S3_ENDPOINT"`

	// CriticalTables overrides database.DefaultCriticalTables for the backup
	// drill's smoke check (comma-separated table names, e.g.
	// "users,licenses,audit_logs,plugins"). Empty (the default) keeps the
	// np_-prefixed convention. Deployed schemas vary in whether they use
	// nSelf's np_ multi-app-isolation prefix, so the drill's critical-table
	// presence check must be project-configurable rather than hardcoded to
	// one convention — see .claude/qa/bugs/drill-critical-tables-naming.md.
	CriticalTables string `env:"BACKUP_CRITICAL_TABLES"`
}

BackupConfig holds backup and recovery configuration. Dir is read by internal/database/backup.go and restore.go for ad-hoc pg_dump/pg_restore. Scheduled, cloud, and retention features are managed via BACKUP_* env vars.

func (BackupConfig) HeartbeatRemote added in v1.4.13

func (BackupConfig) HeartbeatRemote() string

HeartbeatRemote returns the heartbeat remote from the environment, trimmed, or "" when it is not set.

type CSDependency added in v1.4.13

type CSDependency struct {
	Name      string
	Condition string
}

CSDependency is one CS_N_DEPENDS_ON entry. Condition is the compose value (service_started, service_healthy or service_completed_successfully).

type CascadeFile added in v1.3.0

type CascadeFile struct {
	// Name is the filename relative to the project directory, e.g. ".env.secrets".
	Name string
	// Path is Name joined with the project directory.
	Path string
	// Exists reports whether the file is present on disk.
	Exists bool
}

CascadeFile describes one file consulted by the env cascade, in load order.

func EnvCascade added in v1.3.0

func EnvCascade(projectDir, envName string, legacy bool) []CascadeFile

EnvCascade resolves EnvCascadeOrder to on-disk paths under projectDir, with existence checked. Used by `nself env explain` and the migration shim; both need to know not just the order but which files are actually present.

type Config

type Config struct {
	// Core
	ProjectName        string `env:"PROJECT_NAME"`
	BaseDomain         string `env:"BASE_DOMAIN"`
	Env                string `env:"ENV"` // dev, staging, prod
	ProjectDescription string `env:"PROJECT_DESCRIPTION"`
	AdminEmail         string `env:"ADMIN_EMAIL"`
	DBEnvSeeds         bool   `env:"DB_ENV_SEEDS"`
	// AppName is an OPTIONAL app-prefix used to namespace generated core
	// nginx subdomains (gap #5): when set, routes render as
	// "api.{AppName}.{BASE_DOMAIN}" / "auth.{AppName}.{BASE_DOMAIN}" instead
	// of the bare "api.{BASE_DOMAIN}" / "auth.{BASE_DOMAIN}" scheme. Deliberately
	// distinct from PROJECT_NAME (which is always set, for container/network
	// naming) so existing single-app deployments (ummat, unity) that only set
	// PROJECT_NAME are unaffected — AppName defaults to "" (bare scheme).
	AppName string `env:"APP_NAME"`

	// PostgreSQL
	Postgres PostgresConfig

	// Hasura
	Hasura HasuraConfig

	// Auth
	Auth AuthConfig

	// Nginx
	Nginx NginxConfig

	// SSL
	SSLMode           string `env:"SSL_MODE"`            // local, letsencrypt, custom, none
	SSLProvider       string `env:"SSL_PROVIDER"`        // cloudflare, route53, digitalocean, custom
	SSLWildcardDomain string `env:"SSL_WILDCARD_DOMAIN"` // *.example.com
	ExtraSSLDomains   string `env:"EXTRA_SSL_DOMAINS"`   // comma-separated
	CloudflareAPIKey  string `env:"CLOUDFLARE_API_KEY"`  // DNS-01 challenge

	// WAF
	WAFMode string `env:"WAF_MODE"` // off, detection, blocking

	// Optional Services
	Redis      RedisConfig
	Minio      MinioConfig
	Mailpit    MailpitConfig
	Functions  FunctionsConfig
	MLflow     MLflowConfig
	Admin      AdminConfig
	Monitoring MonitoringConfig

	// Search (provider-agnostic)
	Search SearchConfig

	// Email Provider
	Email EmailConfig

	// PgBouncer connection pooler
	PgBouncer PgBouncerConfig

	// Backup & Recovery
	Backup BackupConfig

	// Disaster Recovery
	DR DRConfig

	// Multi-Tenancy & Billing
	Tenant TenantConfig

	// License
	License LicenseConfig

	// Secrets Management
	Secrets SecretsConfig

	// Plugin Pro Configuration
	PluginConfig PluginProConfig

	// Plugin System
	PluginSystem PluginSystemConfig

	// API Docs (Scalar)
	ApiDocs ApiDocsConfig

	// Custom Services
	CustomServices []CustomService // CS_1..CS_10

	// Frontend Apps
	FrontendApps []FrontendApp // FRONTEND_APP_1..FRONTEND_APP_20

	// Remote Schemas
	RemoteSchemas []RemoteSchema

	// Internal Routes (up to 20)
	InternalRoutes []InternalRoute

	// Docker
	DockerNetwork      string `env:"DOCKER_NETWORK"`
	DockerLogMaxSize   string `env:"DOCKER_LOG_MAX_SIZE"`        // 10m
	DockerLogMaxFile   string `env:"DOCKER_LOG_MAX_FILE"`        // 3
	DockerStopGrace    string `env:"DOCKER_STOP_GRACE_PERIOD"`   // 30s
	DockerBuildTimeout int    `env:"NSELF_DOCKER_BUILD_TIMEOUT"` // 300

	// Start/Stop behavior
	StartMode           string `env:"NSELF_START_MODE"`           // smart, fresh, force
	HealthCheckTimeout  int    `env:"NSELF_HEALTH_CHECK_TIMEOUT"` // seconds
	HealthCheckInterval int    `env:"NSELF_HEALTH_CHECK_INTERVAL"`
	HealthCheckRequired int    `env:"NSELF_HEALTH_CHECK_REQUIRED"` // percentage
	CleanupOnStart      string `env:"NSELF_CLEANUP_ON_START"`      // auto/always/never
	AllowExposedPorts   bool   `env:"NSELF_ALLOW_EXPOSED_PORTS"`
	ParallelLimit       int    `env:"NSELF_PARALLEL_LIMIT"` // 5
	LogLevel            string `env:"NSELF_LOG_LEVEL"`      // info
	SkipHealthChecks    bool   `env:"NSELF_SKIP_HEALTH_CHECKS"`
	StopTimeout         int    `env:"NSELF_STOP_TIMEOUT"` // 30

	// Federation — GraphQL Federation via Apollo Router (G05).
	// When true, nself build injects Apollo Router (CS_7) and composes a
	// supergraph schema from installed plugin subgraphs. Default: false.
	FederationEnabled bool `env:"NSELF_FEDERATION"`

	// EmbeddedPG — when true, nself build omits the Docker postgres service
	// and instead relies on the pglite/wasmtime embedded runtime started by
	// `nself start --embedded-pg`. Hasura is wired via a Unix-domain socket
	// bridge. Default: false.
	EmbeddedPG bool `env:"NSELF_EMBEDDED_PG"`

	// Passthrough: arbitrary env vars matching patterns (AUTH_PROVIDER_*, REMOTE_SCHEMA_*, etc.)
	Passthrough map[string]string
}

Config is the top-level configuration struct for an nSelf project. All fields are populated from the .env cascade and environment variables.

func ApplyDefaults

func ApplyDefaults(cfg *Config) (*Config, error)

ApplyDefaults fills every empty/zero field in cfg with the canonical default value. It never overrides a non-empty string, non-zero int, or explicitly-set boolean. Empty string "" is considered unset for string fields; zero is considered unset for int fields.

Environment-specific overrides (Console, DevMode, CORS, BindIP, SSL) are applied after all static defaults.

func Load

func Load(projectDir string) (*Config, error)

Load reads the .env cascade from projectDir, populates a Config struct from os.Getenv, applies smart defaults, and returns the complete configuration.

Cascade order (later overrides earlier), approved 2026-08-23 GATE B (CLI-R18):

.env → .env.{dev|staging|prod} → .env.secrets → .env.local

.env is the shared, committed base; exactly one of .env.dev/.env.staging/ .env.prod loads, matching ENV; .env.secrets never ships in git; .env.local is the personal override and always wins. .env.ai no longer exists as a cascade layer — its content is folded into .env.secrets at init/upgrade (internal/setup/envai.go, internal/migrate/env_order.go).

Setting NSELF_LEGACY_ENV_ORDER=1 restores the pre-CLI-R18 order for exactly one minor version (see EnvCascadeOrder in cascade.go for both orders in full), printing a warning on every use. Run `nself migrate` to move a project off the escape hatch permanently.

Each file is optional. Missing files are silently skipped.

func LoadWithOptions added in v1.4.13

func LoadWithOptions(projectDir string, opts LoadOptions) (*Config, error)

LoadWithOptions is Load with explicit options.

func (*Config) DatabaseURL

func (cfg *Config) DatabaseURL() string

DatabaseURL returns the computed PostgreSQL connection string using internal container networking (always port 5432, host "postgres"). The password is percent-encoded per RFC 3986 for safe URL inclusion (see URLUserInfo).

func (*Config) EmbeddedPGDatabaseURL added in v1.1.3

func (cfg *Config) EmbeddedPGDatabaseURL(runtimeDir string) string

EmbeddedPGDatabaseURL returns a PostgreSQL DSN that connects via the Unix-domain socket bridge created by the pglite/wasmtime embedded runtime. The host field is the runtimeDir path; sslmode=disable is required because the embedded runtime does not perform TLS termination.

Use this DSN only when cfg.EmbeddedPG is true and runtimeDir is the directory passed to embedded.NewEmbeddedPGRuntime.

func (*Config) HostDatabaseURL added in v1.4.13

func (cfg *Config) HostDatabaseURL() string

HostDatabaseURL returns the PostgreSQL connection string for a client that runs on the host, outside the compose network.

Purpose: DatabaseURL is the in-network DSN (host "postgres", port 5432) and does not resolve from the host. Compose publishes Postgres on loopback only, so host-side tools (importer, exporter, table hash) connect to 127.0.0.1 on POSTGRES_PORT. Inputs: cfg.Postgres User, Password, Port (0 means 5432) and DB (empty means "nself", the default applyDefaultsPostgres sets). Outputs: postgresql://<user>:<pass>@127.0.0.1:<port>/<db>, with the credentials percent-encoded through URLUserInfo exactly as DatabaseURL does. Constraints: the result carries the password; callers never log or print it.

func (*Config) IsProduction

func (c *Config) IsProduction() bool

IsProduction reports whether the project environment is production. Both "prod" and "production" are treated as production; the loader normalises "production" → "prod" via normalizeEnv, so only "prod" is checked here.

type CustomService

type CustomService struct {
	Index int    // 1-10
	Name  string // parsed from CS_N, then overwritten with SanitizeName(Name) for
	// Docker service/container naming (hyphens only, no underscores).
	// RawName preserves the pre-sanitization value.
	//
	// Purpose: a user's CS_N name (e.g. "ping_api") may contain underscores,
	// which are valid in a directory name but not in a Docker compose
	// service/container name. parseCustomServices sanitizes Name for that
	// reason, but internal/compose.defaultCustomServiceBuildContext needs
	// the original spelling to find the on-disk `services/<name>` directory
	// the user actually created — using the sanitized Name there produced a
	// build context ("./services/ping-api") that never matched an existing
	// directory ("./services/ping_api"), so the image build failed for any
	// name containing an underscore (see the F13/G-013-adjacent bug this
	// field fixes). Empty when a CustomService is constructed by hand
	// (tests, scaffolding) rather than through parseCustomServices.
	RawName     string
	Template    string // express-ts, fastapi, etc.
	Port        int
	Route       string // empty = internal only
	Public      bool
	Memory      string
	CPU         string
	TablePrefix string // CS_N_TABLE_PREFIX
	ExtraEnv    string // CS_N_ENV (raw key=val pairs, comma-separated)
	BuildPath   string // CS_N_PATH: overrides default ./services/{name} build context

	// HealthCheck is CS_N_HEALTHCHECK: a path (e.g. "/auth/health"), a full
	// "CMD ..." / "CMD-SHELL ..." override, or "disabled"/"none"/"false" to
	// omit the healthcheck entirely. Empty keeps the default GET /health.
	HealthCheck string

	// EnvPassthrough is CS_N_ENV_PASSTHROUGH: a comma-separated allowlist of
	// project .env var names to forward into this container in addition to
	// the fixed core set from coreEnvVars. CS_N_ENV still wins on conflict.
	EnvPassthrough string

	// Image is CS_N_IMAGE: a pre-built image reference (optionally digest-pinned,
	// e.g. "minio/minio:RELEASE.2024-01-16T16-07-38Z@sha256:...") to run instead
	// of building from a Dockerfile. When set, the compose generator emits
	// `image:` and omits `build:` entirely — mutually exclusive with
	// CS_N_PATH (G-013: closes the gap where a pinned third-party image had
	// no CS_N representation and had to be hand-authored into
	// docker-compose.override.yml).
	Image string

	// EnvFile is CS_N_ENV_FILE: a project-relative path to a dotenv-format
	// file whose KEY=VALUE lines are injected into this container. Unlike
	// CS_N_ENV (a single comma-joined line), a file has no comma/newline
	// escaping problem, so it is the right vehicle for many vars or values
	// that themselves contain commas (e.g. SMTP credentials). Precedence:
	// applied after CS_N_ENV_PASSTHROUGH, before CS_N_ENV (CS_N_ENV always
	// wins on conflict, per the existing coreEnvVars contract). Same
	// relative-path rules as BuildPath (no absolute paths, no "..").
	EnvFile string

	// Volumes is CS_N_VOLUMES: a comma-separated list of extra Docker volume
	// mounts in "host:container[:mode]" form (e.g.
	// "./email-templates:/app/templates:ro"), appended to the service's
	// generated volume list. Closes the gap where a required bind mount
	// (e.g. a template directory) had no CS_N representation.
	Volumes string

	// EnvFiles is CS_N_ENV_FILE split on commas, in load order (later wins).
	// EnvFile stays the first entry for callers that read it.
	EnvFiles []string

	// DependsOn is CS_N_DEPENDS_ON: extra compose depends_on entries. Names
	// are syntax-checked here and resolved against the whole project in build
	// post-validation (after the plugin step).
	DependsOn []CSDependency

	// Networks is CS_N_NETWORKS: extra project networks, each
	// <PROJECT_NAME>_*, emitted as external networks (checked at compose time).
	Networks []string

	// Dockerfile is CS_N_DOCKERFILE: path relative to the build context.
	Dockerfile string

	// BuildTarget is CS_N_BUILD_TARGET: the multi-stage build target.
	BuildTarget string

	// Command is CS_N_COMMAND: exec-form command (whitespace-split, no shell).
	Command []string
}

CustomService represents a user-defined custom service (CS_1..CS_10).

func CustomServicesFromEnv added in v1.4.13

func CustomServicesFromEnv() ([]CustomService, error)

CustomServicesFromEnv parses CS_1..CS_10 from the process environment. Build post-validation uses it to read each service's CS_N_DEPENDS_ON after the plugin step, without reloading the project config.

type DRConfig added in v1.0.6

type DRConfig struct {
	SecondaryRegion string `env:"DR_SECONDARY_REGION"` // Hetzner region for standby
	StandbyHost     string `env:"DR_STANDBY_HOST"`     // IP/hostname of warm standby
	DrillSchedule   string `env:"DR_DRILL_SCHEDULE"`   // cron for DR drills (default: off)
}

DRConfig holds disaster recovery configuration.

type ElasticsearchConfig

type ElasticsearchConfig struct {
	Version  string `env:"ELASTICSEARCH_VERSION"` // 8.11.3
	Port     int    `env:"ELASTICSEARCH_PORT"`    // 9200
	Password string `env:"ELASTICSEARCH_PASSWORD"`
	Memory   string `env:"ELASTICSEARCH_MEMORY"` // 1Gi
}

ElasticsearchConfig holds Elasticsearch-specific configuration.

type EmailConfig

type EmailConfig struct {
	Provider            string `env:"EMAIL_PROVIDER"` // mailpit/elasticemail/sendgrid/postmark/mailgun/ses/smtp
	From                string `env:"EMAIL_FROM"`
	ElasticEmailAPIKey  string `env:"ELASTIC_EMAIL_API_KEY"`
	ElasticEmailAccount string `env:"ELASTIC_EMAIL_ACCOUNT_EMAIL"`
	SendGridAPIKey      string `env:"SENDGRID_API_KEY"`
	PostmarkAPIKey      string `env:"POSTMARK_API_KEY"`
	MailgunAPIKey       string `env:"MAILGUN_API_KEY"`
	MailgunDomain       string `env:"MAILGUN_DOMAIN"`
	AWSAccessKeyID      string `env:"AWS_ACCESS_KEY_ID"`
	AWSSecretAccessKey  string `env:"AWS_SECRET_ACCESS_KEY"`
	AWSRegion           string `env:"AWS_REGION"`
	SMTPHost            string `env:"SMTP_HOST"`
	SMTPPort            int    `env:"SMTP_PORT"`
	SMTPUser            string `env:"SMTP_USER"`
	SMTPPass            string `env:"SMTP_PASS"`
	SMTPSecure          bool   `env:"SMTP_SECURE"`
}

EmailConfig holds email provider configuration.

type Explanation added in v1.4.13

type Explanation struct {
	// Key is the variable name asked about.
	Key string
	// Known reports whether the key is in KnownEnvVars().
	Known bool
	// Default is DefaultFor(Key); empty when the key has no static default.
	Default string
	// Env is the active environment name the cascade was built for.
	Env string
	// EnvSource is where Env came from (ResolveEnv: "process environment",
	// ".env" or "default"); empty when the caller passed Env directly.
	EnvSource string
	// Legacy reports that NSELF_LEGACY_ENV_ORDER selected the legacy order.
	Legacy bool
	// Cascade is every file the loader consults, lowest precedence first.
	Cascade []CascadeFile
	// Setters are the existing cascade files that set the key, in cascade order.
	Setters []Setter
	// ProcessSet and ProcessValue describe the process environment.
	ProcessSet   bool
	ProcessValue string
}

Explanation is the answer to "where does this key come from".

func ExplainActive added in v1.4.13

func ExplainActive(projectDir, key string) (Explanation, error)

ExplainActive is ExplainKey for the environment ResolveEnv picks in projectDir, with ResolveEnv's source kept in EnvSource.

func ExplainKey added in v1.4.13

func ExplainKey(projectDir, env, key string) (Explanation, error)

ExplainKey walks the env cascade of projectDir for the active environment env and reports every source of key. A file that exists but cannot be read is an error.

func (Explanation) Effective added in v1.4.13

func (e Explanation) Effective() (source, value string)

Effective names the source the loader ends up using and its value: a cascade file name, "process environment", "default" or "unset".

func (Explanation) SelectsCascade added in v1.4.13

func (e Explanation) SelectsCascade() bool

SelectsCascade reports whether the key is ENV, whose resolved value chooses which cascade files are read. For ENV the process value is consulted first (see ResolveEnv), so EnvSource can name the process environment even when a file wins the final value.

func (Explanation) Winner added in v1.4.13

func (e Explanation) Winner() (file, value string, ok bool)

Winner returns the winning cascade file and its value; ok is false when no file sets the key. The cascade is lowest precedence first, so the last setter wins.

type FrontendApp

type FrontendApp struct {
	Index       int
	DisplayName string
	SystemName  string
	Port        int
	Route       string
	Framework   string
	TablePrefix string
	Image       string // FRONTEND_APP_N_IMAGE (optional docker image reference)
}

FrontendApp represents a frontend application (FRONTEND_APP_1..FRONTEND_APP_20).

type FunctionsConfig

type FunctionsConfig struct {
	Enabled bool   `env:"FUNCTIONS_ENABLED"`
	Version string `env:"FUNCTIONS_VERSION"` // latest
	Port    int    `env:"FUNCTIONS_PORT"`    // 3008
	Route   string `env:"FUNCTIONS_ROUTE"`   // functions.{BD}

	// Runtime selects the container image: node (default), deno, python.
	Runtime string `env:"FUNCTIONS_RUNTIME"` // node|deno|python

	// Resource limits for the functions container.
	Memory  string `env:"FUNCTIONS_MEMORY"`  // 256M
	CPU     string `env:"FUNCTIONS_CPU"`     // 0.5
	Timeout string `env:"FUNCTIONS_TIMEOUT"` // 30s
}

FunctionsConfig holds serverless functions runtime configuration.

type HasuraConfig

type HasuraConfig struct {
	Version     string `env:"HASURA_VERSION"`
	AdminSecret string `env:"HASURA_GRAPHQL_ADMIN_SECRET"`
	JWTKey      string `env:"HASURA_JWT_KEY"`
	JWTType     string `env:"HASURA_JWT_TYPE"` // HS256
	Console     bool   `env:"HASURA_GRAPHQL_ENABLE_CONSOLE"`
	DevMode     bool   `env:"HASURA_GRAPHQL_DEV_MODE"`
	CORSDomain  string `env:"HASURA_GRAPHQL_CORS_DOMAIN"`
	Route       string `env:"HASURA_ROUTE"` // api.{BASE_DOMAIN}
	Port        int    `env:"HASURA_PORT"`  // 8080
	MemLimit    string `env:"HASURA_MEM_LIMIT"`
	CPULimit    string `env:"HASURA_CPU_LIMIT"`
	LogLevel    string `env:"HASURA_GRAPHQL_LOG_LEVEL"` // warn
}

HasuraConfig holds Hasura GraphQL engine configuration.

type InternalRoute

type InternalRoute struct {
	Index     int
	Name      string // INTERNAL_ROUTE_N_NAME
	Subdomain string // INTERNAL_ROUTE_N_SUBDOMAIN
	Target    string // INTERNAL_ROUTE_N_TARGET (e.g., hasura:8080)
	RateZone  string // INTERNAL_ROUTE_N_RATE_ZONE (default: general)
	WebSocket bool   // INTERNAL_ROUTE_N_WEBSOCKET
}

InternalRoute represents an internal Nginx route (INTERNAL_ROUTE_1..INTERNAL_ROUTE_20).

type LicenseConfig added in v1.0.6

type LicenseConfig struct {
	PingURL           string    `env:"LICENSE_PING_URL"`            // https://ping.nself.org
	CachePath         string    `env:"LICENSE_CACHE_PATH"`          // ~/.cache/nself/license.json
	CheckInterval     string    `env:"LICENSE_CHECK_INTERVAL"`      // 6h
	OfflineMode       bool      `env:"LICENSE_OFFLINE_MODE"`        // false
	PublicKeyOverride string    `env:"LICENSE_PUBLIC_KEY_OVERRIDE"` // hex-encoded Ed25519 pubkey for testing
	SunsetAt          time.Time `env:"LICENSE_SUNSET_AT"`           // optional hard cutoff; zero = no sunset
}

LicenseConfig holds license validation and grace period configuration.

Offline grace-period lengths are NOT configurable here: they are fixed constants in internal/license/grace.go (GraceSoftThreshold, GraceHardThreshold), by design — see checker.go's exposure rationale (grace.go's GraceHardThreshold comment). A prior GraceDays field (env:"LICENSE_GRACE_DAYS") was declared here and referenced in a grace.go comment claiming the ladder was configurable, but nothing in the codebase ever read it. Both were removed together, P6-E12-W4-S4-T2, 2026-09.

type LoadOptions added in v1.4.13

type LoadOptions struct {
	// RemoteDeploy drops .env.local from the cascade: the build is for a
	// remote host, and a developer's personal override must not reach it (the
	// env file pushed to the host omits it too).
	RemoteDeploy bool
	// EnvOverlay replaces cascade files by name (".env", ".env.secrets", ...)
	// with the given bytes for this load only: the disk copy of an overlaid
	// name is not read and need not exist. A plan uses it to load the cascade
	// a pending `config set` will leave before the line is written
	// (P7-LIVE-05, env_overlay.go). Nil reads every file from disk.
	EnvOverlay map[string][]byte
}

LoadOptions are the caller's choices for one Load. They are explicit arguments, never read from the environment or kept in process state.

type Loader

type Loader interface {
	Load(dir string) (*Config, error)
}

Loader abstracts configuration loading for testability.

type MLflowConfig

type MLflowConfig struct {
	Enabled         bool   `env:"MLFLOW_ENABLED"`
	Route           string `env:"MLFLOW_ROUTE"`            // mlflow.{BD} — read by ssl/domains.go, urls.go, doctor.go
	Version         string `env:"MLFLOW_VERSION"`          // plugin-managed: populated by nself plugin install mlflow
	Port            int    `env:"MLFLOW_PORT"`             // read by doctor.go for port-conflict checks; plugin-managed: populated by nself plugin install mlflow
	DBName          string `env:"MLFLOW_DB_NAME"`          // plugin-managed: populated by nself plugin install mlflow
	ArtifactsBucket string `env:"MLFLOW_ARTIFACTS_BUCKET"` // plugin-managed: populated by nself plugin install mlflow
	AuthEnabled     bool   `env:"MLFLOW_AUTH_ENABLED"`     // plugin-managed: populated by nself plugin install mlflow
	AuthUsername    string `env:"MLFLOW_AUTH_USERNAME"`    // plugin-managed: populated by nself plugin install mlflow
	AuthPassword    string `env:"MLFLOW_AUTH_PASSWORD"`    // plugin-managed: populated by nself plugin install mlflow
}

MLflowConfig holds MLflow experiment tracking configuration. Compose generation is plugin-managed: nself plugin install mlflow Enabled, Route, and Port are read by ssl/domains.go, urls.go, and doctor.go. All other fields are consumed exclusively by the nself-mlflow plugin at install time.

type MailpitConfig

type MailpitConfig struct {
	Enabled     bool   `env:"MAILPIT_ENABLED"`
	Version     string `env:"MAILPIT_VERSION"`      // latest
	SMTPPort    int    `env:"MAILPIT_SMTP_PORT"`    // 1025
	UIPort      int    `env:"MAILPIT_UI_PORT"`      // 8025
	MaxMessages int    `env:"MAILPIT_MAX_MESSAGES"` // 500
	Route       string `env:"MAILPIT_ROUTE"`        // mail.{BD}
	UIUser      string `env:"MAILPIT_UI_USER"`      // admin (default)
	UIPassword  string `env:"MAILPIT_UI_PASSWORD"`
}

MailpitConfig holds Mailpit local email testing configuration.

type MeiliSearchConfig

type MeiliSearchConfig struct {
	Version   string `env:"MEILISEARCH_VERSION"` // v1.6
	MasterKey string `env:"MEILISEARCH_MASTER_KEY"`
	Env       string `env:"MEILISEARCH_ENV"` // development
}

MeiliSearchConfig holds MeiliSearch-specific configuration.

type MinioConfig

type MinioConfig struct {
	Enabled        bool   `env:"MINIO_ENABLED"`
	Version        string `env:"MINIO_VERSION"`         // latest
	Port           int    `env:"MINIO_PORT"`            // 9000
	ConsolePort    int    `env:"MINIO_CONSOLE_PORT"`    // 9001
	RootUser       string `env:"MINIO_ROOT_USER"`       // minioadmin
	RootPassword   string `env:"MINIO_ROOT_PASSWORD"`   // minioadmin
	DefaultBuckets string `env:"MINIO_DEFAULT_BUCKETS"` // uploads,public,private,temp
	Region         string `env:"MINIO_REGION"`          // us-east-1
	S3AccessKey    string `env:"S3_ACCESS_KEY"`
	S3SecretKey    string `env:"S3_SECRET_KEY"`
	S3Bucket       string `env:"S3_BUCKET"`             // nself
	StorageVersion string `env:"STORAGE_VERSION"`       // 0.6.1
	StorageRoute   string `env:"STORAGE_ROUTE"`         // storage.{BD}
	ConsoleRoute   string `env:"STORAGE_CONSOLE_ROUTE"` // storage-console.{BD}
	MemLimit       string `env:"MINIO_MEMORY"`          // 1G
	CPULimit       string `env:"MINIO_CPU"`             // 0.5
}

MinioConfig holds MinIO S3-compatible object storage configuration.

type MonitoringConfig

type MonitoringConfig struct {
	Enabled              bool   `env:"MONITORING_ENABLED"`
	GrafanaEnabled       bool   `env:"GRAFANA_ENABLED"`
	GrafanaRoute         string `env:"GRAFANA_ROUTE"`             // read by ssl/domains.go, urls.go, doctor.go
	GrafanaAdminPassword string `env:"GRAFANA_ADMIN_PASSWORD"`    // read by config/validator.go
	PrometheusEnabled    bool   `env:"PROMETHEUS_ENABLED"`        // plugin-managed: populated by nself plugin install monitoring
	PrometheusPort       int    `env:"PROMETHEUS_PORT"`           // plugin-managed: populated by nself plugin install monitoring
	GrafanaPort          int    `env:"GRAFANA_PORT"`              // read by urls.go and doctor.go for port display; plugin-managed: populated by nself plugin install monitoring
	GrafanaAdminUser     string `env:"GRAFANA_ADMIN_USER"`        // plugin-managed: populated by nself plugin install monitoring
	LokiEnabled          bool   `env:"LOKI_ENABLED"`              // plugin-managed: populated by nself plugin install monitoring
	LokiPort             int    `env:"LOKI_PORT"`                 // plugin-managed: populated by nself plugin install monitoring
	PromtailEnabled      bool   `env:"PROMTAIL_ENABLED"`          // plugin-managed: populated by nself plugin install monitoring
	TempoEnabled         bool   `env:"TEMPO_ENABLED"`             // plugin-managed: populated by nself plugin install monitoring
	TempoPort            int    `env:"TEMPO_PORT"`                // plugin-managed: populated by nself plugin install monitoring
	AlertmanagerEnabled  bool   `env:"ALERTMANAGER_ENABLED"`      // plugin-managed: populated by nself plugin install monitoring
	AlertmanagerPort     int    `env:"ALERTMANAGER_PORT"`         // plugin-managed: populated by nself plugin install monitoring
	CadvisorEnabled      bool   `env:"CADVISOR_ENABLED"`          // plugin-managed: populated by nself plugin install monitoring
	CadvisorPort         int    `env:"CADVISOR_PORT"`             // plugin-managed: populated by nself plugin install monitoring
	NodeExporterEnabled  bool   `env:"NODE_EXPORTER_ENABLED"`     // plugin-managed: populated by nself plugin install monitoring
	NodeExporterPort     int    `env:"NODE_EXPORTER_PORT"`        // plugin-managed: populated by nself plugin install monitoring
	PGExporterEnabled    bool   `env:"POSTGRES_EXPORTER_ENABLED"` // plugin-managed: populated by nself plugin install monitoring
	PGExporterPort       int    `env:"POSTGRES_EXPORTER_PORT"`    // plugin-managed: populated by nself plugin install monitoring
	RedisExporterEnabled bool   `env:"REDIS_EXPORTER_ENABLED"`    // plugin-managed: populated by nself plugin install monitoring
	RedisExporterPort    int    `env:"REDIS_EXPORTER_PORT"`       // plugin-managed: populated by nself plugin install monitoring

	// S34 additions
	PrometheusRetention      string `env:"PROMETHEUS_RETENTION"`       // e.g. "30d"
	LokiHotDays              int    `env:"LOKI_HOT_DAYS"`              // default 30
	LokiColdDays             int    `env:"LOKI_COLD_DAYS"`             // default 365
	AlertmanagerPagerdutyKey string `env:"ALERTMANAGER_PAGERDUTY_KEY"` // PagerDuty integration key

	// Watchdog
	WatchdogEnabled                bool   `env:"WATCHDOG_ENABLED"`
	WatchdogCircuitBreakerAttempts int    `env:"WATCHDOG_CIRCUIT_BREAKER_ATTEMPTS"` // default 3
	WatchdogCircuitBreakerWindow   string `env:"WATCHDOG_CIRCUIT_BREAKER_WINDOW"`   // default 10m
	WatchdogEscalationWebhook      string `env:"WATCHDOG_ESCALATION_WEBHOOK"`

	// Queue/Jobs
	QueueWorkersPerQueue   int `env:"QUEUE_WORKERS_PER_QUEUE"`   // default 2
	QueueDLQAlertThreshold int `env:"QUEUE_DLQ_ALERT_THRESHOLD"` // default 100

	// Promotion
	PromoteRequiresTwoApprovers bool `env:"PROMOTE_REQUIRES_TWO_APPROVERS"`
}

MonitoringConfig holds monitoring stack configuration. Compose generation is plugin-managed: nself plugin install monitoring Enabled, GrafanaEnabled, GrafanaRoute, GrafanaAdminPassword, and GrafanaPort are read by ssl/domains.go (SSL SANs), config/validator.go (password check), urls.go, and doctor.go. All other fields are consumed exclusively by the nself-monitoring plugin at install time.

type NginxConfig

type NginxConfig struct {
	Version       string `env:"NGINX_VERSION"`              // alpine
	HTTPPort      int    `env:"NGINX_HTTP_PORT"`            // 80
	SSLPort       int    `env:"NGINX_HTTPS_PORT"`           // 443
	MaxBody       string `env:"NGINX_CLIENT_MAX_BODY_SIZE"` // 100M
	BindIP        string `env:"NGINX_BIND_IP"`              // computed: 127.0.0.1 (dev) or 0.0.0.0 (prod) — overridable
	AuthRateLimit string `env:"AUTH_RATE_LIMIT"`            // 30r/m
	RateLimitAPI  string `env:"RATE_LIMIT_API_RPS"`         // 30
	RateLimitAuth string `env:"RATE_LIMIT_AUTH_RPS"`        // 5
	RateLimitAI   string `env:"RATE_LIMIT_AI_RPS"`          // 10

	// FrontedBy names the stack whose nginx fronts this project's domains
	// (e.g. "nself-web"), when this project has no ingress nginx of its own.
	// Empty (the default) means this stack runs its own nginx, unchanged
	// from every prior release.
	//
	// When set, `nself build` omits the nginx service from
	// docker-compose.yml entirely and excludes it from the service counts
	// `nself status`/`nself start` expect. Without this, a stack meant to
	// sit behind another stack's nginx still generates and tries to start
	// its own nginx container, which fails to bind 80/443 (already held by
	// the fronting stack's nginx via docker-proxy) and then sits forever as
	// one unhealthy service nself status can never clear ("6/7" on ntask's
	// staging deploy, 2026-09-03, fronted by nself-web).
	//
	// This flag only removes the container that can never run. It does NOT
	// wire this project's containers onto the fronting stack's Docker
	// network — the fronting nginx's `resolver 127.0.0.11` can only reach
	// container names on a network it is attached to, so making
	// auth.task.staging.nself.org resolve ntask_auth still requires the
	// operator to attach the two projects' networks (or declare one as
	// external in both compose files) by hand. That is a bigger, riskier
	// change — automatically rewriting a Docker network topology from a
	// per-project flag — and belongs in a follow-up once the desired shape
	// of that wiring is decided.
	FrontedBy string `env:"NGINX_FRONTED_BY"`
}

NginxConfig holds Nginx reverse proxy configuration.

type PgBouncerConfig added in v1.0.9

type PgBouncerConfig struct {
	Enabled           bool   `env:"PGBOUNCER_ENABLED"`
	Port              int    `env:"PGBOUNCER_PORT"`                // 6432
	PoolMode          string `env:"PGBOUNCER_POOL_MODE"`           // session, transaction, statement
	MaxClientConn     int    `env:"PGBOUNCER_MAX_CLIENT_CONN"`     // 100
	DefaultPoolSize   int    `env:"PGBOUNCER_DEFAULT_POOL_SIZE"`   // 25
	MinPoolSize       int    `env:"PGBOUNCER_MIN_POOL_SIZE"`       // 5
	ReservePoolSize   int    `env:"PGBOUNCER_RESERVE_POOL_SIZE"`   // 5
	ServerIdleTimeout int    `env:"PGBOUNCER_SERVER_IDLE_TIMEOUT"` // 600
	LogConnections    bool   `env:"PGBOUNCER_LOG_CONNECTIONS"`     // false
	LogDisconnections bool   `env:"PGBOUNCER_LOG_DISCONNECTIONS"`  // false
	AdminUsers        string `env:"PGBOUNCER_ADMIN_USERS"`         // postgres
	StatsUsers        string `env:"PGBOUNCER_STATS_USERS"`         // postgres
}

PgBouncerConfig holds connection pooler configuration.

type PluginProConfig

type PluginProConfig struct {
	NotifySecret    string `env:"NOTIFY_INTERNAL_SECRET"`
	NotifyPort      int    `env:"NOTIFY_PORT"` // 3712
	NotifyVAPIDPub  string `env:"NOTIFY_VAPID_PUBLIC_KEY"`
	NotifyVAPIDPriv string `env:"NOTIFY_VAPID_PRIVATE_KEY"`
	NotifyRoute     string `env:"NOTIFY_ROUTE"`
	CronSecret      string `env:"CRON_INTERNAL_SECRET"`
	CronPort        int    `env:"CRON_PORT"`                   // 3713
	CronRetention   int    `env:"CRON_RETENTION_DAYS"`         // 90
	AIMemLimit      string `env:"PLUGIN_AI_MEMORY_LIMIT"`      // 1g
	AICPULimit      string `env:"PLUGIN_AI_CPU_LIMIT"`         // 1.0
	MuxMemLimit     string `env:"PLUGIN_MUX_MEMORY_LIMIT"`     // 512m
	MuxCPULimit     string `env:"PLUGIN_MUX_CPU_LIMIT"`        // 0.5
	ClawMemLimit    string `env:"PLUGIN_CLAW_MEMORY_LIMIT"`    // 512m
	ClawCPULimit    string `env:"PLUGIN_CLAW_CPU_LIMIT"`       // 0.5
	DefaultMemLimit string `env:"PLUGIN_DEFAULT_MEMORY_LIMIT"` // 512m
	DefaultCPULimit string `env:"PLUGIN_DEFAULT_CPU_LIMIT"`    // 0.5
}

PluginProConfig holds per-plugin configuration for Pro plugins.

type PluginSystemConfig

type PluginSystemConfig struct {
	Dir            string `env:"NSELF_PLUGIN_DIR"`         // ~/.nself/plugins
	Cache          string `env:"NSELF_PLUGIN_CACHE"`       // ~/.nself/cache/plugins
	Registry       string `env:"NSELF_PLUGIN_REGISTRY"`    // https://plugins.nself.org
	CacheTTL       int    `env:"NSELF_REGISTRY_CACHE_TTL"` // 300
	LicenseKey     string `env:"NSELF_PLUGIN_LICENSE_KEY"`
	SkipVerify     bool   `env:"NSELF_LICENSE_SKIP_VERIFY"`
	PingURL        string `env:"NSELF_PING_API_URL"` // https://ping.nself.org
	PricingURL     string `env:"NSELF_PRICING_URL"`  // https://nself.org/pricing
	InternalSecret string `env:"PLUGIN_INTERNAL_SECRET"`
}

PluginSystemConfig holds plugin system management configuration.

type PostgresConfig

type PostgresConfig struct {
	Version string `env:"POSTGRES_VERSION"` // 16-alpine
	// Image, when set, is the exact postgres image nself build emits and wins
	// over every other resolution (see compose.ResolvePostgresImage). Set this
	// to pin a running image (e.g. pgvector/pgvector:pg16) across regens
	// without relying on POSTGRES_EXTENSIONS inference (cli#384).
	Image      string   `env:"POSTGRES_IMAGE"`
	Host       string   `env:"POSTGRES_HOST"` // postgres (container name)
	Port       int      `env:"POSTGRES_PORT"` // 5432
	DB         string   `env:"POSTGRES_DB"`   // nself
	User       string   `env:"POSTGRES_USER"` // postgres
	Password   string   `env:"POSTGRES_PASSWORD"`
	Extensions []string `env:"POSTGRES_EXTENSIONS"`  // comma-separated list
	ExposePort string   `env:"POSTGRES_EXPOSE_PORT"` // auto, true, false
	MemLimit   string   `env:"POSTGRES_MEM_LIMIT"`   // 2g
	CPULimit   string   `env:"POSTGRES_CPU_LIMIT"`   // 2.0
	// MaxConnections overrides Postgres max_connections. The stock default of
	// 100 exhausts under a multi-service stack (PERF-POOL-01); nself raises it.
	MaxConnections int `env:"POSTGRES_MAX_CONNECTIONS"` // default 200
}

PostgresConfig holds PostgreSQL database configuration.

type RedisConfig

type RedisConfig struct {
	Enabled  bool   `env:"REDIS_ENABLED"`
	Version  string `env:"REDIS_VERSION"`   // 7-alpine
	Port     int    `env:"REDIS_PORT"`      // 6379
	Password string `env:"REDIS_PASSWORD"`  // empty = no auth
	Memory   string `env:"REDIS_MEMORY"`    // 512M
	CPU      string `env:"REDIS_CPU"`       // 0.5
	PoolSize int    `env:"REDIS_POOL_SIZE"` // 50 (prod default); use 20 for dev
	// Engine is REDIS_ENGINE: "valkey" or "redis" (lower case), or "" when not
	// set. Empty is resolved at generation (internal/compose cacheEngine): the
	// engine of the project lock's redis pin, else the mode default.
	Engine string `env:"REDIS_ENGINE"`
}

RedisConfig holds Redis cache/queue configuration.

type RemoteSchema

type RemoteSchema struct {
	Index   int
	Name    string
	URL     string
	Headers string // key:val,key:val
}

RemoteSchema represents a Hasura Remote Schema configuration.

type SearchConfig

type SearchConfig struct {
	Enabled     bool   `env:"SEARCH_ENABLED"`
	Engine      string `env:"SEARCH_ENGINE"`  // meilisearch, typesense, etc.
	Port        int    `env:"SEARCH_PORT"`    // auto from provider
	APIKey      string `env:"SEARCH_API_KEY"` // auto-generated if unset
	Route       string `env:"SEARCH_ROUTE"`   // search.{BD}
	IndexPrefix string `env:"SEARCH_INDEX_PREFIX"`
	AutoIndex   bool   `env:"SEARCH_AUTO_INDEX"` // true
	Language    string `env:"SEARCH_LANGUAGE"`   // en

	// Provider-specific (only populated for active provider)
	MeiliSearch   MeiliSearchConfig
	Typesense     TypesenseConfig
	Elasticsearch ElasticsearchConfig
}

SearchConfig holds search engine configuration (provider-agnostic).

type SecretsConfig added in v1.0.6

type SecretsConfig struct {
	AgeKeyPath   string `env:"SECRETS_AGE_KEY_PATH"` // ~/.config/nself/age-key.txt
	DeployAgeKey string `env:"DEPLOY_AGE_KEY"`       // raw age private key for CI/CD
}

SecretsConfig holds secrets management configuration.

type Setter added in v1.4.13

type Setter struct {
	// File is the cascade file name relative to the project directory.
	File string
	// Value is the value the file sets, in clear.
	Value string
}

Setter is one existing cascade file that sets the key.

type TelemetryPreference added in v1.0.11

type TelemetryPreference struct {
	// Enabled is the stored preference from config.toml.
	// This value is overridden to false when NSELF_TELEMETRY_OPT_OUT=1 is set.
	Enabled bool

	// Source describes where the value came from: "env", "config", or "default".
	Source string
}

TelemetryPreference describes the user's telemetry opt-in/out state.

func GetTelemetryPreference added in v1.0.11

func GetTelemetryPreference() TelemetryPreference

GetTelemetryPreference returns the effective telemetry preference. NSELF_TELEMETRY_OPT_OUT=1 beats the config file, which beats the default.

type TenantConfig added in v1.0.6

type TenantConfig struct {
	DefaultPlan             string `env:"TENANT_DEFAULT_PLAN"`               // basic
	DestroyBackupRetainDays int    `env:"TENANT_DESTROY_BACKUP_RETAIN_DAYS"` // 90
	StripeSecretKey         string `env:"STRIPE_SECRET_KEY"`
	StripeWebhookSecret     string `env:"STRIPE_WEBHOOK_SECRET"`
	StripeAPIVersion        string `env:"STRIPE_API_VERSION"` // 2024-04-10
}

TenantConfig holds multi-tenancy and billing configuration.

type TypesenseConfig

type TypesenseConfig struct {
	Version           string `env:"TYPESENSE_VERSION"` // 27.1
	APIKey            string `env:"TYPESENSE_API_KEY"`
	EnableCORS        bool   `env:"TYPESENSE_ENABLE_CORS"`
	LogLevel          string `env:"TYPESENSE_LOG_LEVEL"`
	NumMemoryShards   int    `env:"TYPESENSE_NUM_MEMORY_SHARDS"`
	SnapshotIntervalS int    `env:"TYPESENSE_SNAPSHOT_INTERVAL_SECONDS"`
}

TypesenseConfig holds Typesense-specific configuration.

type ValidatorFunc

type ValidatorFunc struct {
	Name string
	Fn   func(*Config) error
}

ValidatorFunc pairs a human-readable name with a validation function. The name is used in error messages and test assertions.

type ValidatorResult

type ValidatorResult struct {
	Name string
	Err  error
}

ValidatorResult pairs a validator name with its outcome.

func RunAllWithResults

func RunAllWithResults(cfg *Config) []ValidatorResult

RunAllWithResults executes every registered validator and returns one ValidatorResult per validator, preserving the original name and error. Unlike RunAll, no error is returned — the caller inspects results directly.

Directories

Path Synopsis
Package bindings declares, once, which command flags supply which configuration keys (contract:config.env, Constitution 6.1).
Package bindings declares, once, which command flags supply which configuration keys (contract:config.env, Constitution 6.1).

Jump to

Keyboard shortcuts

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