Repository: dbos-inc/dbos-transact-ts
Pull request #849: Use connection strings to configure user DB clients
Author: login: maxdml
Created: 2025-04-10T01:27:17Z | Merged: 2025-04-17T15:40:48Z

This PR crafts connection strings to create user DB clients.

The key changes are:
- All ORMs use the connection string for config
- Except: connect_timeout and pool size, which must be given separately to be considered
- When the config provides a database url, pass it as is (except for debug user/password/host/port overrides if needed)
- When no url is provided, rebuild one from provided parameters and/or defaults


Notes:
- During DBOS Cloud overrides, craft a connection string from the config file. no-verify for linked databases (no ssl_ca), full-verify otherwise
- When a user provides a connection string, we do a little backfill of the poolConfig params for other parts of the code to keep working
- This PR enforces user / password / hostname / dbname are present in a connection string


Others:
- Remove dangling "local suffix" references



Diff:
diff --git a/dbos-config.schema.json b/dbos-config.schema.json
index 6be4f198f..b75b97f5e 100644
--- a/dbos-config.schema.json
+++ b/dbos-config.schema.json
@@ -60,10 +60,6 @@
           "type": "string",
           "description": "If using SSL/TLS to securely connect to a database, path to an SSL root certificate file"
         },
-        "local_suffix": {
-          "type": "boolean",
-          "description": "Whether to suffix app_db_name with '_local'. Set to true when doing local development using a DBOS Cloud database."
-        },
         "app_db_client": {
           "type": "string",
           "description": "Specify the database client to use to connect to the application database",
diff --git a/packages/create/templates/dbos-drizzle/drizzle.config.ts b/packages/create/templates/dbos-drizzle/drizzle.config.ts
index 905dc4139..0fce7e994 100644
--- a/packages/create/templates/dbos-drizzle/drizzle.config.ts
+++ b/packages/create/templates/dbos-drizzle/drizzle.config.ts
@@ -9,11 +9,6 @@ export default defineConfig({
   out: './drizzle',
   dialect: 'postgresql',
   dbCredentials: {
-    host: dbosConfig.poolConfig.host,
-    port: dbosConfig.poolConfig.port,
-    user: dbosConfig.poolConfig.user,
-    password: dbosConfig.poolConfig.password,
-    database: dbosConfig.poolConfig.database,
-    ssl: dbosConfig.poolConfig.ssl,
+    url: dbosConfig.poolConfig.connectionString,
   },
 });
diff --git a/packages/create/templates/dbos-knex/knexfile.js b/packages/create/templates/dbos-knex/knexfile.js
index b0ce150ab..59d6f4691 100644
--- a/packages/create/templates/dbos-knex/knexfile.js
+++ b/packages/create/templates/dbos-knex/knexfile.js
@@ -4,14 +4,7 @@ const [dbosConfig] = parseConfigFile();
 
 const config = {
   client: 'pg',
-  connection: {
-    host: dbosConfig.poolConfig.host,
-    port: dbosConfig.poolConfig.port,
-    user: dbosConfig.poolConfig.user,
-    password: dbosConfig.poolConfig.password,
-    database: dbosConfig.poolConfig.database,
-    ssl: dbosConfig.poolConfig.ssl,
-  },
+  connection: dbosConfig.poolConfig.connectionString,
   migrations: {
     directory: './migrations',
   },
diff --git a/packages/create/templates/dbos-typeorm/datasource.ts b/packages/create/templates/dbos-typeorm/datasource.ts
index 4917ff3ad..29bc98ad0 100644
--- a/packages/create/templates/dbos-typeorm/datasource.ts
+++ b/packages/create/templates/dbos-typeorm/datasource.ts
@@ -6,12 +6,7 @@ const [dbosConfig] = parseConfigFile();
 
 const AppDataSource = new DataSource({
   type: 'postgres',
-  host: dbosConfig.poolConfig!.host,
-  port: dbosConfig.poolConfig!.port,
-  username: dbosConfig.poolConfig!.user,
-  password: dbosConfig.poolConfig!.password as string,
-  database: dbosConfig.poolConfig!.database,
-  ssl: dbosConfig.poolConfig!.ssl as TlsOptions,
+  url: dbosConfig.poolConfig.connectionString,
   entities: ['dist/entities/*.js'],
   migrations: ['dist/migrations/*.js'],
 });
diff --git a/src/client.ts b/src/client.ts
index b69f8a66f..fee04d784 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -3,7 +3,7 @@ import { PostgresSystemDatabase, SystemDatabase, WorkflowStatusInternal } from '
 import { GlobalLogger as Logger } from './telemetry/logs';
 import { v4 as uuidv4 } from 'uuid';
 import { RetrievedHandle, StatusString, WorkflowHandle } from './workflow';
-import { parseDbString, parseSSLConfig } from './dbos-runtime/config';
+import { constructPoolConfig } from './dbos-runtime/config';
 import { DBOSJSON } from './utils';
 
 /**
@@ -43,19 +43,14 @@ export class DBOSClient {
   private readonly systemDatabase: SystemDatabase;
 
   private constructor(databaseUrl: string, systemDatabase?: string) {
-    const dbConfig = parseDbString(databaseUrl);
+    const poolConfig: PoolConfig = constructPoolConfig({
+      database: {},
+      database_url: databaseUrl,
+      application: {},
+      env: {},
+    });
 
-    const poolConfig: PoolConfig = {
-      host: dbConfig.hostname,
-      port: dbConfig.port,
-      user: dbConfig.username,
-      password: dbConfig.password,
-      database: dbConfig.app_db_name,
-      ssl: parseSSLConfig(dbConfig),
-      connectionTimeoutMillis: dbConfig.connectionTimeoutMillis,
-    };
-
-    systemDatabase ??= `${dbConfig.app_db_name}_dbos_sys`;
+    systemDatabase ??= `${poolConfig.database}_dbos_sys`;
 
     this.logger = new Logger();
     this.systemDatabase = new PostgresSystemDatabase(poolConfig, systemDatabase, this.logger);
diff --git a/src/dbos-executor.ts b/src/dbos-executor.ts
index 7daadc79e..65b3275c3 100644
--- a/src/dbos-executor.ts
+++ b/src/dbos-executor.ts
@@ -331,24 +331,12 @@ export class DBOSExecutor implements DBOSExecutorContext {
       // TODO: make Prisma work with debugger proxy.
       // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-require-imports
       const { PrismaClient } = require(path.join(process.cwd(), 'node_modules', '@prisma', 'client')); // Find the prisma client in the node_modules of the current project
-      let dbUrl = `postgresql://${userDBConfig.user}:${userDBConfig.password as string}@${userDBConfig.host}:${userDBConfig.port}/${userDBConfig.database}`;
-      const queryParams: Record<string, string | number> = {};
-      if (userDBConfig.connectionTimeoutMillis) {
-        queryParams['connect_timeout'] = userDBConfig.connectionTimeoutMillis;
-      }
-      if (userDBConfig.max) {
-        queryParams['connection_limit'] = String(userDBConfig.max);
-      }
-      const queryString = new URLSearchParams(queryParams as Record<string, string>).toString();
-      if (queryString) {
-        dbUrl += `?${queryString}`;
-      }
       this.userDatabase = new PrismaUserDatabase(
         // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
         new PrismaClient({
           datasources: {
             db: {
-              url: dbUrl,
+              url: userDBConfig.connectionString,
             },
           },
         }),
@@ -361,16 +349,11 @@ export class DBOSExecutor implements DBOSExecutorContext {
         this.userDatabase = new TypeORMDatabase(
           // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
           new DataSourceExports.DataSource({
-            type: 'postgres', // perhaps should move to config file
-            host: userDBConfig.host,
-            port: userDBConfig.port,
-            username: userDBConfig.user,
-            password: userDBConfig.password,
-            database: userDBConfig.database,
+            type: 'postgres',
+            url: userDBConfig.connectionString,
+            connectTimeoutMS: userDBConfig.connectionTimeoutMillis,
             entities: this.typeormEntities,
-            ssl: userDBConfig.ssl,
             poolSize: userDBConfig.max,
-            connectTimeoutMS: userDBConfig.connectionTimeoutMillis,
           }),
         );
       } catch (s) {
@@ -382,13 +365,8 @@ export class DBOSExecutor implements DBOSExecutorContext {
       const knexConfig: Knex.Config = {
         client: 'postgres',
         connection: {
-          host: userDBConfig.host,
-          port: userDBConfig.port,
-          user: userDBConfig.user,
-          password: userDBConfig.password,
-          database: userDBConfig.database,
-          ssl: userDBConfig.ssl,
-          connectTimeout: userDBConfig.connectionTimeoutMillis,
+          connectionString: userDBConfig.connectionString,
+          connectionTimeoutMillis: userDBConfig.connectionTimeoutMillis,
         },
         pool: {
           min: 0,
diff --git a/src/dbos-runtime/config.ts b/src/dbos-runtime/config.ts
index 3a7207547..3d51076e9 100644
--- a/src/dbos-runtime/config.ts
+++ b/src/dbos-runtime/config.ts
@@ -8,10 +8,8 @@ import { UserDatabaseName } from '../user_database';
 import { TelemetryConfig } from '../telemetry';
 import { writeFileSync } from 'fs';
 import Ajv, { ValidateFunction } from 'ajv';
-import { parse } from 'pg-connection-string';
 import path from 'path';
 import validator from 'validator';
-import fs from 'fs';
 import { GlobalLogger } from '../telemetry/logs';
 import dbosConfigSchema from '../../dbos-config.schema.json';
 
@@ -31,7 +29,6 @@ export interface DBConfig {
   app_db_client?: UserDatabaseName;
   migrate?: string[];
   rollback?: string[];
-  local_suffix?: boolean;
 }
 
 export interface ConfigFile {
@@ -130,7 +127,7 @@ function retrieveApplicationName(configFile: ConfigFile): string {
   if (appName !== undefined) {
     return appName;
   }
-  const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json')).toString()) as {
+  const packageJson = JSON.parse(readFileSync(path.join(process.cwd(), 'package.json')).toString()) as {
     name: string;
   };
   appName = packageJson.name;
@@ -142,43 +139,173 @@ function retrieveApplicationName(configFile: ConfigFile): string {
   return appName;
 }
 
+/**
+ * Build a PoolConfig object.
+ *
+ * If configFile.database_url is provided, set it directly in poolConfig.connectionString and backfill the other poolConfig options.
+ * Backfilling allows the rest of the code to access database parameters easily.
+ * We configure the ORMs with the connection string
+ * We still need to extract pool size and connectionTimeoutMillis from the config file and give them manually to the ORMs.
+ *
+ * If configFile.database_url is not provided, we build the connection string from the configFile.database object.
+ *
+ * In debug mode, apply overrides from DBOS_DBHOST, DBOS_DBPORT, DBOS_DBUSER, and DBOS_DBPASSWORD.
+ *
+ * Default configuration:
+ * - Hostname: localhost
+ * - Port: 5432
+ * - Username: postgres
+ * - Password: $PGPASSWORD
+ * - Database name: transformed application name. The name is either the one provided in the config file or the one found in package.json.
+ *
+ * @param configFile - The configuration to be used.
+ * @param cliOptions - Optional CLI options.
+ * @returns PoolConfig - The constructed PoolConfig object.
+ */
 export function constructPoolConfig(configFile: ConfigFile, cliOptions?: ParseOptions): PoolConfig {
-  // Load database connection parameters. If they're not in dbos-config.yaml, load from .dbos/db_connection. Else, use defaults.
+  // FIXME: this is not a good place to set the app name
+  const appName = retrieveApplicationName(configFile);
+  if (globalParams.appName === '') {
+    globalParams.appName = appName;
+  }
+
+  const isDebugMode = process.env.DBOS_DEBUG_WORKFLOW_ID !== undefined;
+
   if (!cliOptions?.silent) {
     const logger = new GlobalLogger();
-    if (process.env.DBOS_DBHOST) {
+    if (isDebugMode) {
       logger.info('Loading database connection parameters from debug environment variables');
+    } else if (configFile.database_url) {
+      logger.info('Loading database connection parameters from database_url');
     } else if (configFile.database.hostname) {
       logger.info('Loading database connection parameters from dbos-config.yaml');
     } else {
       logger.info('Using default database connection parameters');
     }
   }
-  configFile.database.hostname = process.env.DBOS_DBHOST || configFile.database.hostname || 'localhost';
-  const dbos_dbport = process.env.DBOS_DBPORT ? parseInt(process.env.DBOS_DBPORT) : undefined;
-  configFile.database.port = dbos_dbport || configFile.database.port || 5432;
-  configFile.database.username = process.env.DBOS_DBUSER || configFile.database.username || 'postgres';
-  configFile.database.password =
-    process.env.DBOS_DBPASSWORD || configFile.database.password || process.env.PGPASSWORD || 'dbos';
-
-  let databaseName: string | undefined = configFile.database.app_db_name;
-  const appName = retrieveApplicationName(configFile);
-  if (globalParams.appName === '') {
-    globalParams.appName = appName;
-  }
-  if (databaseName === undefined) {
-    databaseName = appName.toLowerCase().replaceAll('-', '_').replaceAll(' ', '_');
-    if (databaseName.match(/^\d/)) {
-      databaseName = '_' + databaseName; // Append an underscore if the name starts with a digit
+
+  let connectionString = undefined;
+  let databaseName = undefined;
+  let connectionTimeoutMillis = 3000;
+
+  // If a database_url is found, parse it to backfill the poolConfig
+  if (configFile.database_url) {
+    const url = new URL(configFile.database_url);
+    // If in debug mode, apply the debug overrides
+    if (isDebugMode) {
+      if (process.env.DBOS_DBHOST) {
+        url.hostname = process.env.DBOS_DBHOST;
+      }
+      if (process.env.DBOS_DBPORT) {
+        url.port = process.env.DBOS_DBPORT;
+      }
+      if (process.env.DBOS_DBUSER) {
+        url.username = process.env.DBOS_DBUSER;
+      }
+      if (process.env.DBOS_DBPASSWORD) {
+        url.password = process.env.DBOS_DBPASSWORD;
+      }
+      configFile.database_url = url.toString();
+    }
+    connectionString = configFile.database_url;
+    databaseName = url.pathname.substring(1);
+
+    const queryParams = url.searchParams;
+    if (queryParams.has('connect_timeout')) {
+      connectionTimeoutMillis = parseInt(queryParams.get('connect_timeout')!, 10) * 1000;
+    }
+
+    // Validate required fields
+    const missingFields: string[] = [];
+    if (!url.username) missingFields.push('username');
+    if (!url.password) missingFields.push('password');
+    if (!url.hostname) missingFields.push('hostname');
+    if (!databaseName) missingFields.push('database name');
+
+    if (missingFields.length > 0) {
+      throw new Error(`Invalid database URL: missing required field(s): ${missingFields.join(', ')}`);
+    }
+
+    // Backfill the poolConfig
+    configFile.database = {
+      ...configFile.database,
+      hostname: url.hostname,
+      port: url.port ? parseInt(url.port, 10) : 5432,
+      username: url.username,
+      password: url.password,
+      app_db_name: databaseName,
+    };
+
+    if (!cliOptions?.silent) {
+      const logger = new GlobalLogger();
+      let logConnectionString = `postgresql://${configFile.database.username}:***@${configFile.database.hostname}:${configFile.database.port}/${databaseName}`;
+      const logQueryParamsArray = Object.entries(url.searchParams.entries()).map(([key, value]) => `${key}=${value}`);
+      if (logQueryParamsArray.length > 0) {
+        logConnectionString += `?${logQueryParamsArray.join('&')}`;
+      }
+      logger.info(`Using database connection string: ${logConnectionString}`);
+    }
+  } else {
+    // Else, build the config from configFile.database and apply overrides
+    configFile.database.hostname = process.env.DBOS_DBHOST || configFile.database.hostname || 'localhost';
+    const dbos_dbport = process.env.DBOS_DBPORT ? parseInt(process.env.DBOS_DBPORT) : undefined;
+    configFile.database.port = dbos_dbport || configFile.database.port || 5432;
+    configFile.database.username = process.env.DBOS_DBUSER || configFile.database.username || 'postgres';
+    configFile.database.password =
+      process.env.DBOS_DBPASSWORD || configFile.database.password || process.env.PGPASSWORD || 'dbos';
+    connectionTimeoutMillis = configFile.database.connectionTimeoutMillis || 3000;
+
+    databaseName = configFile.database.app_db_name;
+    // Construct the database name from the application name, if needed
+    if (databaseName === undefined) {
+      databaseName = appName.toLowerCase().replaceAll('-', '_').replaceAll(' ', '_');
+      if (databaseName.match(/^\d/)) {
+        databaseName = '_' + databaseName; // Append an underscore if the name starts with a digit
+      }
+    }
+
+    connectionString = `postgresql://${configFile.database.username}:${configFile.database.password}@${configFile.database.hostname}:${configFile.database.port}/${databaseName}`;
+
+    // Build connection string query parameters
+    const queryParams: string[] = [];
+
+    queryParams.push(`connect_timeout=${connectionTimeoutMillis / 1000}`);
+
+    // SSL configuration
+    const ssl = parseSSLConfig(configFile.database);
+    if (ssl === false) {
+      queryParams.push(`sslmode=disable`);
+    } else if (ssl && 'ca' in ssl) {
+      queryParams.push(`sslmode=verify-full`);
+      queryParams.push(`sslrootcert=${configFile.database.ssl_ca}`);
+    } else {
+      queryParams.push(`sslmode=no-verify`);
+    }
+
+    if (queryParams.length > 0) {
+      connectionString += `?${queryParams.join('&')}`;
+    }
+
+    if (!cliOptions?.silent) {
+      const logger = new GlobalLogger();
+      let logConnectionString = `postgresql://${configFile.database.username}:***@${configFile.database.hostname}:${configFile.database.port}/${databaseName}`;
+      if (queryParams.length > 0) {
+        logConnectionString += `?${queryParams.join('&')}`;
+      }
+      logger.info(`Using database connection string: ${logConnectionString}`);
     }
   }
+
+  // Build the final poolConfig
   const poolConfig: PoolConfig = {
+    connectionString,
+    connectionTimeoutMillis,
     host: configFile.database.hostname,
     port: configFile.database.port,
     user: configFile.database.username,
     password: configFile.database.password,
-    connectionTimeoutMillis: configFile.database.connectionTimeoutMillis || 3000,
     database: databaseName,
+    max: cliOptions?.userDbPoolSize || 20,
   };
 
   if (!poolConfig.database) {
@@ -186,8 +313,6 @@ export function constructPoolConfig(configFile: ConfigFile, cliOptions?: ParseOp
       `DBOS configuration (${dbosConfigFilePath}) does not contain application database name`,
     );
   }
-
-  poolConfig.ssl = parseSSLConfig(configFile.database);
   return poolConfig;
 }
 
@@ -233,6 +358,7 @@ export interface ParseOptions {
   appVersion?: string | boolean;
   silent?: boolean;
   forceConsole?: boolean;
+  userDbPoolSize?: number;
 }
 
 /*
@@ -249,12 +375,6 @@ export function parseConfigFile(cliOptions?: ParseOptions): [DBOSConfigInternal,
     throw new DBOSInitializationError(`DBOS configuration file ${configFilePath} is empty`);
   }
 
-  if (configFile.database.local_suffix === true && configFile.database.hostname === 'localhost') {
-    throw new DBOSInitializationError(
-      `Invalid configuration (${configFilePath}): local_suffix may only be true when connecting to remote databases, not to localhost`,
-    );
-  }
-
   if (configFile.language && configFile.language !== 'node') {
     throw new DBOSInitializationError(`${configFilePath} specifies invalid language ${configFile.language}`);
   }
@@ -263,9 +383,6 @@ export function parseConfigFile(cliOptions?: ParseOptions): [DBOSConfigInternal,
   /* Handle user database config */
   /*******************************/
 
-  if (configFile.database_url) {
-    configFile.database = parseDbString(configFile.database_url);
-  }
   const poolConfig = constructPoolConfig(configFile, cliOptions);
 
   if (!isValidDBname(poolConfig.database!)) {
@@ -386,21 +503,16 @@ export function translatePublicDBOSconfig(
   }
 
   // Resolve database configuration
-  let dburlConfig: DBConfig = {};
-  if (config.databaseUrl) {
-    dburlConfig = parseDbString(config.databaseUrl);
-  }
-
   const poolConfig = constructPoolConfig(
     {
       name: appName,
-      database: dburlConfig,
+      database: {},
+      database_url: config.databaseUrl,
       application: {},
       env: {},
     },
-    { silent: true },
+    { silent: true, userDbPoolSize: config.userDbPoolSize },
   );
-  poolConfig.max = config.userDbPoolSize || 20;
 
   const translatedConfig: DBOSConfigInternal = {
     name: appName,
@@ -432,26 +544,6 @@ export function translatePublicDBOSconfig(
   return [translatedConfig, runtimeConfig];
 }
 
-export function parseDbString(dbString: string): DBConfig {
-  const parsed = parse(dbString);
-  // pg-connection-string parse query parameters in a way that doesn't match ConnectionOptions (parse return type)
-  // so we resort to the more generic URL() to parse query parameters
-  const url = new URL(dbString);
-  const queryParams = Object.fromEntries(url.searchParams.entries());
-  return {
-    hostname: parsed.host || undefined,
-    port: parsed.port ? parseInt(parsed.port, 10) : undefined,
-    username: parsed.user || undefined,
-    password: parsed.password || undefined,
-    app_db_name: parsed.database || undefined,
-    ssl: 'sslmode' in parsed && (parsed.sslmode === 'require' || parsed.sslmode === 'verify-full'),
-    ssl_ca: queryParams['sslrootcert'] || undefined,
-    connectionTimeoutMillis: queryParams['connect_timeout']
-      ? parseInt(queryParams['connect_timeout'], 10) * 1000
-      : undefined,
-  };
-}
-
 export function overwrite_config(
   providedDBOSConfig: DBOSConfigInternal,
   providedRuntimeConfig: DBOSRuntimeConfig,
@@ -475,6 +567,11 @@ export function overwrite_config(
 
   const appName = configFile!.name || providedDBOSConfig.name;
 
+  if (configFile.database.ssl_ca) {
+    configFile.database_url = `postgresql://${configFile.database.username}:${configFile.database.password}@${configFile.database.hostname}:${configFile.database.port}/${configFile.database.app_db_name}?connect_timeout=3&sslmode=verify-full&sslrootcert=${configFile.database.ssl_ca}`;
+  } else {
+    configFile.database_url = `postgresql://${configFile.database.username}:${configFile.database.password}@${configFile.database.hostname}:${configFile.database.port}/${configFile.database.app_db_name}?connect_timeout=3&sslmode=no-verify`;
+  }
   const poolConfig = constructPoolConfig(configFile!);
 
   if (!providedDBOSConfig.telemetry.OTLPExporter) {
@@ -499,7 +596,7 @@ export function overwrite_config(
     name: appName,
     poolConfig: poolConfig,
     telemetry: providedDBOSConfig.telemetry,
-    system_database: configFile!.database.sys_db_name || poolConfig.database + '_dbos_sys', // Unexepected, but possible
+    system_database: configFile!.database.sys_db_name || poolConfig.database + '_dbos_sys', // Unexpected, but possible
   };
 
   const overwriteDBOSRuntimeConfig = {
diff --git a/src/dbos-runtime/migrate.ts b/src/dbos-runtime/migrate.ts
index 04352be18..6b04cb231 100644
--- a/src/dbos-runtime/migrate.ts
+++ b/src/dbos-runtime/migrate.ts
@@ -79,8 +79,11 @@ export function rollbackMigration(_config: DBOSConfigInternal, configFile: Confi
 async function createDBOSTables(systemDbName: string, userPoolConfig: PoolConfig) {
   const logger = new GlobalLogger();
 
-  const systemPoolConfig = { ...userPoolConfig };
-  systemPoolConfig.database = systemDbName;
+  const sysDbConnectionString = new URL(userPoolConfig.connectionString!);
+  sysDbConnectionString.pathname = `/${systemDbName}`;
+  const systemPoolConfig: PoolConfig = {
+    connectionString: sysDbConnectionString.toString(),
+  };
 
   const pgUserClient = new Client(userPoolConfig);
   await pgUserClient.connect();
@@ -109,10 +112,10 @@ async function createDBOSTables(systemDbName: string, userPoolConfig: PoolConfig
 
   // Create the DBOS system database.
   const dbExists = await pgUserClient.query<ExistenceCheck>(
-    `SELECT EXISTS (SELECT FROM pg_database WHERE datname = '${systemPoolConfig.database}')`,
+    `SELECT EXISTS (SELECT FROM pg_database WHERE datname = '${systemDbName}')`,
   );
   if (!dbExists.rows[0].exists) {
-    await pgUserClient.query(`CREATE DATABASE ${systemPoolConfig.database}`);
+    await pgUserClient.query(`CREATE DATABASE ${systemDbName}`);
   }
 
   // Load the DBOS system schema.
diff --git a/src/system_database.ts b/src/system_database.ts
index 65797a6b5..ba5782bc2 100644
--- a/src/system_database.ts
+++ b/src/system_database.ts
@@ -230,19 +230,21 @@ export class PostgresSystemDatabase implements SystemDatabase {
   readonly notificationsMap: Record<string, () => void> = {};
   readonly workflowEventsMap: Record<string, () => void> = {};
 
-  static readonly connectionTimeoutMillis = 10000; // 10 second timeout
-
   constructor(
     readonly pgPoolConfig: PoolConfig,
     readonly systemDatabaseName: string,
     readonly logger: Logger,
     readonly sysDbPoolSize?: number,
   ) {
-    this.systemPoolConfig = { ...pgPoolConfig };
-    this.systemPoolConfig.database = systemDatabaseName;
-    this.systemPoolConfig.connectionTimeoutMillis = PostgresSystemDatabase.connectionTimeoutMillis;
-    // This sets the application_name column in pg_stat_activity
-    this.systemPoolConfig.application_name = `dbos_transact_${globalParams.executorID}_${globalParams.appVersion}`;
+    // Craft a db string from the app db string, replacing the database name:
+    const systemDbConnectionString = new URL(pgPoolConfig.connectionString!);
+    systemDbConnectionString.pathname = `/${systemDatabaseName}`;
+
+    this.systemPoolConfig = {
+      connectionString: systemDbConnectionString.toString(),
+      // This sets the application_name column in pg_stat_activity
+      application_name: `dbos_transact_${globalParams.executorID}_${globalParams.appVersion}`,
+    };
     this.pool = new Pool(this.systemPoolConfig);
     const knexConfig = {
       client: 'pg',
diff --git a/src/user_database.ts b/src/user_database.ts
index 0d17c5617..2cf72c2d1 100644
--- a/src/user_database.ts
+++ b/src/user_database.ts
@@ -21,8 +21,15 @@ export async function createDBIfDoesNotExist(poolConfig: PoolConfig, logger: Log
   } catch (error) {
     logger.info(`Database ${poolConfig.database} does not exist, creating...`);
   }
-  const app_database = poolConfig.database;
-  const postgresConfig = { ...poolConfig, database: 'postgres' };
+  // Craft a db string from the app db string, replacing the database name:
+  const pgDbConnectionString = new URL(poolConfig.connectionString!);
+  const app_database = pgDbConnectionString.pathname.substring(1);
+  pgDbConnectionString.pathname = '/postgres';
+
+  const postgresConfig = {
+    ...poolConfig,
+    connectionString: pgDbConnectionString.toString(),
+  };
   const postgresClient = new Client(postgresConfig);
   let connection_failed = true;
   try {
diff --git a/tests/client.test.ts b/tests/client.test.ts
index 7f3436d1e..24fc9fc22 100644
--- a/tests/client.test.ts
+++ b/tests/client.test.ts
@@ -49,7 +49,8 @@ describe('DBOSClient', () => {
 
   beforeAll(async () => {
     config = generateDBOSTestConfig();
-    const $poolConfig = config.poolConfig!;
+    const $poolConfig = structuredClone(config.poolConfig!);
+    $poolConfig.connectionString = undefined;
     database_url = `postgres://${$poolConfig.user}:${$poolConfig.password as string}@${$poolConfig.host}:${$poolConfig.port}/${$poolConfig.database}`;
     poolConfig = { ...$poolConfig, database: config.system_database };
     await setUpDBOSTestDb(config);
diff --git a/tests/dbos-runtime/config.test.ts b/tests/dbos-runtime/config.test.ts
index f565b4f96..5be89589a 100644
--- a/tests/dbos-runtime/config.test.ts
+++ b/tests/dbos-runtime/config.test.ts
@@ -6,12 +6,14 @@ import { UserDatabaseName } from '../../src/user_database';
 import { PoolConfig } from 'pg';
 import {
   ConfigFile,
+  DBConfig,
   loadConfigFile,
   parseConfigFile,
-  parseDbString,
   translatePublicDBOSconfig,
   overwrite_config,
+  constructPoolConfig,
   dbosConfigFilePath,
+  parseSSLConfig,
 } from '../../src/dbos-runtime/config';
 import { DBOSRuntimeConfig, defaultEntryPoint } from '../../src/dbos-runtime/runtime';
 import { DBOSConfigKeyTypeError, DBOSInitializationError } from '../../src/error';
@@ -25,9 +27,9 @@ describe('dbos-config', () => {
       name: 'some app'
       language: 'node'
       database:
-        hostname: 'some host'
+        hostname: 'somehost'
         port: 1234
-        username: 'some user'
+        username: 'someuser'
         password: \${PGPASSWORD}
         app_db_name: 'some_db'
         ssl: false
@@ -64,6 +66,241 @@ describe('dbos-config', () => {
     });
   });
 
+  describe('constructPoolConfig', () => {
+    beforeEach(() => {
+      process.env = {};
+    });
+
+    const baseConfig = (): ConfigFile => ({
+      name: 'Test App',
+      application: {},
+      env: {},
+      database: {},
+    });
+
+    function assertPoolConfig(pool: any, expected: Partial<typeof pool>) {
+      expect(pool.host).toBe(expected.host);
+      expect(pool.port).toBe(expected.port);
+      expect(pool.user).toBe(expected.user);
+      expect(pool.password).toBe(expected.password);
+      expect(pool.database).toBe(expected.database);
+      expect(pool.connectionString).toBe(expected.connectionString);
+      expect(pool.connectionTimeoutMillis).toBe(expected.connectionTimeoutMillis);
+    }
+
+    test('uses default values when config is empty', () => {
+      const config = baseConfig();
+      const pool = constructPoolConfig(config);
+
+      assertPoolConfig(pool, {
+        host: 'localhost',
+        port: 5432,
+        user: 'postgres',
+        password: 'dbos',
+        database: 'test_app',
+        connectionString: 'postgresql://postgres:dbos@localhost:5432/test_app?connect_timeout=3&sslmode=disable',
+        connectionTimeoutMillis: 3000,
+      });
+    });
+
+    test('gets db name from  pkg json name', () => {
+      const mockPackageJsoString = `{"name": "appname"}`;
+      jest.spyOn(fs, 'readFileSync').mockReturnValueOnce(mockPackageJsoString);
+      const config = baseConfig();
+      config.name = undefined;
+      config.database.app_db_name = undefined;
+
+      const pool = constructPoolConfig(config);
+
+      assertPoolConfig(pool, {
+        host: 'localhost',
+        port: 5432,
+        user: 'postgres',
+        password: 'dbos',
+        database: 'appname',
+        connectionTimeoutMillis: 3000,
+        connectionString: 'postgresql://postgres:dbos@localhost:5432/appname?connect_timeout=3&sslmode=disable',
+      });
+    });
+
+    test('throws when app name and package.json are missing', () => {
+      jest.spyOn(utils, 'readFileSync').mockReturnValueOnce('{}'); // load package.json
+
+      const config = baseConfig();
+      config.name = undefined;
+
+      expect(() => constructPoolConfig(config)).toThrow(DBOSInitializationError);
+    });
+
+    test('uses environment variable overrides', () => {
+      process.env.DBOS_DBHOST = 'envhost';
+      process.env.DBOS_DBPORT = '7777';
+      process.env.DBOS_DBUSER = 'envuser';
+      process.env.DBOS_DBPASSWORD = 'envpass';
+
+      const config = baseConfig();
+      config.database.app_db_name = 'appdb';
+      config.database.ssl = false;
+      config.database.hostname = 'something else';
+
+      const pool = constructPoolConfig(config);
+
+      assertPoolConfig(pool, {
+        host: 'envhost',
+        port: 7777,
+        user: 'envuser',
+        password: 'envpass',
+        database: 'appdb',
+        connectionString: 'postgresql://envuser:envpass@envhost:7777/appdb?connect_timeout=3&sslmode=disable',
+        connectionTimeoutMillis: 3000,
+      });
+    });
+
+    test('uses environment variable overrides with connection string input', () => {
+      process.env.DBOS_DBHOST = 'envhost';
+      process.env.DBOS_DBPORT = '7777';
+      process.env.DBOS_DBUSER = 'envuser';
+      process.env.DBOS_DBPASSWORD = 'envpass';
+      process.env.DBOS_DEBUG_WORKFLOW_ID = 'debug-workflow-id';
+
+      const config = baseConfig();
+      config.database_url = 'postgresql://a:b@c:1234/appdb?connect_timeout=22&sslmode=disable';
+
+      const pool = constructPoolConfig(config);
+
+      assertPoolConfig(pool, {
+        host: 'envhost',
+        port: 7777,
+        user: 'envuser',
+        password: 'envpass',
+        database: 'appdb',
+        connectionString: 'postgresql://envuser:envpass@envhost:7777/appdb?connect_timeout=22&sslmode=disable',
+        connectionTimeoutMillis: 22000,
+      });
+    });
+
+    test('uses mixed parameters from defaults, config, and environment', () => {
+      process.env.DBOS_DBHOST = 'env-host.com';
+
+      const config = baseConfig();
+      config.database = {
+        username: 'configured_user',
+        app_db_name: 'configured_db',
+      };
+
+      const pool = constructPoolConfig(config, { userDbPoolSize: 7 });
+
+      assertPoolConfig(pool, {
+        host: 'env-host.com', // from DBOS_DBHOST
+        port: 5432, // default
+        user: 'configured_user', // from config.database
+        password: 'dbos', // default
+        database: 'configured_db', // from config.database
+        max: 7, // from userDbPoolSize
+        connectionTimeoutMillis: 3000, // default
+        connectionString:
+          'postgresql://configured_user:dbos@env-host.com:5432/configured_db?connect_timeout=3&sslmode=no-verify',
+      });
+    });
+
+    test('respects ssl_ca and builds verify-full connection string', () => {
+      jest.spyOn(utils, 'readFileSync').mockReturnValueOnce('CA_CERT'); // readFileSync for CA
+
+      const config = baseConfig();
+      config.database = {
+        hostname: 'db',
+        port: 5432,
+        username: 'u',
+        password: 'p',
+        ssl_ca: 'ca.pem',
+      };
+
+      const pool = constructPoolConfig(config);
+      assertPoolConfig(pool, {
+        host: 'db',
+        port: 5432,
+        user: 'u',
+        password: 'p',
+        database: 'test_app',
+        connectionTimeoutMillis: 3000,
+        connectionString: 'postgresql://u:p@db:5432/test_app?connect_timeout=3&sslmode=verify-full&sslrootcert=ca.pem',
+      });
+    });
+
+    test('parses all connection parameters from database_url, backfill poolConfig and ignore provided configFile.database parameters', () => {
+      const dbUrl = 'postgresql://url_user:url_pass@url_host:9999/url_db?sslmode=require&connect_timeout=15&extra=1';
+
+      const config = baseConfig();
+      config.database_url = dbUrl;
+      config.database = {
+        hostname: 'a',
+        port: 1234,
+        username: 'b',
+      };
+      const pool = constructPoolConfig(config);
+
+      assertPoolConfig(pool, {
+        host: 'url_host',
+        port: 9999,
+        user: 'url_user',
+        password: 'url_pass',
+        database: 'url_db',
+        connectionString: dbUrl,
+        connectionTimeoutMillis: 15000,
+      });
+    });
+
+    test('constructPoolConfig correctly handles app names with spaces', () => {
+      const config = baseConfig();
+      config.name = 'app name with spaces';
+      config.database.ssl = true;
+      const pool = constructPoolConfig(config);
+      assertPoolConfig(pool, {
+        host: 'localhost',
+        port: 5432,
+        user: 'postgres',
+        password: 'dbos',
+        database: 'app_name_with_spaces',
+        connectionTimeoutMillis: 3000,
+        connectionString:
+          'postgresql://postgres:dbos@localhost:5432/app_name_with_spaces?connect_timeout=3&sslmode=no-verify',
+      });
+    });
+
+    test('constructPoolConfig throws with invalid database_url format', () => {
+      const config = baseConfig();
+      config.database_url = 'not-a-valid-url';
+
+      expect(() => constructPoolConfig(config)).toThrow();
+    });
+
+    test('constructPoolConfig throws when database_url is missing required fields', () => {
+      // Test missing password
+      const config1 = baseConfig();
+      config1.database_url = 'postgres://user@host:5432/db';
+
+      expect(() => constructPoolConfig(config1)).toThrow(/missing required field\(s\): password/);
+
+      // Test missing username and password
+      const config2 = baseConfig();
+      config2.database_url = 'postgres://host:5432/db';
+
+      expect(() => constructPoolConfig(config2)).toThrow(/missing required field\(s\): username, password/);
+
+      // Test missing hostname
+      const config3 = baseConfig();
+      config3.database_url = 'postgres://user:pass@:5432/db';
+
+      expect(() => constructPoolConfig(config3)).toThrow(/Invalid URL/);
+
+      // Test missing database name
+      const config4 = baseConfig();
+      config4.database_url = 'postgres://user:pass@host:5432/';
+
+      expect(() => constructPoolConfig(config4)).toThrow(/missing required field\(s\): database name/);
+    });
+  });
+
   describe('Configuration parsing', () => {
     // reset environment variables for each test as per https://stackoverflow.com/a/48042799
     const OLD_ENV = process.env;
@@ -83,16 +320,7 @@ describe('dbos-config', () => {
       const [dbosConfig, runtimeConfig]: [DBOSConfig, DBOSRuntimeConfig] = parseConfigFile(mockCLIOptions);
 
       // Test pool config options
-      const poolConfig: PoolConfig = dbosConfig.poolConfig!;
-      expect(poolConfig.host).toBe('some host');
-      expect(poolConfig.port).toBe(1234);
-      expect(poolConfig.user).toBe('some user');
-      expect(poolConfig.password).toBe(process.env.PGPASSWORD);
-      expect(poolConfig.connectionTimeoutMillis).toBe(3000);
-      expect(poolConfig.database).toBe('some_db');
-      expect(poolConfig.ssl).toBe(false);
-
-      expect(dbosConfig.userDbclient).toBe(UserDatabaseName.KNEX);
+      expect(dbosConfig.poolConfig).toBeDefined();
 
       // Application config
       const applicationConfig: object = dbosConfig.application || {};
@@ -157,57 +385,6 @@ describe('dbos-config', () => {
       expect(() => parseConfigFile(mockCLIOptions)).toThrow(DBOSInitializationError);
     });
 
-    test('config file loads default without database section', () => {
-      const localMockDBOSConfigYamlString = `
-        name: some-app
-      `;
-      jest.spyOn(utils, 'readFileSync').mockReturnValueOnce(localMockDBOSConfigYamlString);
-      jest.spyOn(utils, 'readFileSync').mockReturnValueOnce('SQL STATEMENTS');
-      const [dbosConfig, _dbosRuntimeConfig]: [DBOSConfig, DBOSRuntimeConfig] = parseConfigFile(mockCLIOptions);
-      expect(dbosConfig.poolConfig!.host).toEqual('localhost');
-      expect(dbosConfig.poolConfig!.port).toEqual(5432);
-      expect(dbosConfig.poolConfig!.user).toEqual('postgres');
-      expect(dbosConfig.poolConfig!.password).toEqual(process.env.PGPASSWORD);
-      expect(dbosConfig.poolConfig!.database).toEqual('some_app');
-    });
-
-    test('parseConfigFile prioritizes database_url over database field', () => {
-      const localMockDBOSConfigYamlString = `
-            name: some-app
-            database_url: 'postgres://some_user:some_password@some_host:1234/some_db'
-            database:
-                hostname: 'localhost'
-                port: 5432
-                username: 'postgres'
-                password: \${PGPASSWORD}
-                app_db_name: 'some_db'
-            `;
-      jest.spyOn(utils, 'readFileSync').mockReturnValueOnce(localMockDBOSConfigYamlString);
-      jest.spyOn(utils, 'readFileSync').mockReturnValueOnce('SQL STATEMENTS');
-      const [dbosConfig, _dbosRuntimeConfig]: [DBOSConfig, DBOSRuntimeConfig] = parseConfigFile(mockCLIOptions);
-      expect(dbosConfig.poolConfig!.host).toEqual('some_host');
-      expect(dbosConfig.poolConfig!.port).toEqual(1234);
-      expect(dbosConfig.poolConfig!.user).toEqual('some_user');
-      expect(dbosConfig.poolConfig!.password).toEqual('some_password');
-      expect(dbosConfig.poolConfig!.database).toEqual('some_db');
-    });
-
-    test('config file loads mixed params', () => {
-      const localMockDBOSConfigYamlString = `
-        name: some-app
-        database:
-          hostname: 'some host'
-      `;
-      jest.spyOn(utils, 'readFileSync').mockReturnValueOnce(localMockDBOSConfigYamlString);
-      jest.spyOn(utils, 'readFileSync').mockReturnValueOnce('SQL STATEMENTS');
-      const [dbosConfig, _dbosRuntimeConfig]: [DBOSConfig, DBOSRuntimeConfig] = parseConfigFile(mockCLIOptions);
-      expect(dbosConfig.poolConfig!.host).toEqual('some host');
-      expect(dbosConfig.poolConfig!.port).toEqual(5432);
-      expect(dbosConfig.poolConfig!.user).toEqual('postgres');
-      expect(dbosConfig.poolConfig!.password).toEqual(process.env.PGPASSWORD);
-      expect(dbosConfig.poolConfig!.database).toEqual('some_app');
-    });
-
     test('config file specifies the wrong language', () => {
       const localMockDBOSConfigYamlString = `
       language: 'python'
@@ -223,58 +400,11 @@ describe('dbos-config', () => {
       jest.spyOn(utils, 'readFileSync').mockReturnValueOnce('SQL STATEMENTS');
       expect(() => parseConfigFile(mockCLIOptions)).toThrow(DBOSInitializationError);
     });
-
-    test('Config pool settings can be overridden by environment variables', () => {
-      const mockDBOSConfigYamlString = `
-      name: 'some app'
-      language: 'node'
-      database:
-        hostname: 'some host'
-        port: 1234
-        username: 'some user'
-        password: 'some password'
-        app_db_name: 'some_db'
-        local_suffix: true
-        ssl: false`;
-
-      jest.spyOn(utils, 'readFileSync').mockReturnValue(mockDBOSConfigYamlString);
-
-      process.env.DBOS_DBHOST = 'DBHOST_OVERRIDE';
-      process.env.DBOS_DBPORT = '99999';
-      process.env.DBOS_DBUSER = 'DBUSER_OVERRIDE';
-      process.env.DBOS_DBPASSWORD = 'DBPASSWORD_OVERRIDE';
-      process.env.DBOS_DBLOCALSUFFIX = 'false';
-
-      const [dbosConfig, runtimeConfig]: [DBOSConfig, DBOSRuntimeConfig] = parseConfigFile(mockCLIOptions);
-
-      const poolConfig: PoolConfig = dbosConfig.poolConfig!;
-      expect(poolConfig.host).toBe('DBHOST_OVERRIDE');
-      expect(poolConfig.port).toBe(99999);
-      expect(poolConfig.user).toBe('DBUSER_OVERRIDE');
-      expect(poolConfig.password).toBe('DBPASSWORD_OVERRIDE');
-      expect(poolConfig.database).toBe('some_db');
-    });
-
-    test('constructPoolConfig correctly handles app names with spaces', () => {
-      const mockDBOSConfigYamlString = `
-        name: 'some app with spaces'
-        `;
-
-      jest.spyOn(utils, 'readFileSync').mockReturnValueOnce(mockDBOSConfigYamlString);
-
-      const [dbosConfig, _]: [DBOSConfig, DBOSRuntimeConfig] = parseConfigFile(mockCLIOptions);
-      const poolConfig: PoolConfig = dbosConfig.poolConfig!;
-      expect(poolConfig.database).toBe('some_app_with_spaces');
-    });
   });
 
   describe('context getConfig()', () => {
     beforeEach(() => {
-      jest.spyOn(utils, 'readFileSync').mockReturnValue(mockDBOSConfigYamlString);
-    });
-
-    afterEach(() => {
-      jest.restoreAllMocks();
+      jest.spyOn(utils, 'readFileSync').mockReturnValueOnce(mockDBOSConfigYamlString);
     });
 
     test('getConfig returns the expected values', async () => {
@@ -301,16 +431,17 @@ describe('dbos-config', () => {
 
     test('getConfig returns the default value when no application config is provided', async () => {
       const localMockDBOSConfigYamlString = `
+        name: some-app
         database:
-          hostname: 'some host'
+          hostname: 'somehost'
           port: 1234
-          username: 'some user'
+          username: 'someuser'
           password: \${PGPASSWORD}
           connectionTimeoutMillis: 3000
           app_db_name: 'some_db'
       `;
       jest.restoreAllMocks();
-      jest.spyOn(utils, 'readFileSync').mockReturnValue(localMockDBOSConfigYamlString);
+      jest.spyOn(utils, 'readFileSync').mockReturnValueOnce(localMockDBOSConfigYamlString);
       const [dbosConfig, _dbosRuntimeConfig]: [DBOSConfigInternal, DBOSRuntimeConfig] = parseConfigFile(mockCLIOptions);
       const dbosExec = new DBOSExecutor(dbosConfig);
       const ctx: WorkflowContextImpl = new WorkflowContextImpl(
@@ -329,10 +460,11 @@ describe('dbos-config', () => {
 
     test('environment variables are set correctly', async () => {
       const localMockDBOSConfigYamlString = `
+        name: some-app
         database:
-          hostname: 'some host'
+          hostname: 'somehost'
           port: 1234
-          username: 'some user'
+          username: 'someuser'
           password: \${PGPASSWORD}
           connectionTimeoutMillis: 3000
           app_db_name: 'some_db'
@@ -341,7 +473,7 @@ describe('dbos-config', () => {
           RANDENV: \${SOMERANDOMENV}
       `;
       jest.restoreAllMocks();
-      jest.spyOn(utils, 'readFileSync').mockReturnValue(localMockDBOSConfigYamlString);
+      jest.spyOn(utils, 'readFileSync').mockReturnValueOnce(localMockDBOSConfigYamlString);
       const [dbosConfig, _dbosRuntimeConfig]: [DBOSConfigInternal, DBOSRuntimeConfig] = parseConfigFile(mockCLIOptions);
       const dbosExec = new DBOSExecutor(dbosConfig);
       expect(process.env.FOOFOO).toBe('barbar');
@@ -349,77 +481,6 @@ describe('dbos-config', () => {
       await dbosExec.telemetryCollector.destroy();
     });
 
-    test('ssl true enables ssl', async () => {
-      const localMockDBOSConfigYamlString = `
-        database:
-          hostname: 'localhost'
-          port: 1234
-          username: 'some user'
-          password: \${PGPASSWORD}
-          connectionTimeoutMillis: 3000
-          app_db_name: 'some_db'
-          ssl: true
-        env:
-          FOOFOO: barbar
-      `;
-      jest.restoreAllMocks();
-      jest.spyOn(utils, 'readFileSync').mockReturnValue(localMockDBOSConfigYamlString);
-      const [dbosConfig, _dbosRuntimeConfig]: [DBOSConfig, DBOSRuntimeConfig] = parseConfigFile(mockCLIOptions);
-      expect(dbosConfig.poolConfig!.ssl).toEqual({ rejectUnauthorized: false });
-    });
-
-    test('config works without app_db_name', async () => {
-      const localMockDBOSConfigYamlString = `
-        name: some-app
-        database:
-          hostname: 'localhost'
-          port: 1234
-          username: 'some user'
-          password: \${PGPASSWORD}
-      `;
-      jest.restoreAllMocks();
-      jest.spyOn(utils, 'readFileSync').mockReturnValue(localMockDBOSConfigYamlString);
-      const [dbosConfig, _dbosRuntimeConfig]: [DBOSConfig, DBOSRuntimeConfig] = parseConfigFile(mockCLIOptions);
-      const poolConfig = dbosConfig.poolConfig;
-      expect(poolConfig!.database).toBe('some_app');
-    });
-
-    test('ssl defaults off for localhost', async () => {
-      const localMockDBOSConfigYamlString = `
-        database:
-          hostname: 'localhost'
-          port: 1234
-          username: 'some user'
-          password: \${PGPASSWORD}
-          connectionTimeoutMillis: 3000
-          app_db_name: 'some_db'
-        env:
-          FOOFOO: barbar
-      `;
-      jest.restoreAllMocks();
-      jest.spyOn(utils, 'readFileSync').mockReturnValue(localMockDBOSConfigYamlString);
-      const [dbosConfig, _dbosRuntimeConfig]: [DBOSConfig, DBOSRuntimeConfig] = parseConfigFile(mockCLIOptions);
-      expect(dbosConfig.poolConfig!.ssl).toBe(false);
-    });
-
-    test('ssl defaults on for not-localhost', async () => {
-      const localMockDBOSConfigYamlString = `
-        database:
-          hostname: 'some host'
-          port: 1234
-          username: 'some user'
-          password: \${PGPASSWORD}
-          connectionTimeoutMillis: 3000
-          app_db_name: 'some_db'
-        env:
-          FOOFOO: barbar
-      `;
-      jest.restoreAllMocks();
-      jest.spyOn(utils, 'readFileSync').mockReturnValue(localMockDBOSConfigYamlString);
-      const [dbosConfig, _dbosRuntimeConfig]: [DBOSConfig, DBOSRuntimeConfig] = parseConfigFile(mockCLIOptions);
-      expect(dbosConfig.poolConfig!.ssl).toEqual({ rejectUnauthorized: false });
-    });
-
     test('getConfig throws when it finds a value of different type than the default', async () => {
       const [dbosConfig, _dbosRuntimeConfig]: [DBOSConfigInternal, DBOSRuntimeConfig] = parseConfigFile(mockCLIOptions);
       const dbosExec = new DBOSExecutor(dbosConfig);
@@ -479,6 +540,7 @@ describe('dbos-config', () => {
       ];
       for (const dbName of invalidNames) {
         const localMockDBOSConfigYamlString = `
+          name: 'some-app'
           database:
               hostname: 'some host'
               port: 1234
@@ -487,7 +549,7 @@ describe('dbos-config', () => {
               app_db_name: '${dbName}'
         `;
         jest.restoreAllMocks();
-        jest.spyOn(utils, 'readFileSync').mockReturnValue(localMockDBOSConfigYamlString);
+        jest.spyOn(utils, 'readFileSync').mockReturnValueOnce(localMockDBOSConfigYamlString);
         expect(() => parseConfigFile(mockCLIOptions)).toThrow(DBOSInitializationError);
       }
     });
@@ -557,9 +619,10 @@ describe('dbos-config', () => {
           user: 'jon',
           password: 'doe',
           database: 'dbostest',
-          connectionTimeoutMillis: 7000,
-          ssl: { ca: [certdata], rejectUnauthorized: true },
           max: 20,
+          connectionString:
+            'postgres://jon:doe@mother:2345/dbostest?sslmode=require&sslrootcert=my_cert&connect_timeout=7',
+          connectionTimeoutMillis: 7000,
         },
         userDbclient: UserDatabaseName.PRISMA,
         telemetry: {
@@ -601,9 +664,9 @@ describe('dbos-config', () => {
           user: 'postgres',
           password: process.env.PGPASSWORD || 'dbos',
           database: 'appname',
-          connectionTimeoutMillis: 3000,
-          ssl: false,
           max: 20,
+          connectionTimeoutMillis: 3000,
+          connectionString: 'postgresql://postgres:dbos@localhost:5432/appname?connect_timeout=3&sslmode=disable',
         },
         userDbclient: UserDatabaseName.KNEX,
         telemetry: {
@@ -674,9 +737,9 @@ describe('dbos-config', () => {
           user: 'postgres',
           password: process.env.PGPASSWORD || 'dbos',
           database: 'appname',
-          connectionTimeoutMillis: 3000,
-          ssl: false,
           max: 20,
+          connectionTimeoutMillis: 3000,
+          connectionString: 'postgresql://postgres:dbos@localhost:5432/appname?connect_timeout=3&sslmode=disable',
         },
         userDbclient: UserDatabaseName.KNEX,
         telemetry: {
@@ -726,127 +789,6 @@ describe('dbos-config', () => {
     });
   });
 
-  describe('parseDbString', () => {
-    test('should correctly parse a full connection string with extra parameters', () => {
-      // The parse function we use actually reads the certificate.
-      jest.spyOn(fs, 'readFileSync').mockReturnValue('cert');
-      const dbString =
-        'postgres://user:password@localhost:5432/mydatabase?sslmode=require&sslrootcert=my_cert.pem&connect_timeout=5&extra_param=ignore_me';
-
-      const result = parseDbString(dbString);
-
-      expect(result).toEqual({
-        hostname: 'localhost',
-        port: 5432,
-        username: 'user',
-        password: 'password',
-        app_db_name: 'mydatabase',
-        ssl: true,
-        ssl_ca: 'my_cert.pem',
-        connectionTimeoutMillis: 5000,
-      });
-      jest.restoreAllMocks();
-    });
-
-    test('should parse a connection string with no parameters', () => {
-      const dbString = 'postgres://user:password@localhost:5432/mydatabase';
-
-      const result = parseDbString(dbString);
-
-      expect(result).toEqual({
-        hostname: 'localhost',
-        port: 5432,
-        username: 'user',
-        password: 'password',
-        app_db_name: 'mydatabase',
-        ssl: false,
-        ssl_ca: undefined,
-        connectionTimeoutMillis: undefined,
-      });
-    });
-
-    test('should parse a connection string with only some parameters', () => {
-      const dbString = 'postgres://user@localhost:5432/mydatabase?sslmode=require';
-
-      const result = parseDbString(dbString);
-
-      expect(result).toEqual({
-        hostname: 'localhost',
-        port: 5432,
-        username: 'user',
-        password: undefined,
-        app_db_name: 'mydatabase',
-        ssl: true, // Since sslmode=require is present
-        ssl_ca: undefined,
-        connectionTimeoutMillis: undefined,
-      });
-    });
-
-    test('should parse a connection string missing port', () => {
-      const dbString = 'postgres://user:password@localhost/mydatabase';
-
-      const result = parseDbString(dbString);
-
-      expect(result).toEqual({
-        hostname: 'localhost',
-        port: undefined, // No port provided
-        username: 'user',
-        password: 'password',
-        app_db_name: 'mydatabase',
-        ssl: false,
-        ssl_ca: undefined,
-        connectionTimeoutMillis: undefined,
-      });
-    });
-
-    test('should parse a connection string missing username and password', () => {
-      const dbString = 'postgres://localhost:5432/mydatabase';
-
-      const result = parseDbString(dbString);
-
-      expect(result).toEqual({
-        hostname: 'localhost',
-        port: 5432,
-        username: undefined,
-        password: undefined,
-        app_db_name: 'mydatabase',
-        ssl: false,
-        ssl_ca: undefined,
-        connectionTimeoutMillis: undefined,
-      });
-    });
-
-    test('should parse a connection string missing database name', () => {
-      const dbString = 'postgres://user:password@localhost:5432';
-
-      const result = parseDbString(dbString);
-
-      expect(result).toEqual({
-        hostname: 'localhost',
-        port: 5432,
-        username: 'user',
-        password: 'password',
-        app_db_name: undefined,
-        ssl: false,
-        ssl_ca: undefined,
-        connectionTimeoutMillis: undefined,
-      });
-    });
-
-    test('should handle an empty connection string (invalid case)', () => {
-      expect(() => parseDbString('')).toThrow();
-    });
-
-    test('should handle an invalid connection string format', () => {
-      expect(() => parseDbString('not-a-valid-db-string')).toThrow();
-    });
-
-    test('should handle a connection string missing hostname', () => {
-      const dbString = 'postgres://user:password@:5432/mydatabase';
-      expect(() => parseDbString(dbString)).toThrow();
-    });
-  });
-
   describe('overwrite_config', () => {
     test('should return the original configs when config file is not found', () => {
       const providedDBOSConfig: DBOSConfigInternal = {
@@ -1004,6 +946,9 @@ describe('dbos-config', () => {
       expect(resultDBOSConfig.poolConfig?.user).toEqual('user-from-file');
       expect(resultDBOSConfig.poolConfig?.password).toEqual('password-from-file');
       expect(resultDBOSConfig.poolConfig?.database).toEqual('db_from_file');
+      expect(resultDBOSConfig.poolConfig?.connectionString).toEqual(
+        'postgresql://user-from-file:password-from-file@db-host-from-file:1234/db_from_file?connect_timeout=3&sslmode=no-verify',
+      );
 
       // System database name should be from file
       expect(resultDBOSConfig.system_database).toEqual('sys_db_from_file');
@@ -1034,6 +979,51 @@ describe('dbos-config', () => {
       expect(resultRuntimeConfig.setup).toEqual(providedRuntimeConfig.setup);
     });
 
+    test('should craft a verify-full address with an ssl_ca provided', () => {
+      // Mock the config file content with database settings
+      const mockDBOSConfigYamlString = `
+            name: app-from-file
+            database:
+                hostname: db-host-from-file
+                port: 1234
+                username: user-from-file
+                password: password-from-file
+                app_db_name: db_from_file
+                ssl_ca: my_cert
+            `;
+      jest.spyOn(utils, 'readFileSync').mockReturnValue(mockDBOSConfigYamlString);
+
+      const providedDBOSConfig: DBOSConfigInternal = {
+        name: 'test-app',
+        poolConfig: {
+          host: 'localhost',
+          port: 5432,
+          user: 'postgres',
+          password: 'password',
+          database: 'test_db',
+        },
+        userDbclient: UserDatabaseName.KNEX,
+        system_database: 'abc',
+        telemetry: {},
+      };
+
+      const providedRuntimeConfig: DBOSRuntimeConfig = {
+        port: 3000,
+        admin_port: 4000,
+        runAdminServer: false,
+        entrypoints: ['app.js'],
+        start: [],
+        setup: [],
+      };
+
+      const [resultDBOSConfig] = overwrite_config(providedDBOSConfig, providedRuntimeConfig);
+
+      // Database settings should reflect what's in the file
+      expect(resultDBOSConfig.poolConfig?.connectionString).toEqual(
+        'postgresql://user-from-file:password-from-file@db-host-from-file:1234/db_from_file?connect_timeout=3&sslmode=verify-full&sslrootcert=my_cert',
+      );
+    });
+
     test('should use config file content when parameters are missing from provided config', () => {
       // Mock the config file content with database settings
       const mockDBOSConfigYamlString = `
@@ -1102,7 +1092,7 @@ describe('dbos-config', () => {
             sys_db_name: sys_db_from_file
         `;
 
-      jest.spyOn(utils, 'readFileSync').mockReturnValue(mockDBOSConfigYamlString);
+      jest.spyOn(utils, 'readFileSync').mockReturnValueOnce(mockDBOSConfigYamlString);
 
       const providedDBOSConfig: DBOSConfigInternal = {
         name: 'test-app',
@@ -1154,7 +1144,7 @@ describe('dbos-config', () => {
         app_db_name: db_from_file
     `;
 
-      jest.spyOn(utils, 'readFileSync').mockReturnValue(mockDBOSConfigYamlString);
+      jest.spyOn(utils, 'readFileSync').mockReturnValueOnce(mockDBOSConfigYamlString);
 
       const providedDBOSConfig: DBOSConfigInternal = {
         name: 'test-app',
@@ -1182,4 +1172,68 @@ describe('dbos-config', () => {
       expect(resultDBOSConfig.system_database).toEqual(`${resultDBOSConfig.poolConfig?.database}_dbos_sys`);
     });
   });
+  describe('SSL Configuration Parsing', () => {
+    const originalReadFileSync = fs.readFileSync;
+
+    beforeEach(() => {
+      jest.spyOn(utils, 'readFileSync').mockReturnValue('mock-certificate-data');
+    });
+
+    afterEach(() => {
+      jest.restoreAllMocks();
+    });
+
+    test('parseSSLConfig returns false when SSL is explicitly disabled', () => {
+      const dbConfig: DBConfig = {
+        ssl: false,
+      };
+      expect(parseSSLConfig(dbConfig)).toBe(false);
+    });
+
+    test('parseSSLConfig returns certificate config when SSL CA is provided', () => {
+      const dbConfig: DBConfig = {
+        ssl: true,
+        ssl_ca: '/path/to/ca.pem',
+      };
+      const result = parseSSLConfig(dbConfig);
+      expect(result).toEqual({
+        ca: ['mock-certificate-data'],
+        rejectUnauthorized: true,
+      });
+      expect(utils.readFileSync).toHaveBeenCalledWith('/path/to/ca.pem');
+    });
+
+    test('parseSSLConfig disables SSL for localhost by default', () => {
+      const dbConfig: DBConfig = {
+        hostname: 'localhost',
+      };
+      expect(parseSSLConfig(dbConfig)).toBe(false);
+    });
+
+    test('parseSSLConfig disables SSL for 127.0.0.1 by default', () => {
+      const dbConfig: DBConfig = {
+        hostname: '127.0.0.1',
+      };
+      expect(parseSSLConfig(dbConfig)).toBe(false);
+    });
+
+    test('parseSSLConfig enables SSL with no verification for non-local hosts by default', () => {
+      const dbConfig: DBConfig = {
+        hostname: 'remote-db.example.com',
+      };
+      expect(parseSSLConfig(dbConfig)).toEqual({
+        rejectUnauthorized: false,
+      });
+    });
+
+    test('parseSSLConfig forces SSL for localhost when explicitly enabled', () => {
+      const dbConfig: DBConfig = {
+        hostname: 'localhost',
+        ssl: true,
+      };
+      expect(parseSSLConfig(dbConfig)).toEqual({
+        rejectUnauthorized: false,
+      });
+    });
+  });
 });
diff --git a/tests/dbos-runtime/runtime.test.ts b/tests/dbos-runtime/runtime.test.ts
index dc55b9f13..200b2c62e 100644
--- a/tests/dbos-runtime/runtime.test.ts
+++ b/tests/dbos-runtime/runtime.test.ts
@@ -16,6 +16,15 @@ async function waitForMessageTest(
   const stdin = command.stdin as unknown as Writable;
   const stderr = command.stderr as unknown as Writable;
 
+  // Capture and print stdout/stderr
+  stdout?.on('data', (data) => {
+    process.stdout.write(`[child stdout]: ${data}`);
+  });
+
+  stderr?.on('data', (data) => {
+    process.stderr.write(`[child stderr]: ${data}`);
+  });
+
   if (!adminPort) {
     adminPort = (Number(port) + 1).toString();
   }
@@ -79,7 +88,7 @@ function configureTemplate() {
   if (process.env.PGPASSWORD === undefined) {
     process.env.PGPASSWORD = 'dbos';
   }
-  execSync('npx dbos migrate', { env: process.env });
+  execSync('npx dbos migrate', { env: process.env, stdio: 'inherit' });
 }
 
 describe('runtime-tests-knex', () => {
@@ -132,7 +141,7 @@ describe('runtime-tests-typeorm', () => {
   });
 
   test('test hello-typeorm runtime', async () => {
-    const command = spawn('node_modules/@dbos-inc/dbos-sdk/dist/src/dbos-runtime/cli.js', ['start'], {
+    const command = spawn('node', ['node_modules/@dbos-inc/dbos-sdk/dist/src/dbos-runtime/cli.js', 'start'], {
       env: process.env,
     });
     await waitForMessageTest(command, '3000');
@@ -156,7 +165,7 @@ describe('runtime-tests-prisma', () => {
   });
 
   test('test hello-prisma runtime', async () => {
-    const command = spawn('node_modules/@dbos-inc/dbos-sdk/dist/src/dbos-runtime/cli.js', ['start'], {
+    const command = spawn('node', ['node_modules/@dbos-inc/dbos-sdk/dist/src/dbos-runtime/cli.js', 'start'], {
       env: process.env,
     });
     await waitForMessageTest(command, '3000');
@@ -180,7 +189,7 @@ describe('runtime-tests-drizzle', () => {
   });
 
   test('test hello-drizzle runtime', async () => {
-    const command = spawn('node_modules/@dbos-inc/dbos-sdk/dist/src/dbos-runtime/cli.js', ['start'], {
+    const command = spawn('node', ['node_modules/@dbos-inc/dbos-sdk/dist/src/dbos-runtime/cli.js', 'start'], {
       env: process.env,
     });
     await waitForMessageTest(command, '3000');
diff --git a/tests/drizzle.test.ts b/tests/drizzle.test.ts
index a74ad259c..6f8e49136 100644
--- a/tests/drizzle.test.ts
+++ b/tests/drizzle.test.ts
@@ -12,9 +12,9 @@ import {
   Transaction,
   TransactionContext,
 } from '../src';
-import { DBOSConfig } from '../src/dbos-executor';
+import { DBOSConfig, DBOSConfigInternal } from '../src/dbos-executor';
 import { UserDatabaseName } from '../src/user_database';
-import { generateDBOSTestConfig, generatePublicDBOSTestConfig, setUpDBOSTestDb } from './helpers';
+import { generateDBOSTestConfig, setUpDBOSTestDb } from './helpers';
 import { pgTable, serial, text } from 'drizzle-orm/pg-core';
 import { NodePgDatabase } from 'drizzle-orm/node-postgres';
 import { eq } from 'drizzle-orm';
@@ -213,9 +213,10 @@ describe('drizzle-auth-tests', () => {
 class TestEngine {
   @DBOS.transaction()
   static async testEngine() {
+    const pc = (DBOS.dbosConfig as DBOSConfigInternal).poolConfig;
     const ds = DBOS.drizzleClient;
     // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
-    expect((ds as any).session.client._connectionTimeoutMillis).toEqual(3000);
+    expect((ds as any).session.client._connectionTimeoutMillis).toEqual(pc.connectionTimeoutMillis);
     // Drizzle doesn't expose the pool directly
     await Promise.resolve();
   }
@@ -223,10 +224,12 @@ class TestEngine {
 
 describe('typeorm-engine-config-tests', () => {
   test('engine-config', async () => {
-    const config = generatePublicDBOSTestConfig({
+    const config = {
+      name: 'dbostest',
       userDbclient: UserDatabaseName.DRIZZLE,
       userDbPoolSize: 2,
-    });
+      databaseUrl: `postgres://postgres:${process.env.PGPASSWORD || 'dbos'}@localhost:5432/dbostest?connect_timeout=7`,
+    };
     await setUpDBOSTestDb(config);
     DBOS.setConfig(config);
     await DBOS.launch();
diff --git a/tests/helpers.ts b/tests/helpers.ts
index facc9d39e..d1fd374f3 100644
--- a/tests/helpers.ts
+++ b/tests/helpers.ts
@@ -3,7 +3,7 @@ import { Client } from 'pg';
 import { UserDatabaseName } from '../src/user_database';
 import { DBOS } from '../src';
 import { sleepms } from '../src/utils';
-import { translatePublicDBOSconfig } from '../src/dbos-runtime/config';
+import { translatePublicDBOSconfig, constructPoolConfig, ConfigFile } from '../src/dbos-runtime/config';
 
 /* DB management helpers */
 export function generateDBOSTestConfig(dbClient?: UserDatabaseName): DBOSConfigInternal {
@@ -11,27 +11,34 @@ export function generateDBOSTestConfig(dbClient?: UserDatabaseName): DBOSConfigI
   if (!dbPassword) {
     throw new Error('DB_PASSWORD or PGPASSWORD environment variable not set');
   }
+  const silenceLogs = process.env.SILENCE_LOGS === 'true';
 
-  const silenceLogs: boolean = process.env.SILENCE_LOGS === 'true' ? true : false;
+  const databaseUrl = `postgresql://postgres:${dbPassword}@localhost:5432/dbostest?sslmode=disable`;
 
-  const dbosTestConfig: DBOSConfigInternal = {
-    poolConfig: {
-      host: 'localhost',
-      port: 5432,
-      user: 'postgres',
-      password: process.env.PGPASSWORD,
-      // We can use another way of randomizing the DB name if needed
-      database: 'dbostest',
+  const configFile: ConfigFile = {
+    name: 'dbostest',
+    database: {
+      app_db_client: dbClient || UserDatabaseName.PGNODE,
     },
+    database_url: databaseUrl,
     application: {
       counter: 3,
       shouldExist: 'exists',
     },
+    env: {},
     telemetry: {
       logs: {
         silent: silenceLogs,
       },
     },
+  };
+
+  const poolConfig = constructPoolConfig(configFile, { silent: true });
+
+  const dbosTestConfig: DBOSConfigInternal = {
+    poolConfig,
+    application: configFile.application,
+    telemetry: configFile.telemetry!,
     system_database: 'dbostest_dbos_sys',
     userDbclient: dbClient || UserDatabaseName.PGNODE,
   };
diff --git a/tests/knex.test.ts b/tests/knex.test.ts
index 26e778543..5cce49314 100644
--- a/tests/knex.test.ts
+++ b/tests/knex.test.ts
@@ -2,9 +2,9 @@ import request from 'supertest';
 
 import { DBOS, Authentication, MiddlewareContext } from '../src';
 import { DBOSInvalidWorkflowTransitionError, DBOSNotAuthorizedError } from '../src/error';
-import { DBOSConfig } from '../src/dbos-executor';
+import { DBOSConfig, DBOSConfigInternal } from '../src/dbos-executor';
 import { UserDatabaseName } from '../src/user_database';
-import { TestKvTable, generateDBOSTestConfig, generatePublicDBOSTestConfig, setUpDBOSTestDb } from './helpers';
+import { TestKvTable, generateDBOSTestConfig, setUpDBOSTestDb } from './helpers';
 import { v1 as uuidv1 } from 'uuid';
 import { Knex } from 'knex';
 import { DatabaseError } from 'pg';
@@ -191,21 +191,26 @@ describe('knex-auth-tests', () => {
 class TestEngine {
   @DBOS.transaction()
   static async testEngine() {
+    const pc = (DBOS.dbosConfig as DBOSConfigInternal).poolConfig;
     const ds = DBOS.knexClient;
     // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
-    expect((ds as any).context.client.config.connection.connectTimeout).toEqual(3000);
+    expect((ds as any).context.client.connectionSettings.connectionString).toEqual(pc.connectionString);
     // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
-    expect((ds as any).context.client.config.pool.max).toEqual(2);
+    expect((ds as any).context.client.config.pool.max).toEqual(pc.max);
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
+    expect((ds as any).context.client.connectionSettings.connectionTimeoutMillis).toBe(pc.connectionTimeoutMillis);
     await Promise.resolve();
   }
 }
 
 describe('knex-engine-config-tests', () => {
   test('engine-config', async () => {
-    const config = generatePublicDBOSTestConfig({
+    const config = {
+      name: 'dbostest',
       userDbclient: UserDatabaseName.KNEX,
       userDbPoolSize: 2,
-    });
+      databaseUrl: `postgres://postgres:${process.env.PGPASSWORD || 'dbos'}@localhost:5432/dbostest?connect_timeout=7`,
+    };
     await setUpDBOSTestDb(config);
     DBOS.setConfig(config);
     await DBOS.launch();
diff --git a/tests/pgnode.test.ts b/tests/pgnode.test.ts
index 9de0371f1..9ac95fd1f 100644
--- a/tests/pgnode.test.ts
+++ b/tests/pgnode.test.ts
@@ -1,15 +1,16 @@
 import { DBOS } from '../src';
-import { DBOSExecutor } from '../src/dbos-executor';
+import { DBOSExecutor, DBOSConfigInternal } from '../src/dbos-executor';
 import { PostgresSystemDatabase } from '../src/system_database';
 import { UserDatabaseName } from '../src/user_database';
-import { generatePublicDBOSTestConfig, setUpDBOSTestDb } from './helpers';
+import { setUpDBOSTestDb } from './helpers';
 
 class TestEngine {
   @DBOS.transaction()
   static async testEngine() {
+    const pc = (DBOS.dbosConfig as DBOSConfigInternal).poolConfig;
     const ds = DBOS.pgClient;
     // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
-    expect((ds as any)._connectionTimeoutMillis).toEqual(3000);
+    expect((ds as any)._connectionTimeoutMillis).toEqual(pc.connectionTimeoutMillis);
     // PG doesn't expose the pool directly
     await Promise.resolve();
   }
@@ -17,11 +18,12 @@ class TestEngine {
 
 describe('pgnode-engine-config-tests', () => {
   test('engine-config', async () => {
-    const config = generatePublicDBOSTestConfig({
+    const config = {
       userDbclient: UserDatabaseName.PGNODE,
       userDbPoolSize: 2,
       sysDbPoolSize: 42,
-    });
+      databaseUrl: `postgres://postgres:${process.env.PGPASSWORD || 'dbos'}@localhost:5432/dbostest?connect_timeout=7`,
+    };
     await setUpDBOSTestDb(config);
     DBOS.setConfig(config);
     await DBOS.launch();
diff --git a/tests/prisma.test.ts b/tests/prisma.test.ts
index 89f3af126..d1aa1c22e 100644
--- a/tests/prisma.test.ts
+++ b/tests/prisma.test.ts
@@ -1,7 +1,7 @@
 import request from 'supertest';
 
 import { PrismaClient, testkv } from '@prisma/client';
-import { generateDBOSTestConfig, generatePublicDBOSTestConfig, setUpDBOSTestDb } from './helpers';
+import { generateDBOSTestConfig, setUpDBOSTestDb } from './helpers';
 import {
   Transaction,
   TransactionContext,
@@ -242,9 +242,7 @@ class TestEngine {
   static async testEngine() {
     const pc = (DBOS.dbosConfig as DBOSConfigInternal).poolConfig;
     // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
-    expect((DBOS.prismaClient as any)._engineConfig.overrideDatasources.db.url).toBe(
-      `postgresql://${String(pc.user)}:${String(pc.password)}@${String(pc.host)}:${String(pc.port)}/${String(pc.database)}?connect_timeout=3000&connection_limit=2`,
-    );
+    expect((DBOS.prismaClient as any)._engineConfig.overrideDatasources.db.url).toBe(pc.connectionString);
     const r = await DBOS.prismaClient.$queryRawUnsafe('SELECT 1');
     expect(r.length).toBe(1);
     await Promise.resolve();
@@ -255,10 +253,12 @@ describe('prisma-engine-config-tests', () => {
   let config: DBOSConfig;
 
   test('prisma-engine-config', async () => {
-    config = generatePublicDBOSTestConfig({
+    config = {
+      name: 'dbostest',
       userDbclient: UserDatabaseName.PRISMA,
-      userDbPoolSize: 2,
-    });
+      userDbPoolSize: 2, // This is ignored with Prisma
+      databaseUrl: `postgresql://postgres:${process.env.PGPASSWORD || 'dbos'}@localhost:5432/dbostest?connection_limit=2&connect_timeout=3`,
+    };
     await setUpDBOSTestDb(config);
     DBOS.setConfig(config);
     await DBOS.launch();
diff --git a/tests/proc-test/knexfile.js b/tests/proc-test/knexfile.js
index b0ce150ab..59d6f4691 100644
--- a/tests/proc-test/knexfile.js
+++ b/tests/proc-test/knexfile.js
@@ -4,14 +4,7 @@ const [dbosConfig] = parseConfigFile();
 
 const config = {
   client: 'pg',
-  connection: {
-    host: dbosConfig.poolConfig.host,
-    port: dbosConfig.poolConfig.port,
-    user: dbosConfig.poolConfig.user,
-    password: dbosConfig.poolConfig.password,
-    database: dbosConfig.poolConfig.database,
-    ssl: dbosConfig.poolConfig.ssl,
-  },
+  connection: dbosConfig.poolConfig.connectionString,
   migrations: {
     directory: './migrations',
   },
diff --git a/tests/stored-proc.test.ts b/tests/stored-proc.test.ts
index e97ebe327..73baabcfd 100644
--- a/tests/stored-proc.test.ts
+++ b/tests/stored-proc.test.ts
@@ -20,7 +20,7 @@ describe('stored-proc-tests', () => {
     process.chdir('tests/proc-test');
 
     const [config] = parseConfigFile();
-    await runSql({ ...config.poolConfig, database: 'postgres' }, async (client) => {
+    await runSql({ ...config.poolConfig, database: 'postgres', connectionString: undefined }, async (client) => {
       await client.query(`DROP DATABASE IF EXISTS ${config.poolConfig.database};`);
       await client.query(`DROP DATABASE IF EXISTS ${config.system_database};`);
       await client.query(`CREATE DATABASE ${config.poolConfig.database};`);
diff --git a/tests/typeorm.test.ts b/tests/typeorm.test.ts
index 7383661b8..23206a2df 100644
--- a/tests/typeorm.test.ts
+++ b/tests/typeorm.test.ts
@@ -3,9 +3,9 @@ import request from 'supertest';
 import { Entity, Column, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm';
 import { EntityManager, Unique } from 'typeorm';
 
-import { generateDBOSTestConfig, generatePublicDBOSTestConfig, setUpDBOSTestDb } from './helpers';
+import { generateDBOSTestConfig, setUpDBOSTestDb } from './helpers';
 import { OrmEntities, Authentication, MiddlewareContext, DBOS } from '../src';
-import { DBOSConfig } from '../src/dbos-executor';
+import { DBOSConfig, DBOSConfigInternal } from '../src/dbos-executor';
 import { v1 as uuidv1 } from 'uuid';
 import { UserDatabaseName } from '../src/user_database';
 import { DBOSInvalidWorkflowTransitionError, DBOSNotAuthorizedError } from '../src/error';
@@ -214,21 +214,26 @@ describe('typeorm-auth-tests', () => {
 class TestEngine {
   @DBOS.transaction()
   static async testEngine() {
+    const pc = (DBOS.dbosConfig as DBOSConfigInternal).poolConfig;
     const ds = DBOS.typeORMClient;
     // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
-    expect((ds as any).connection.driver.master.options.connectionTimeoutMillis).toBe(3000);
+    expect((ds as any).connection.driver.master.options.connectionString).toBe(pc.connectionString);
     // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
-    expect((ds as any).connection.driver.master.options.max).toBe(2);
+    expect((ds as any).connection.driver.master.options.max).toBe(pc.max);
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
+    expect((ds as any).queryRunner.databaseConnection._connectionTimeoutMillis).toBe(pc.connectionTimeoutMillis);
     await Promise.resolve();
   }
 }
 
 describe('typeorm-engine-config-tests', () => {
   test('engine-config', async () => {
-    const config = generatePublicDBOSTestConfig({
+    const config = {
+      name: 'dbostest',
       userDbclient: UserDatabaseName.TYPEORM,
       userDbPoolSize: 2,
-    });
+      databaseUrl: `postgres://postgres:${process.env.PGPASSWORD || 'dbos'}@localhost:5432/dbostest?connect_timeout=7`,
+    };
     await setUpDBOSTestDb(config);
     DBOS.setConfig(config);
     await DBOS.launch();


Reviews:
user: login: maxdml
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:34:06Z
user: login: maxdml
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:38:03Z
user: login: maxdml
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:38:28Z
user: login: kraftp
body: I'm very confused by this PR. The idea, as I understood it, was to move entirely towards connection strings. If one is provided, pass it directly to the ORM (they all fully support connection strings, as far as I'm aware). If no connection string is provided, construct a connection string from the `dbos-config.yaml` parameters.

That doesn't seem to be what's happening here--as far as I can tell, we're still parsing connection strings into database configs (why is `parseDbString` still around?) and then parsing them back into database strings in `constructPoolConfig` (potentially losing information in the process).
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T00:05:52Z
user: login: kraftp
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T00:06:40Z
user: login: maxdml
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T00:31:59Z
user: login: kraftp
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T15:23:11Z
user: login: kraftp
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T15:24:35Z
user: login: kraftp
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T15:25:46Z
user: login: kraftp
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T15:31:45Z
user: login: kraftp
body: This looks good. We need to get rid of the backfilling and the code that doesn't use connection strings ASAP, it's going to cause bugs.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T15:35:49Z
user: login: maxdml
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T15:51:10Z
user: login: maxdml
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T21:01:52Z
user: login: kraftp
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-16T23:56:26Z

Review comments:
diff_hunk: @@ -247,6 +304,7 @@ export interface ParseOptions {
   appVersion?: string | boolean;
   silent?: boolean;
   forceConsole?: boolean;
+  userDbPoolSize?: number;
path: src/dbos-runtime/config.ts
user: login: maxdml
body: A bit of a hack of `ParseOptions` to pass more user-provided info to `constructPoolConfig`
created_at: 2025-04-10T15:34:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -49,7 +49,8 @@ describe('DBOSClient', () => {
 
   beforeAll(async () => {
     config = generateDBOSTestConfig();
-    const $poolConfig = config.poolConfig!;
+    const $poolConfig = structuredClone(config.poolConfig!);
path: tests/client.test.ts
user: login: maxdml
body: deep copy
created_at: 2025-04-10T15:38:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,35 +3,43 @@ import { Client } from 'pg';
 import { UserDatabaseName } from '../src/user_database';
 import { DBOS } from '../src';
 import { sleepms } from '../src/utils';
-import { translatePublicDBOSconfig } from '../src/dbos-runtime/config';
+import { translatePublicDBOSconfig, constructPoolConfig, ConfigFile } from '../src/dbos-runtime/config';
 
 /* DB management helpers */
 export function generateDBOSTestConfig(dbClient?: UserDatabaseName): DBOSConfigInternal {
   const dbPassword: string | undefined = process.env.DB_PASSWORD || process.env.PGPASSWORD;
   if (!dbPassword) {
     throw new Error('DB_PASSWORD or PGPASSWORD environment variable not set');
   }
+  const silenceLogs = process.env.SILENCE_LOGS === 'true';
 
-  const silenceLogs: boolean = process.env.SILENCE_LOGS === 'true' ? true : false;
+  const databaseUrl = `postgresql://postgres:${dbPassword}@localhost:5432/dbostest?sslmode=disable`;
 
-  const dbosTestConfig: DBOSConfigInternal = {
-    poolConfig: {
-      host: 'localhost',
-      port: 5432,
-      user: 'postgres',
-      password: process.env.PGPASSWORD,
-      // We can use another way of randomizing the DB name if needed
-      database: 'dbostest',
+  const configFile: ConfigFile = {
+    name: 'dbostest',
+    database: {
+      app_db_client: dbClient || UserDatabaseName.PGNODE,
     },
+    database_url: databaseUrl,
     application: {
       counter: 3,
       shouldExist: 'exists',
     },
+    env: {},
     telemetry: {
       logs: {
         silent: silenceLogs,
+        logLevel: 'debug',
       },
     },
+  };
+
+  const poolConfig = constructPoolConfig(configFile, { silent: true });
path: tests/helpers.ts
user: login: maxdml
body: systematically use this core function in the tests
created_at: 2025-04-10T15:38:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -376,13 +360,9 @@ export class DBOSExecutor implements DBOSExecutorContext {
       const knexConfig: Knex.Config = {
         client: 'postgres',
         connection: {
-          host: userDBConfig.host,
-          port: userDBConfig.port,
-          user: userDBConfig.user,
-          password: userDBConfig.password,
-          database: userDBConfig.database,
+          connectionString: userDBConfig.connectionString,
           ssl: userDBConfig.ssl,
path: src/dbos-executor.ts
user: login: kraftp
body: Why are we still passing this in?  Shouldn't this information always be in the connection string? Do any of the ORMs not support passing in SSL info through connection strings?
created_at: 2025-04-12T00:06:40Z
author_association: MEMBER
diff_hunk: @@ -376,13 +360,9 @@ export class DBOSExecutor implements DBOSExecutorContext {
       const knexConfig: Knex.Config = {
         client: 'postgres',
         connection: {
-          host: userDBConfig.host,
-          port: userDBConfig.port,
-          user: userDBConfig.user,
-          password: userDBConfig.password,
-          database: userDBConfig.database,
+          connectionString: userDBConfig.connectionString,
           ssl: userDBConfig.ssl,
path: src/dbos-executor.ts
user: login: maxdml
body: Let me check. I might have been misled that ssl was having the same limitation than connectTimeout and pool (i.e., knex/typeorm don't always respect what's in the connection string.)
created_at: 2025-04-12T00:31:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -142,52 +139,191 @@ function retrieveApplicationName(configFile: ConfigFile): string {
   return appName;
 }
 
+/**
+ * Build a PoolConfig object.
+ *
+ * If configFile.database_url is provided, set it directly in poolConfig.connectionString and backfill the other poolConfig options.
+ * Backfilling allows the rest of the code to access database parameters easily.
+ * We configure the ORMs with the connection string
+ * ORMs accepting a poolConfig object, like pg-node, state the connectionString takes precedence.
+ *
+ * If configFile.database_url is not provided, we build the connection string from the configFile.database object.
+ *
+ * In debug mode, apply overrides from DBOS_DBHOST, DBOS_DBPORT, DBOS_DBUSER, and DBOS_DBPASSWORD.
+ *
+ * Default configuration:
+ * - Hostname: localhost
+ * - Port: 5432
+ * - Username: postgres
+ * - Password: $PGPASSWORD
+ * - Database name: transformed application name. The name is either the one provided in the config file or the one found in package.json.
+ *
+ * @param configFile - The configuration to be used.
+ * @param cliOptions - Optional CLI options.
+ * @returns PoolConfig - The constructed PoolConfig object.
+ */
 export function constructPoolConfig(configFile: ConfigFile, cliOptions?: ParseOptions): PoolConfig {
-  // Load database connection parameters. If they're not in dbos-config.yaml, load from .dbos/db_connection. Else, use defaults.
+  // FIXME: this is not a good place to set the app name
+  const appName = retrieveApplicationName(configFile);
+  if (globalParams.appName === '') {
+    globalParams.appName = appName;
+  }
+
+  const isDebugMode = process.env.DBOS_DEBUG_WORKFLOW_ID !== undefined;
+
   if (!cliOptions?.silent) {
     const logger = new GlobalLogger();
-    if (process.env.DBOS_DBHOST) {
+    if (isDebugMode) {
       logger.info('Loading database connection parameters from debug environment variables');
+    } else if (configFile.database_url) {
+      logger.info('Loading database connection parameters from database_url');
     } else if (configFile.database.hostname) {
       logger.info('Loading database connection parameters from dbos-config.yaml');
     } else {
       logger.info('Using default database connection parameters');
     }
   }
-  configFile.database.hostname = process.env.DBOS_DBHOST || configFile.database.hostname || 'localhost';
-  const dbos_dbport = process.env.DBOS_DBPORT ? parseInt(process.env.DBOS_DBPORT) : undefined;
-  configFile.database.port = dbos_dbport || configFile.database.port || 5432;
-  configFile.database.username = process.env.DBOS_DBUSER || configFile.database.username || 'postgres';
-  configFile.database.password =
-    process.env.DBOS_DBPASSWORD || configFile.database.password || process.env.PGPASSWORD || 'dbos';
-
-  let databaseName: string | undefined = configFile.database.app_db_name;
-  const appName = retrieveApplicationName(configFile);
-  if (globalParams.appName === '') {
-    globalParams.appName = appName;
-  }
-  if (databaseName === undefined) {
-    databaseName = appName.toLowerCase().replaceAll('-', '_').replaceAll(' ', '_');
-    if (databaseName.match(/^\d/)) {
-      databaseName = '_' + databaseName; // Append an underscore if the name starts with a digit
+
+  let connectionString = undefined;
+  let databaseName = undefined;
+
+  // If a database_url is found, parse it to backfill the poolConfig
+  if (configFile.database_url) {
+    const url = new URL(configFile.database_url);
+    // If in debug mode, apply the debug overrides
+    if (isDebugMode) {
+      if (process.env.DBOS_DBHOST) {
+        url.hostname = process.env.DBOS_DBHOST;
+      }
+      if (process.env.DBOS_DBPORT) {
+        url.port = process.env.DBOS_DBPORT;
+      }
+      if (process.env.DBOS_DBUSER) {
+        url.username = process.env.DBOS_DBUSER;
+      }
+      if (process.env.DBOS_DBPASSWORD) {
+        url.password = process.env.DBOS_DBPASSWORD;
+      }
+      configFile.database_url = url.toString();
+    }
+    connectionString = configFile.database_url;
+
+    // From here backfill the poolConfig
+
+    const queryParams = Object.fromEntries(url.searchParams.entries());
+
+    // Validate required fields
+    const missingFields: string[] = [];
+    if (!url.username) missingFields.push('username');
+    if (!url.password) missingFields.push('password');
+    if (!url.hostname) missingFields.push('hostname');
+
+    if (missingFields.length > 0) {
+      throw new Error(`Invalid database URL: missing required field(s): ${missingFields.join(', ')}`);
+    }
+
+    // Create database config object
+    configFile.database = {
+      hostname: url.hostname,
+      port: url.port ? parseInt(url.port, 10) : 5432,
+      username: url.username,
+      password: url.password,
+      app_db_name: url.pathname.substring(1) || undefined,
+      ssl: queryParams['sslmode'] === 'require' || queryParams['sslmode'] === 'verify-full',
+      ssl_ca: queryParams['sslrootcert'] || undefined,
+      connectionTimeoutMillis: queryParams['connect_timeout']
+        ? parseInt(queryParams['connect_timeout'], 10) * 1000
+        : 3000,
+    };
+
+    databaseName = configFile.database.app_db_name;
path: src/dbos-runtime/config.ts
user: login: kraftp
body: What is this? Why do we need it if we're getting a connection string passed in?
created_at: 2025-04-15T15:23:11Z
author_association: MEMBER
diff_hunk: @@ -142,52 +139,191 @@ function retrieveApplicationName(configFile: ConfigFile): string {
   return appName;
 }
 
+/**
+ * Build a PoolConfig object.
+ *
+ * If configFile.database_url is provided, set it directly in poolConfig.connectionString and backfill the other poolConfig options.
+ * Backfilling allows the rest of the code to access database parameters easily.
+ * We configure the ORMs with the connection string
+ * ORMs accepting a poolConfig object, like pg-node, state the connectionString takes precedence.
+ *
+ * If configFile.database_url is not provided, we build the connection string from the configFile.database object.
+ *
+ * In debug mode, apply overrides from DBOS_DBHOST, DBOS_DBPORT, DBOS_DBUSER, and DBOS_DBPASSWORD.
+ *
+ * Default configuration:
+ * - Hostname: localhost
+ * - Port: 5432
+ * - Username: postgres
+ * - Password: $PGPASSWORD
+ * - Database name: transformed application name. The name is either the one provided in the config file or the one found in package.json.
+ *
+ * @param configFile - The configuration to be used.
+ * @param cliOptions - Optional CLI options.
+ * @returns PoolConfig - The constructed PoolConfig object.
+ */
 export function constructPoolConfig(configFile: ConfigFile, cliOptions?: ParseOptions): PoolConfig {
-  // Load database connection parameters. If they're not in dbos-config.yaml, load from .dbos/db_connection. Else, use defaults.
+  // FIXME: this is not a good place to set the app name
+  const appName = retrieveApplicationName(configFile);
+  if (globalParams.appName === '') {
+    globalParams.appName = appName;
+  }
+
+  const isDebugMode = process.env.DBOS_DEBUG_WORKFLOW_ID !== undefined;
+
   if (!cliOptions?.silent) {
     const logger = new GlobalLogger();
-    if (process.env.DBOS_DBHOST) {
+    if (isDebugMode) {
       logger.info('Loading database connection parameters from debug environment variables');
+    } else if (configFile.database_url) {
+      logger.info('Loading database connection parameters from database_url');
     } else if (configFile.database.hostname) {
       logger.info('Loading database connection parameters from dbos-config.yaml');
     } else {
       logger.info('Using default database connection parameters');
     }
   }
-  configFile.database.hostname = process.env.DBOS_DBHOST || configFile.database.hostname || 'localhost';
-  const dbos_dbport = process.env.DBOS_DBPORT ? parseInt(process.env.DBOS_DBPORT) : undefined;
-  configFile.database.port = dbos_dbport || configFile.database.port || 5432;
-  configFile.database.username = process.env.DBOS_DBUSER || configFile.database.username || 'postgres';
-  configFile.database.password =
-    process.env.DBOS_DBPASSWORD || configFile.database.password || process.env.PGPASSWORD || 'dbos';
-
-  let databaseName: string | undefined = configFile.database.app_db_name;
-  const appName = retrieveApplicationName(configFile);
-  if (globalParams.appName === '') {
-    globalParams.appName = appName;
-  }
-  if (databaseName === undefined) {
-    databaseName = appName.toLowerCase().replaceAll('-', '_').replaceAll(' ', '_');
-    if (databaseName.match(/^\d/)) {
-      databaseName = '_' + databaseName; // Append an underscore if the name starts with a digit
+
+  let connectionString = undefined;
+  let databaseName = undefined;
+
+  // If a database_url is found, parse it to backfill the poolConfig
+  if (configFile.database_url) {
+    const url = new URL(configFile.database_url);
+    // If in debug mode, apply the debug overrides
+    if (isDebugMode) {
+      if (process.env.DBOS_DBHOST) {
+        url.hostname = process.env.DBOS_DBHOST;
+      }
+      if (process.env.DBOS_DBPORT) {
+        url.port = process.env.DBOS_DBPORT;
+      }
+      if (process.env.DBOS_DBUSER) {
+        url.username = process.env.DBOS_DBUSER;
+      }
+      if (process.env.DBOS_DBPASSWORD) {
+        url.password = process.env.DBOS_DBPASSWORD;
+      }
+      configFile.database_url = url.toString();
+    }
+    connectionString = configFile.database_url;
+
+    // From here backfill the poolConfig
+
+    const queryParams = Object.fromEntries(url.searchParams.entries());
+
+    // Validate required fields
+    const missingFields: string[] = [];
+    if (!url.username) missingFields.push('username');
+    if (!url.password) missingFields.push('password');
+    if (!url.hostname) missingFields.push('hostname');
+
+    if (missingFields.length > 0) {
+      throw new Error(`Invalid database URL: missing required field(s): ${missingFields.join(', ')}`);
+    }
+
+    // Create database config object
+    configFile.database = {
+      hostname: url.hostname,
+      port: url.port ? parseInt(url.port, 10) : 5432,
+      username: url.username,
+      password: url.password,
+      app_db_name: url.pathname.substring(1) || undefined,
+      ssl: queryParams['sslmode'] === 'require' || queryParams['sslmode'] === 'verify-full',
+      ssl_ca: queryParams['sslrootcert'] || undefined,
+      connectionTimeoutMillis: queryParams['connect_timeout']
+        ? parseInt(queryParams['connect_timeout'], 10) * 1000
+        : 3000,
+    };
+
+    databaseName = configFile.database.app_db_name;
path: src/dbos-runtime/config.ts
user: login: kraftp
body: Oh, I see, you want the database to be optional in the connection string. I don't think we should support this. Only going to cause trouble down the line as we move to only supporting connection strings.
created_at: 2025-04-15T15:24:34Z
author_association: MEMBER
diff_hunk: @@ -142,52 +139,191 @@ function retrieveApplicationName(configFile: ConfigFile): string {
   return appName;
 }
 
+/**
+ * Build a PoolConfig object.
+ *
+ * If configFile.database_url is provided, set it directly in poolConfig.connectionString and backfill the other poolConfig options.
+ * Backfilling allows the rest of the code to access database parameters easily.
+ * We configure the ORMs with the connection string
+ * ORMs accepting a poolConfig object, like pg-node, state the connectionString takes precedence.
+ *
+ * If configFile.database_url is not provided, we build the connection string from the configFile.database object.
+ *
+ * In debug mode, apply overrides from DBOS_DBHOST, DBOS_DBPORT, DBOS_DBUSER, and DBOS_DBPASSWORD.
+ *
+ * Default configuration:
+ * - Hostname: localhost
+ * - Port: 5432
+ * - Username: postgres
+ * - Password: $PGPASSWORD
+ * - Database name: transformed application name. The name is either the one provided in the config file or the one found in package.json.
+ *
+ * @param configFile - The configuration to be used.
+ * @param cliOptions - Optional CLI options.
+ * @returns PoolConfig - The constructed PoolConfig object.
+ */
 export function constructPoolConfig(configFile: ConfigFile, cliOptions?: ParseOptions): PoolConfig {
-  // Load database connection parameters. If they're not in dbos-config.yaml, load from .dbos/db_connection. Else, use defaults.
+  // FIXME: this is not a good place to set the app name
+  const appName = retrieveApplicationName(configFile);
+  if (globalParams.appName === '') {
+    globalParams.appName = appName;
+  }
+
+  const isDebugMode = process.env.DBOS_DEBUG_WORKFLOW_ID !== undefined;
+
   if (!cliOptions?.silent) {
     const logger = new GlobalLogger();
-    if (process.env.DBOS_DBHOST) {
+    if (isDebugMode) {
       logger.info('Loading database connection parameters from debug environment variables');
+    } else if (configFile.database_url) {
+      logger.info('Loading database connection parameters from database_url');
     } else if (configFile.database.hostname) {
       logger.info('Loading database connection parameters from dbos-config.yaml');
     } else {
       logger.info('Using default database connection parameters');
     }
   }
-  configFile.database.hostname = process.env.DBOS_DBHOST || configFile.database.hostname || 'localhost';
-  const dbos_dbport = process.env.DBOS_DBPORT ? parseInt(process.env.DBOS_DBPORT) : undefined;
-  configFile.database.port = dbos_dbport || configFile.database.port || 5432;
-  configFile.database.username = process.env.DBOS_DBUSER || configFile.database.username || 'postgres';
-  configFile.database.password =
-    process.env.DBOS_DBPASSWORD || configFile.database.password || process.env.PGPASSWORD || 'dbos';
-
-  let databaseName: string | undefined = configFile.database.app_db_name;
-  const appName = retrieveApplicationName(configFile);
-  if (globalParams.appName === '') {
-    globalParams.appName = appName;
-  }
-  if (databaseName === undefined) {
-    databaseName = appName.toLowerCase().replaceAll('-', '_').replaceAll(' ', '_');
-    if (databaseName.match(/^\d/)) {
-      databaseName = '_' + databaseName; // Append an underscore if the name starts with a digit
+
+  let connectionString = undefined;
+  let databaseName = undefined;
+
+  // If a database_url is found, parse it to backfill the poolConfig
+  if (configFile.database_url) {
+    const url = new URL(configFile.database_url);
+    // If in debug mode, apply the debug overrides
+    if (isDebugMode) {
+      if (process.env.DBOS_DBHOST) {
+        url.hostname = process.env.DBOS_DBHOST;
+      }
+      if (process.env.DBOS_DBPORT) {
+        url.port = process.env.DBOS_DBPORT;
+      }
+      if (process.env.DBOS_DBUSER) {
+        url.username = process.env.DBOS_DBUSER;
+      }
+      if (process.env.DBOS_DBPASSWORD) {
+        url.password = process.env.DBOS_DBPASSWORD;
+      }
+      configFile.database_url = url.toString();
+    }
+    connectionString = configFile.database_url;
+
+    // From here backfill the poolConfig
+
+    const queryParams = Object.fromEntries(url.searchParams.entries());
+
+    // Validate required fields
+    const missingFields: string[] = [];
+    if (!url.username) missingFields.push('username');
+    if (!url.password) missingFields.push('password');
+    if (!url.hostname) missingFields.push('hostname');
+
+    if (missingFields.length > 0) {
+      throw new Error(`Invalid database URL: missing required field(s): ${missingFields.join(', ')}`);
+    }
+
+    // Create database config object
+    configFile.database = {
+      hostname: url.hostname,
+      port: url.port ? parseInt(url.port, 10) : 5432,
+      username: url.username,
+      password: url.password,
+      app_db_name: url.pathname.substring(1) || undefined,
+      ssl: queryParams['sslmode'] === 'require' || queryParams['sslmode'] === 'verify-full',
+      ssl_ca: queryParams['sslrootcert'] || undefined,
+      connectionTimeoutMillis: queryParams['connect_timeout']
+        ? parseInt(queryParams['connect_timeout'], 10) * 1000
+        : 3000,
+    };
+
+    databaseName = configFile.database.app_db_name;
path: src/dbos-runtime/config.ts
user: login: kraftp
body: Recommendation is to delete this code and make the database required. Will make things easier in the future.

If we keep it optional, then we have to modify the connection string and add the generated database to it for consistency.
created_at: 2025-04-15T15:25:46Z
author_association: MEMBER
diff_hunk: @@ -21,8 +21,15 @@ export async function createDBIfDoesNotExist(poolConfig: PoolConfig, logger: Log
   } catch (error) {
     logger.info(`Database ${poolConfig.database} does not exist, creating...`);
   }
+  // Craft a db string from the app db string, replacing the database name:
+  const pgDbConnectionString = new URL(poolConfig.connectionString!);
+  pgDbConnectionString.pathname = '/postgres';
+
   const app_database = poolConfig.database;
path: src/user_database.ts
user: login: kraftp
body: This mixing of stuff that uses the connection string and stuff that uses the pool config scares me.
created_at: 2025-04-15T15:31:45Z
author_association: MEMBER
diff_hunk: @@ -142,52 +139,191 @@ function retrieveApplicationName(configFile: ConfigFile): string {
   return appName;
 }
 
+/**
+ * Build a PoolConfig object.
+ *
+ * If configFile.database_url is provided, set it directly in poolConfig.connectionString and backfill the other poolConfig options.
+ * Backfilling allows the rest of the code to access database parameters easily.
+ * We configure the ORMs with the connection string
+ * ORMs accepting a poolConfig object, like pg-node, state the connectionString takes precedence.
+ *
+ * If configFile.database_url is not provided, we build the connection string from the configFile.database object.
+ *
+ * In debug mode, apply overrides from DBOS_DBHOST, DBOS_DBPORT, DBOS_DBUSER, and DBOS_DBPASSWORD.
+ *
+ * Default configuration:
+ * - Hostname: localhost
+ * - Port: 5432
+ * - Username: postgres
+ * - Password: $PGPASSWORD
+ * - Database name: transformed application name. The name is either the one provided in the config file or the one found in package.json.
+ *
+ * @param configFile - The configuration to be used.
+ * @param cliOptions - Optional CLI options.
+ * @returns PoolConfig - The constructed PoolConfig object.
+ */
 export function constructPoolConfig(configFile: ConfigFile, cliOptions?: ParseOptions): PoolConfig {
-  // Load database connection parameters. If they're not in dbos-config.yaml, load from .dbos/db_connection. Else, use defaults.
+  // FIXME: this is not a good place to set the app name
+  const appName = retrieveApplicationName(configFile);
+  if (globalParams.appName === '') {
+    globalParams.appName = appName;
+  }
+
+  const isDebugMode = process.env.DBOS_DEBUG_WORKFLOW_ID !== undefined;
+
   if (!cliOptions?.silent) {
     const logger = new GlobalLogger();
-    if (process.env.DBOS_DBHOST) {
+    if (isDebugMode) {
       logger.info('Loading database connection parameters from debug environment variables');
+    } else if (configFile.database_url) {
+      logger.info('Loading database connection parameters from database_url');
     } else if (configFile.database.hostname) {
       logger.info('Loading database connection parameters from dbos-config.yaml');
     } else {
       logger.info('Using default database connection parameters');
     }
   }
-  configFile.database.hostname = process.env.DBOS_DBHOST || configFile.database.hostname || 'localhost';
-  const dbos_dbport = process.env.DBOS_DBPORT ? parseInt(process.env.DBOS_DBPORT) : undefined;
-  configFile.database.port = dbos_dbport || configFile.database.port || 5432;
-  configFile.database.username = process.env.DBOS_DBUSER || configFile.database.username || 'postgres';
-  configFile.database.password =
-    process.env.DBOS_DBPASSWORD || configFile.database.password || process.env.PGPASSWORD || 'dbos';
-
-  let databaseName: string | undefined = configFile.database.app_db_name;
-  const appName = retrieveApplicationName(configFile);
-  if (globalParams.appName === '') {
-    globalParams.appName = appName;
-  }
-  if (databaseName === undefined) {
-    databaseName = appName.toLowerCase().replaceAll('-', '_').replaceAll(' ', '_');
-    if (databaseName.match(/^\d/)) {
-      databaseName = '_' + databaseName; // Append an underscore if the name starts with a digit
+
+  let connectionString = undefined;
+  let databaseName = undefined;
+
+  // If a database_url is found, parse it to backfill the poolConfig
+  if (configFile.database_url) {
+    const url = new URL(configFile.database_url);
+    // If in debug mode, apply the debug overrides
+    if (isDebugMode) {
+      if (process.env.DBOS_DBHOST) {
+        url.hostname = process.env.DBOS_DBHOST;
+      }
+      if (process.env.DBOS_DBPORT) {
+        url.port = process.env.DBOS_DBPORT;
+      }
+      if (process.env.DBOS_DBUSER) {
+        url.username = process.env.DBOS_DBUSER;
+      }
+      if (process.env.DBOS_DBPASSWORD) {
+        url.password = process.env.DBOS_DBPASSWORD;
+      }
+      configFile.database_url = url.toString();
+    }
+    connectionString = configFile.database_url;
+
+    // From here backfill the poolConfig
+
+    const queryParams = Object.fromEntries(url.searchParams.entries());
+
+    // Validate required fields
+    const missingFields: string[] = [];
+    if (!url.username) missingFields.push('username');
+    if (!url.password) missingFields.push('password');
+    if (!url.hostname) missingFields.push('hostname');
+
+    if (missingFields.length > 0) {
+      throw new Error(`Invalid database URL: missing required field(s): ${missingFields.join(', ')}`);
+    }
+
+    // Create database config object
+    configFile.database = {
+      hostname: url.hostname,
+      port: url.port ? parseInt(url.port, 10) : 5432,
+      username: url.username,
+      password: url.password,
+      app_db_name: url.pathname.substring(1) || undefined,
+      ssl: queryParams['sslmode'] === 'require' || queryParams['sslmode'] === 'verify-full',
+      ssl_ca: queryParams['sslrootcert'] || undefined,
+      connectionTimeoutMillis: queryParams['connect_timeout']
+        ? parseInt(queryParams['connect_timeout'], 10) * 1000
+        : 3000,
+    };
+
+    databaseName = configFile.database.app_db_name;
path: src/dbos-runtime/config.ts
user: login: maxdml
body: I had this train of thoughts earlier. I was worried about 1-week backward compatibility with people not providing a dbname.
created_at: 2025-04-15T15:51:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,8 +21,15 @@ export async function createDBIfDoesNotExist(poolConfig: PoolConfig, logger: Log
   } catch (error) {
     logger.info(`Database ${poolConfig.database} does not exist, creating...`);
   }
+  // Craft a db string from the app db string, replacing the database name:
+  const pgDbConnectionString = new URL(poolConfig.connectionString!);
+  pgDbConnectionString.pathname = '/postgres';
+
   const app_database = poolConfig.database;
path: src/user_database.ts
user: login: maxdml
body: updated to find the db name from the connection string.
created_at: 2025-04-16T21:01:52Z
author_association: CONTRIBUTOR

PR comments:
user: login: maxdml
created_at: 2025-04-12T00:26:13Z
body: > I'm very confused by this PR. The idea, as I understood it, was to move entirely towards connection strings. If one is provided, pass it directly to the ORM (they all fully support connection strings, as far as I'm aware). If no connection string is provided, construct a connection string from the `dbos-config.yaml` parameters.
> 
> That doesn't seem to be what's happening here--as far as I can tell, we're still parsing connection strings into database configs (why is `parseDbString` still around?) and then parsing them back into database strings in `constructPoolConfig` (potentially losing information in the process).

The reason for doing a parse connection string + rebuild is to guarantee consistency between our internal fields and the provided connection string. Crucially the connection parameters are given as is from the provided connection string (the "rebuild" part is just for the base user:password@host:port)

We also need to allow DBOS_DBxyz overrides for the debugger to work. In that specific aspect, this design makes things very future proof if we want to support more native postgres env var, without compromising correctness.

The ORMS now use the connection string.

One thing to note: knex/typeorm seem to want being given pool size / connect timeout in separate parameters. The documentation is not exactly clear on the topic and I think it is safer to given them explicitly.
author_association: CONTRIBUTOR
user: login: kraftp
created_at: 2025-04-12T00:54:36Z
body: > The reason for doing a parse connection string + rebuild is to guarantee consistency between our internal fields and the provided connection string. Crucially the connection parameters are given as is from the provided connection string (the "rebuild" part is just for the base user:password@host:port)
> 
> We also need to allow DBOS_DBxyz overrides for the debugger to work. In that specific aspect, this design makes things very future proof if we want to support more native postgres env var, without compromising correctness.
> 
> The ORMS now use the connection string.
> 
> One thing to note: knex/typeorm seem to want being given pool size / connect timeout in separate parameters. The documentation is not exactly clear on the topic and I think it is safer to given them explicitly.

Got it!  In that case, let's make this explicit by merging `parseDbString` into `constructPoolConfig`. Let's also NOT modify user-provided connection strings AT ALL except for debugger and cloud overrides.
author_association: MEMBER
user: login: maxdml
created_at: 2025-04-16T21:01:34Z
body: > We need to get rid of the backfilling

I removed as much backfill as possible for now.
author_association: CONTRIBUTOR