// loads environment-based configuration. package config import ( "fmt" "os" "strconv" "strings" ) const ( defaultPort = 3001 defaultDBPort = 5432 defaultPollIntervalMS = 60000 minPollIntervalMS = 1000 defaultDigestIntervalHours = 24 ) type Config struct { Port int APIBasePath string DatabaseURL string DBHost string DBPort int DBUser string DBPassword string DBName string FirebaseProjectID string EthRPCURL string PollIntervalMS int NodeEnv string ResendAPIKey string EmailFrom string SupportInboxEmail string DigestIntervalHours int StripeSecretKey string StripeWebhookSecret string StripePriceIDPremium string StripePriceIDPro string StripePriceIDPremiumAnnual string StripePriceIDProAnnual string StripePublishableKey string FrontendURL string } // Load reads configuration from environment variables and returns a Config. func Load() (*Config, error) { cfg := &Config{ Port: getEnvInt("PORT", defaultPort), APIBasePath: getEnv("API_BASE_PATH", "/v1"), DatabaseURL: os.Getenv("DATABASE_URL"), DBHost: getEnv("DB_HOST", "localhost"), DBPort: getEnvInt("DB_PORT", defaultDBPort), DBUser: os.Getenv("DB_USER"), DBPassword: os.Getenv("DB_PASSWORD"), DBName: os.Getenv("DB_NAME"), FirebaseProjectID: os.Getenv("FIREBASE_PROJECT_ID"), EthRPCURL: os.Getenv("ETH_RPC_URL"), PollIntervalMS: getEnvInt("POLL_INTERVAL_MS", defaultPollIntervalMS), NodeEnv: getEnv("NODE_ENV", "development"), ResendAPIKey: os.Getenv("RESEND_API_KEY"), EmailFrom: getEnv("EMAIL_FROM", "Koin Ping "), SupportInboxEmail: getEnv("SUPPORT_INBOX_EMAIL", "sj@sjdev.co"), DigestIntervalHours: getEnvInt("DIGEST_INTERVAL_HOURS", defaultDigestIntervalHours), StripeSecretKey: os.Getenv("STRIPE_SECRET_KEY"), StripeWebhookSecret: os.Getenv("STRIPE_WEBHOOK_SECRET"), StripePriceIDPremium: os.Getenv("STRIPE_PRICE_ID_PREMIUM"), StripePriceIDPro: os.Getenv("STRIPE_PRICE_ID_PRO"), StripePriceIDPremiumAnnual: os.Getenv("STRIPE_PRICE_ID_PREMIUM_ANNUAL"), StripePriceIDProAnnual: os.Getenv("STRIPE_PRICE_ID_PRO_ANNUAL"), StripePublishableKey: os.Getenv("STRIPE_PUBLISHABLE_KEY"), FrontendURL: getEnv("FRONTEND_URL", "http://localhost:3000"), } if cfg.PollIntervalMS < minPollIntervalMS { return nil, fmt.Errorf("POLL_INTERVAL_MS must be >= 1000, got %d", cfg.PollIntervalMS) //nolint:err113 } return cfg, nil } func (c *Config) DSN() string { if c.DatabaseURL != "" { // Append sslmode=disable if not already specified. if !strings.Contains(c.DatabaseURL, "sslmode=") { sep := "?" if strings.Contains(c.DatabaseURL, "?") { sep = "&" } return c.DatabaseURL + sep + "sslmode=disable" } return c.DatabaseURL } return fmt.Sprintf( "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", c.DBHost, c.DBPort, c.DBUser, c.DBPassword, c.DBName, ) } // TierForPriceID maps a Stripe price ID back to the corresponding // subscription tier. Returns empty string if the price is unrecognised. func (c *Config) TierForPriceID(priceID string) string { if priceID == "" { return "" } switch priceID { case c.StripePriceIDPremium, c.StripePriceIDPremiumAnnual: return "premium" case c.StripePriceIDPro, c.StripePriceIDProAnnual: return "pro" default: return "" } } func getEnv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } func getEnvInt(key string, fallback int) int { if v := os.Getenv(key); v != "" { if n, err := strconv.Atoi(v); err == nil { return n } } return fallback }