77 "path/filepath"
88 "strings"
99
10+ "github.com/joho/godotenv"
1011 "github.com/uptrace/bun"
1112 "github.com/uptrace/bun/migrate"
1213
@@ -839,6 +840,11 @@ func (p *DatabasePlugin) getDatabaseConnection(ctx cli.CommandContext) (*bun.DB,
839840// This provides automatic environment variable expansion, file merging, and proper
840841// namespace support for both 'database' and 'extensions.database' keys.
841842func (p * DatabasePlugin ) loadDatabaseConfig (dbName , appName string ) (database.DatabaseConfig , error ) {
843+ // CRITICAL: Load .env files BEFORE creating ConfigManager
844+ // ConfigManager expands environment variables when reading config files,
845+ // so .env vars must be in the environment at that point
846+ p .loadEnvFiles ()
847+
842848 // Create a temporary Forge app to access ConfigManager
843849 // This gives us all the benefits: file discovery, merging, env var expansion, etc.
844850 app := forge .NewApp (forge.AppConfig {
@@ -848,6 +854,7 @@ func (p *DatabasePlugin) loadDatabaseConfig(dbName, appName string) (database.Da
848854 // Enable config auto-discovery to find config.yaml and config.local.yaml
849855 EnableConfigAutoDiscovery : true ,
850856 ConfigSearchPaths : []string {p .config .RootDir , filepath .Join (p .config .RootDir , "config" )},
857+ Logger : forge .NewNoopLogger (),
851858 })
852859
853860 cm := app .Config ()
@@ -923,6 +930,55 @@ func getDatabaseNames(databases []database.DatabaseConfig) []string {
923930 return names
924931}
925932
933+ // loadEnvFiles loads environment variables from .env files.
934+ // Loads in order of priority (later files override earlier ones):
935+ // 1. .env (base configuration)
936+ // 2. .env.local (local overrides, gitignored)
937+ // 3. .env.{environment} (environment-specific)
938+ // 4. .env.{environment}.local (environment-specific local overrides)
939+ //
940+ // This follows the standard dotenv convention used by many frameworks.
941+ func (p * DatabasePlugin ) loadEnvFiles () {
942+ if p .config == nil {
943+ return
944+ }
945+
946+ // Determine environment (default to development)
947+ env := os .Getenv ("FORGE_ENV" )
948+ if env == "" {
949+ env = os .Getenv ("GO_ENV" )
950+ }
951+ if env == "" {
952+ env = "development"
953+ }
954+
955+ // Files to load in priority order (earlier = lower priority)
956+ envFiles := []string {
957+ filepath .Join (p .config .RootDir , ".env" ),
958+ filepath .Join (p .config .RootDir , ".env.local" ),
959+ }
960+
961+ // Add environment-specific files
962+ if env != "" {
963+ envFiles = append (envFiles ,
964+ filepath .Join (p .config .RootDir , fmt .Sprintf (".env.%s" , env )),
965+ filepath .Join (p .config .RootDir , fmt .Sprintf (".env.%s.local" , env )),
966+ )
967+ }
968+
969+ // Load each file that exists
970+ for _ , envFile := range envFiles {
971+ if _ , err := os .Stat (envFile ); err == nil {
972+ // Load without overriding existing env vars (godotenv.Load would override)
973+ // We use Overload to ensure later files take precedence
974+ if err := godotenv .Overload (envFile ); err != nil {
975+ // Silently continue - .env files are optional
976+ continue
977+ }
978+ }
979+ }
980+ }
981+
926982// cliLoggerAdapter adapts CLI context to database.Logger interface.
927983type cliLoggerAdapter struct {
928984 ctx cli.CommandContext
0 commit comments