diff --git a/.changeset/embedded-postgres-connection-cap.md b/.changeset/embedded-postgres-connection-cap.md new file mode 100644 index 0000000000..1c5f572f89 --- /dev/null +++ b/.changeset/embedded-postgres-connection-cap.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Let operators set the embedded PostgreSQL connection cap in Advanced Settings. +category: feature +dev: External PostgreSQL runtime pools now default to 3 connections; embedded PostgreSQL reads the global cap on restart. diff --git a/packages/core/src/__tests__/settings-defaults.test.ts b/packages/core/src/__tests__/settings-defaults.test.ts index 04dfc78ef6..b055b6ad2e 100644 --- a/packages/core/src/__tests__/settings-defaults.test.ts +++ b/packages/core/src/__tests__/settings-defaults.test.ts @@ -36,6 +36,12 @@ describe("settings defaults invariants", () => { expect(isGlobalOnlySettingsKey("localNetworkDiscoveryEnabled")).toBe(true); }); + it("keeps the embedded PostgreSQL connection cap global and high by default", () => { + expect(DEFAULT_GLOBAL_SETTINGS.embeddedPostgresMaxConnections).toBe(500); + expect(GLOBAL_SETTINGS_KEYS).toContain("embeddedPostgresMaxConnections"); + expect(PROJECT_SETTINGS_KEYS).not.toContain("embeddedPostgresMaxConnections"); + }); + it("defaults dashboard keyboard shortcuts globally", () => { expect(DEFAULT_GLOBAL_SETTINGS.dashboardKeyboardShortcuts).toEqual({ quickChat: "Space", diff --git a/packages/core/src/postgres/connection.ts b/packages/core/src/postgres/connection.ts index cfb4420f64..5e08ca6ea8 100644 --- a/packages/core/src/postgres/connection.ts +++ b/packages/core/src/postgres/connection.ts @@ -43,7 +43,10 @@ const log = createLogger("postgres-connection"); * The embedded mode may use an even smaller pool. These can be tuned via * environment variables if needed. */ -const DEFAULT_POOL_MAX = 10; +// External databases frequently impose a low connection quota. Each Fusion +// store owns a runtime pool plus a migration session, so keep the default +// deliberately small; callers with a known capacity can still override it. +const DEFAULT_POOL_MAX = 3; const DEFAULT_CONNECT_TIMEOUT_SECONDS = 10; const DEFAULT_IDLE_TIMEOUT_SECONDS = 20; diff --git a/packages/core/src/postgres/startup-factory.ts b/packages/core/src/postgres/startup-factory.ts index 67f5d1318c..e2281977d6 100644 --- a/packages/core/src/postgres/startup-factory.ts +++ b/packages/core/src/postgres/startup-factory.ts @@ -308,7 +308,7 @@ async function isEmptyNonUtf8Cluster(db: PostgresJsDatabase, + options: Pick, bypassProjectIsolation = false, ): Promise { try { @@ -326,7 +326,7 @@ async function bootSchemaBackend( } async function bootSchemaBackendOnce( - options: Pick, + options: Pick, bypassProjectIsolation = false, ): Promise { const env = options.env ?? process.env; @@ -347,12 +347,23 @@ async function bootSchemaBackendOnce( if (backend.mode === "embedded") { const { EmbeddedPostgresLifecycle, defaultEmbeddedDataDir, DEFAULT_EMBEDDED_DATABASE } = await import("./embedded-lifecycle.js"); + const { GlobalSettingsStore } = await import("../global-settings.js"); + // Tests that boot an embedded backend intentionally do not have an + // operator global-settings directory. Production always reads the shared + // global preference before it starts the server. + const configuredMaxConnections = process.env.VITEST === "true" && !options.globalSettingsDir + ? undefined + : (await new GlobalSettingsStore(options.globalSettingsDir).getSettings()).embeddedPostgresMaxConnections; + const maxConnections = Number.isInteger(configuredMaxConnections) + ? Math.min(2_000, Math.max(32, configuredMaxConnections!)) + : 500; const dataDir = resolve(options.embeddedDataDir ?? defaultEmbeddedDataDir()); embeddedDataDir = dataDir; log.log(`startup-factory: starting embedded PostgreSQL (data dir ${dataDir})`); embeddedLifecycle = new EmbeddedPostgresLifecycle({ dataDir, database: DEFAULT_EMBEDDED_DATABASE, + postgresFlags: ["-c", `max_connections=${maxConnections}`], onLog: (message) => log.log(message), onError: (error) => log.error(String(error)), }); diff --git a/packages/core/src/settings-schema.ts b/packages/core/src/settings-schema.ts index bbb7b2a88f..9c984509e4 100644 --- a/packages/core/src/settings-schema.ts +++ b/packages/core/src/settings-schema.ts @@ -72,6 +72,10 @@ type ProjectSettingsSchema = Omit> = { /* FNXC:SettingsBackups 2026-07-16-14:35: shared PostgreSQL backup policy may only write through its global Database Backups section. */ - "backups-global": new Set(["autoBackupEnabled", "autoBackupSchedule", "autoBackupRetention", "autoBackupDir"]), + "backups-global": new Set(["autoBackupEnabled", "autoBackupSchedule", "autoBackupRetention", "autoBackupDir", "embeddedPostgresMaxConnections"]), appearance: new Set([ "themeMode", "colorTheme", diff --git a/packages/dashboard/app/components/settings/sections/DatabaseBackupsSection.tsx b/packages/dashboard/app/components/settings/sections/DatabaseBackupsSection.tsx index 8410e8057f..1c1c52aa0b 100644 --- a/packages/dashboard/app/components/settings/sections/DatabaseBackupsSection.tsx +++ b/packages/dashboard/app/components/settings/sections/DatabaseBackupsSection.tsx @@ -44,6 +44,24 @@ export function DatabaseBackupsSection({ form, setForm, backupInfo, backupLoadin )} }

{t("settings.backups.databaseBackups", "Database Backups")}

+
+ {t("settings.database.advanced", "Advanced database settings")} + setForm((f) => ({ ...f, embeddedPostgresMaxConnections: v ?? 500 }))} + error={form.embeddedPostgresMaxConnections !== undefined && (form.embeddedPostgresMaxConnections < 32 || form.embeddedPostgresMaxConnections > 2000) + ? t("settings.database.embeddedConnectionCapError", "Enter a value between 32 and 2,000.") + : undefined} + /> +