diff --git a/apps/studio/components/interfaces/Database/RestoreToNewProject/RestoreToNewProject.tsx b/apps/studio/components/interfaces/Database/RestoreToNewProject/RestoreToNewProject.tsx
index d551554ea05..b829edbfdf3 100644
--- a/apps/studio/components/interfaces/Database/RestoreToNewProject/RestoreToNewProject.tsx
+++ b/apps/studio/components/interfaces/Database/RestoreToNewProject/RestoreToNewProject.tsx
@@ -30,6 +30,7 @@ import {
useIsAwsK8sCloudProvider,
useIsHighAvailability,
useIsOrioleDb,
+ useOrioleDbReleaseStage,
useSelectedProjectQuery,
} from '@/hooks/misc/useSelectedProject'
import { DOCS_URL, PROJECT_STATUS } from '@/lib/constants'
@@ -41,6 +42,7 @@ export const RestoreToNewProject = () => {
const { hasAccess: hasAccessToRestoreToNewProject, isLoading: isLoadingEntitlement } =
useCheckEntitlements('backup.restore_to_new_project')
const isOrioleDb = useIsOrioleDb()
+ const orioleDbReleaseStage = useOrioleDbReleaseStage()
const isAwsK8s = useIsAwsK8sCloudProvider()
const isHighAvailability = useIsHighAvailability()
@@ -130,7 +132,7 @@ export const RestoreToNewProject = () => {
)
}
diff --git a/apps/studio/data/database/backups-query.ts b/apps/studio/data/database/backups-query.ts
index c1bb3e8b02b..8cdf819cd8e 100644
--- a/apps/studio/data/database/backups-query.ts
+++ b/apps/studio/data/database/backups-query.ts
@@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query'
import { databaseKeys } from './keys'
import type { components } from '@/data/api'
import { get, handleError } from '@/data/fetchers'
-import { useIsOrioleDbInAws } from '@/hooks/misc/useSelectedProject'
+import { useIsOrioleDbInAws, useOrioleDbReleaseStage } from '@/hooks/misc/useSelectedProject'
import { PROJECT_STATUS } from '@/lib/constants'
import type { ResponseError, UseCustomQueryOptions } from '@/types'
@@ -35,13 +35,14 @@ export const useBackupsQuery = (
) => {
// [Joshen] Check for specifically false to account for project not loaded yet
const isOrioleDbInAws = useIsOrioleDbInAws()
+ const orioleDbReleaseStage = useOrioleDbReleaseStage()
return useQuery({
queryKey: databaseKeys.backups(projectRef),
queryFn: ({ signal }) => getBackups({ projectRef }, signal),
enabled:
enabled &&
- !isOrioleDbInAws &&
+ !(isOrioleDbInAws && orioleDbReleaseStage === 'alpha') &&
typeof projectRef !== 'undefined' &&
projectStatus !== PROJECT_STATUS.COMING_UP &&
projectStatus !== PROJECT_STATUS.UNKNOWN,
diff --git a/apps/studio/hooks/misc/useSelectedProject.ts b/apps/studio/hooks/misc/useSelectedProject.ts
index 4fd2d5fd894..84757654077 100644
--- a/apps/studio/hooks/misc/useSelectedProject.ts
+++ b/apps/studio/hooks/misc/useSelectedProject.ts
@@ -1,5 +1,10 @@
import { useParams } from 'common'
+import {
+ isOrioleDbVersionAtLeast,
+ ORIOLEDB_PUBLIC_BETA_VERSION,
+ type OrioleDbReleaseStage,
+} from './useSelectedProject.utils'
import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
import { PROJECT_STATUS, PROVIDERS } from '@/lib/constants'
@@ -54,6 +59,16 @@ export const useIsOrioleDbInAws = () => {
return isOrioleDbInAws
}
+// OrioleDB moved from Public Alpha to Public Beta at engine version 17.11.0.001.
+// PITR remains unsupported for every OrioleDB version regardless of stage.
+export const useOrioleDbReleaseStage = (): OrioleDbReleaseStage | undefined => {
+ const { data: project } = useSelectedProjectQuery()
+ if (!project?.dbVersion?.endsWith('orioledb')) return undefined
+ return isOrioleDbVersionAtLeast(project.dbVersion, ORIOLEDB_PUBLIC_BETA_VERSION)
+ ? 'beta'
+ : 'alpha'
+}
+
// [Joshen TODO] There's a duplicate method `resolveHighAvailability` in `useHighAvailability.constants`
export const useIsHighAvailability = () => {
const { data: project } = useSelectedProjectQuery()
diff --git a/apps/studio/hooks/misc/useSelectedProject.utils.test.ts b/apps/studio/hooks/misc/useSelectedProject.utils.test.ts
new file mode 100644
index 00000000000..20d9f58cc04
--- /dev/null
+++ b/apps/studio/hooks/misc/useSelectedProject.utils.test.ts
@@ -0,0 +1,151 @@
+import { describe, expect, it } from 'vitest'
+
+import {
+ appVersionAtLeast,
+ compareAppVersions,
+ isOrioleDbVersionAtLeast,
+ parseAppVersionKey,
+} from './useSelectedProject.utils'
+
+describe('parseAppVersionKey', () => {
+ it('parses a dbVersion string into a version key', () => {
+ expect(parseAppVersionKey('supabase-postgres-17.9.9.999-orioledb')).toEqual([17, 9, 9, 999])
+ })
+
+ it('parses dbVersion strings with an arch prefix', () => {
+ expect(parseAppVersionKey('supabase-postgres-arm64-17.9.0.019')).toEqual([17, 9, 0, 19])
+ expect(parseAppVersionKey('supabase-postgres-x86_64-17.9.0.019')).toEqual([17, 9, 0, 19])
+ })
+
+ it('returns null for non-conforming strings', () => {
+ expect(parseAppVersionKey('CUSTOM_AMI_SENTINEL')).toBeNull()
+ expect(parseAppVersionKey('not-a-version')).toBeNull()
+ })
+
+ it('returns null for null or undefined input', () => {
+ expect(parseAppVersionKey(null)).toBeNull()
+ expect(parseAppVersionKey(undefined)).toBeNull()
+ })
+})
+
+describe('compareAppVersions', () => {
+ it('returns a negative number when a is older than b', () => {
+ expect(
+ compareAppVersions('supabase-postgres-17.8.9.999', 'supabase-postgres-17.9.0.019')
+ ).toBeLessThan(0)
+ })
+
+ it('returns zero when versions are equal', () => {
+ expect(compareAppVersions('supabase-postgres-17.9.0.019', 'supabase-postgres-17.9.0.019')).toBe(
+ 0
+ )
+ })
+
+ it('returns a positive number when a is newer than b', () => {
+ expect(
+ compareAppVersions('supabase-postgres-17.11.0.001', 'supabase-postgres-17.9.0.019')
+ ).toBeGreaterThan(0)
+ })
+
+ it('compares numerically rather than by segment digit-width', () => {
+ // Regression case for the digit-concatenation bug in lib/helpers.ts'
+ // getSemanticVersion, where '15.1.1.2' (-> '15112') compares as less than
+ // '14.1.0.44' (-> '141044') despite being the newer version.
+ expect(
+ compareAppVersions('supabase-postgres-15.1.1.2', 'supabase-postgres-14.1.0.44')
+ ).toBeGreaterThan(0)
+ })
+
+ it('throws on unparseable input', () => {
+ expect(() => compareAppVersions('not-a-version', 'supabase-postgres-17.9.0.019')).toThrow()
+ expect(() => compareAppVersions('supabase-postgres-17.9.0.019', 'not-a-version')).toThrow()
+ })
+})
+
+describe('appVersionAtLeast', () => {
+ it('returns true when the version is above or equal to the threshold', () => {
+ expect(appVersionAtLeast('supabase-postgres-17.9.9.999', 'supabase-postgres-17.9.0.019')).toBe(
+ true
+ )
+ expect(appVersionAtLeast('supabase-postgres-17.9.0.019', 'supabase-postgres-17.9.0.019')).toBe(
+ true
+ )
+ })
+
+ it('returns false when the version is below the threshold', () => {
+ expect(appVersionAtLeast('supabase-postgres-17.8.9.999', 'supabase-postgres-17.9.0.019')).toBe(
+ false
+ )
+ })
+
+ it('returns false for an unparseable version instead of throwing', () => {
+ expect(appVersionAtLeast('not-a-version', 'supabase-postgres-17.9.0.019')).toBe(false)
+ })
+})
+
+describe('isOrioleDbVersionAtLeast', () => {
+ it('returns true when the engine version is above the threshold', () => {
+ expect(
+ isOrioleDbVersionAtLeast(
+ 'supabase-postgres-17.11.0.002-orioledb',
+ 'supabase-postgres-17.11.0.001-orioledb'
+ )
+ ).toBe(true)
+ expect(
+ isOrioleDbVersionAtLeast(
+ 'supabase-postgres-17.12.0.001-orioledb',
+ 'supabase-postgres-17.11.0.001-orioledb'
+ )
+ ).toBe(true)
+ })
+
+ it('returns true when the engine version equals the threshold', () => {
+ expect(
+ isOrioleDbVersionAtLeast(
+ 'supabase-postgres-17.11.0.001-orioledb',
+ 'supabase-postgres-17.11.0.001-orioledb'
+ )
+ ).toBe(true)
+ })
+
+ it('returns false when the engine version is below the threshold', () => {
+ expect(
+ isOrioleDbVersionAtLeast(
+ 'supabase-postgres-17.10.9.999-orioledb',
+ 'supabase-postgres-17.11.0.001-orioledb'
+ )
+ ).toBe(false)
+ // wal-g (scheduled backup) support landed at 17.9.0.019, which is still
+ // below the Public Beta cutoff — these projects remain alpha.
+ expect(
+ isOrioleDbVersionAtLeast(
+ 'supabase-postgres-17.9.0.019-orioledb',
+ 'supabase-postgres-17.11.0.001-orioledb'
+ )
+ ).toBe(false)
+ })
+
+ it('returns false for a non-OrioleDB dbVersion', () => {
+ expect(
+ isOrioleDbVersionAtLeast(
+ 'supabase-postgres-17.11.0.001',
+ 'supabase-postgres-17.11.0.001-orioledb'
+ )
+ ).toBe(false)
+ })
+
+ it('returns false for undefined dbVersion', () => {
+ expect(isOrioleDbVersionAtLeast(undefined, 'supabase-postgres-17.11.0.001-orioledb')).toBe(
+ false
+ )
+ })
+
+ it('returns false for a non-OrioleDB threshold', () => {
+ expect(
+ isOrioleDbVersionAtLeast(
+ 'supabase-postgres-17.11.0.001-orioledb',
+ 'supabase-postgres-17.11.0.001'
+ )
+ ).toBe(false)
+ })
+})
diff --git a/apps/studio/hooks/misc/useSelectedProject.utils.ts b/apps/studio/hooks/misc/useSelectedProject.utils.ts
new file mode 100644
index 00000000000..d61ad9a8a08
--- /dev/null
+++ b/apps/studio/hooks/misc/useSelectedProject.utils.ts
@@ -0,0 +1,42 @@
+export type OrioleDbReleaseStage = 'alpha' | 'beta'
+
+// OrioleDB engine versions ship as 4-part strings (e.g. `17.11.0.001`), which
+// `lib/semver` doesn't parse (it's strict 3-part semver), so compare them separately here.
+export const ORIOLEDB_PUBLIC_BETA_VERSION = 'supabase-postgres-17.11.0.001-orioledb'
+
+// Sortable [major, minor, patch, build] key parsed from a dbVersion string like
+// `supabase-postgres-17.11.0.001-orioledb` or `supabase-postgres-arm64-17.11.0.001`.
+const APP_VERSION_KEY_RE = /^supabase-postgres-(?:arm64-|x86_64-)?(\d+)\.(\d+)\.(\d+)\.(\d+)/
+
+export type AppVersionKey = [number, number, number, number]
+
+// Returns null for non-conforming strings — callers should treat null as "not orderable".
+export function parseAppVersionKey(version: string | null | undefined): AppVersionKey | null {
+ if (!version) return null
+
+ const match = APP_VERSION_KEY_RE.exec(version)
+ if (!match) return null
+
+ return [Number(match[1]), Number(match[2]), Number(match[3]), Number(match[4])]
+}
+
+// Negative when `a` is older, zero when equal, positive when newer.
+// Throws on unparseable input — null-tolerant callers parse first.
+export function compareAppVersions(a: string, b: string): number {
+ const keyA = parseAppVersionKey(a)
+ const keyB = parseAppVersionKey(b)
+ if (!keyA) throw new Error(`unparseable app_version: ${a}`)
+ if (!keyB) throw new Error(`unparseable app_version: ${b}`)
+
+ return keyA[0] - keyB[0] || keyA[1] - keyB[1] || keyA[2] - keyB[2] || keyA[3] - keyB[3]
+}
+
+export function appVersionAtLeast(version: string, threshold: string): boolean {
+ if (!parseAppVersionKey(version)) return false
+ return compareAppVersions(version, threshold) >= 0
+}
+
+export const isOrioleDbVersionAtLeast = (dbVersion: string | undefined, threshold: string) => {
+ if (!dbVersion?.endsWith('orioledb') || !threshold.endsWith('orioledb')) return false
+ return appVersionAtLeast(dbVersion, threshold)
+}
diff --git a/apps/studio/pages/project/[ref]/database/backups/pitr.tsx b/apps/studio/pages/project/[ref]/database/backups/pitr.tsx
index 26bd2ac0649..220456873ad 100644
--- a/apps/studio/pages/project/[ref]/database/backups/pitr.tsx
+++ b/apps/studio/pages/project/[ref]/database/backups/pitr.tsx
@@ -28,7 +28,11 @@ import { useBackupsQuery } from '@/data/database/backups-query'
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useHighAvailability } from '@/hooks/misc/useHighAvailability'
-import { useIsOrioleDbInAws, useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
+import {
+ useIsOrioleDbInAws,
+ useOrioleDbReleaseStage,
+ useSelectedProjectQuery,
+} from '@/hooks/misc/useSelectedProject'
import { DOCS_URL, PROJECT_STATUS } from '@/lib/constants'
import type { NextPageWithLayout } from '@/types'
@@ -71,6 +75,7 @@ const PITR = () => {
const { hasAccess: hasAccessToPitr, isLoading: isLoadingEntitlements } =
useCheckEntitlements('pitr.available_variants')
const isOrioleDbInAws = useIsOrioleDbInAws()
+ const orioleDbReleaseStage = useOrioleDbReleaseStage()
const {
data: backups,
error,
@@ -96,8 +101,8 @@ const PITR = () => {
return (
diff --git a/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx b/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx
index 4ac3a7a045c..3d1a47e37b1 100644
--- a/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx
+++ b/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx
@@ -1,6 +1,7 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { DatabaseBackup, Info } from 'lucide-react'
+import { Admonition } from 'ui-patterns/Admonition'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
PageHeader,
@@ -17,12 +18,17 @@ import DatabaseBackupsNav from '@/components/interfaces/Database/Backups/Databas
import { DatabaseLayout } from '@/components/layouts/DatabaseLayout/DatabaseLayout'
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
import { AlertError } from '@/components/ui/AlertError'
+import { DocsButton } from '@/components/ui/DocsButton'
import { HighAvailabilityDisabledEmptyState } from '@/components/ui/HighAvailability/HighAvailabilityDisabledEmptyState'
import InformationBox from '@/components/ui/InformationBox'
import { NoPermission } from '@/components/ui/NoPermission'
import { useBackupsQuery } from '@/data/database/backups-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
-import { useIsHighAvailability } from '@/hooks/misc/useSelectedProject'
+import {
+ useIsHighAvailability,
+ useIsOrioleDbInAws,
+ useOrioleDbReleaseStage,
+} from '@/hooks/misc/useSelectedProject'
import { DOCS_URL } from '@/lib/constants'
import type { NextPageWithLayout } from '@/types'
@@ -62,6 +68,8 @@ const ScheduledBackups = () => {
} = useBackupsQuery({ projectRef })
const isHighAvailability = useIsHighAvailability()
+ const isOrioleDbInAws = useIsOrioleDbInAws()
+ const orioleDbReleaseStage = useOrioleDbReleaseStage()
const isPitrEnabled = backups?.pitr_enabled
const { can: canReadScheduledBackups, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
@@ -69,6 +77,18 @@ const ScheduledBackups = () => {
'back_ups'
)
+ if (isOrioleDbInAws && orioleDbReleaseStage === 'alpha') {
+ return (
+
+
+
+ )
+ }
+
if (isHighAvailability) {
return (