// Package config reads the process environment once, at startup, so that a // missing setting fails the boot rather than the first request that needs it. package config import ( "fmt" "os" "strings" ) type Config struct { DatabaseURL string // BaseURL is the public origin. It decides whether the session cookie is // marked Secure and what host magic links point at, so it must be the real // external URL in production, not the listen address. BaseURL string Addr string SessionKey []byte AdminEmails []string // WebDir holds the built frontend: index.html is never served from here, // only the hashed assets and the Vite manifest the shell reads. WebDir string Dev bool } func Load() (*Config, error) { c := &Config{ DatabaseURL: os.Getenv("DATABASE_URL"), BaseURL: envOr("BASE_URL", "http://localhost:3000"), Addr: envOr("ADDR", ":3000"), WebDir: envOr("WEB_DIR", "./web"), Dev: os.Getenv("APP_ENV") == "development", } if c.DatabaseURL == "" { return nil, fmt.Errorf("DATABASE_URL environment variable is missing") } secret := os.Getenv("SESSION_SECRET") if secret == "" { return nil, fmt.Errorf("SESSION_SECRET environment variable is missing (openssl rand -base64 32)") } c.SessionKey = []byte(secret) for _, e := range strings.Split(os.Getenv("ADMIN_EMAILS"), ",") { if e = strings.ToLower(strings.TrimSpace(e)); e != "" { c.AdminEmails = append(c.AdminEmails, e) } } c.BaseURL = strings.TrimRight(c.BaseURL, "/") return c, nil } // Secure reports whether cookies may carry the Secure flag. Setting it on a // plain-HTTP origin would make the browser drop the cookie silently, which // looks exactly like a broken login. func (c *Config) Secure() bool { return strings.HasPrefix(c.BaseURL, "https://") } func envOr(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback }