diff --git a/apps/studio/components/interfaces/App/AppBannerWrapper.tsx b/apps/studio/components/interfaces/App/AppBannerWrapper.tsx
index 7a19a898f9..7ae2be788e 100644
--- a/apps/studio/components/interfaces/App/AppBannerWrapper.tsx
+++ b/apps/studio/components/interfaces/App/AppBannerWrapper.tsx
@@ -2,6 +2,7 @@ import { useMonaco } from '@monaco-editor/react'
import { useTheme } from 'next-themes'
import { PropsWithChildren, useMemo } from 'react'
+import { ClockSkewBanner } from 'components/layouts/AppLayout/ClockSkewBanner'
import IncidentBanner from 'components/layouts/AppLayout/IncidentBanner'
import { NoticeBanner } from 'components/layouts/AppLayout/NoticeBanner'
import { RestrictionBanner } from 'components/layouts/AppLayout/RestrictionBanner'
@@ -16,6 +17,7 @@ const AppBannerWrapper = ({ children }: PropsWithChildren<{}>) => {
const ongoingIncident = useFlag('ongoingIncident')
const showNoticeBanner = useFlag('showNoticeBanner')
+ const clockSkewBanner = useFlag('clockSkewBanner')
// Define the supabase theme for Monaco before anything is rendered. Using useEffect would sometime load the theme
// after the editor was loaded, so it looked off. useMemo will always be run before rendering
@@ -32,6 +34,7 @@ const AppBannerWrapper = ({ children }: PropsWithChildren<{}>) => {
{ongoingIncident && }
{showNoticeBanner && }
{profile !== undefined && }
+ {clockSkewBanner && }
{children}
diff --git a/apps/studio/components/layouts/AppLayout/ClockSkewBanner.tsx b/apps/studio/components/layouts/AppLayout/ClockSkewBanner.tsx
new file mode 100644
index 0000000000..2cc9cc500a
--- /dev/null
+++ b/apps/studio/components/layouts/AppLayout/ClockSkewBanner.tsx
@@ -0,0 +1,58 @@
+import { BASE_PATH } from 'lib/constants'
+import { useCallback, useEffect, useState } from 'react'
+import { Button } from 'ui'
+
+// Show the banner if the clock skew is greater than 2 minutes
+const CLOCK_SKEW_THRESHOLD = 2 * 60 * 1000
+// check every 5 minutes
+const CLOCK_SKEW_CHECK_INTERVAL = 30 * 60 * 1000
+
+const isClockSkewed = async () => {
+ try {
+ const response = await fetch(`${BASE_PATH}/api/get-utc-time`)
+ const data = await response.json()
+ // The received time is in UTC timezone, add Z at the end to make JS understand that
+ const serverTime = new Date(data.utcTime).getTime()
+ const clientTime = new Date().getTime()
+ const clockSkew = Math.abs(clientTime - serverTime)
+
+ return clockSkew > CLOCK_SKEW_THRESHOLD
+ } catch {
+ return false
+ }
+}
+
+export const ClockSkewBanner = () => {
+ const [clockSkew, setClockSkew] = useState(false)
+
+ const checkClockSkew = useCallback(async () => {
+ const value = await isClockSkewed()
+ setClockSkew(value)
+ }, [])
+
+ useEffect(() => {
+ // check for clock skew every CLOCK_SKEW_CHECK_INTERVAL
+ checkClockSkew()
+ const interval = setInterval(checkClockSkew, CLOCK_SKEW_CHECK_INTERVAL)
+
+ return () => clearInterval(interval)
+ }, [checkClockSkew])
+
+ if (!clockSkew) return null
+
+ return (
+
+
+ Your computer's clock appears to be inaccurate. This can cause issues with certain features.
+
+
+
+ )
+}
diff --git a/apps/studio/components/layouts/AppLayout/RestrictionBanner.tsx b/apps/studio/components/layouts/AppLayout/RestrictionBanner.tsx
index c449ea96d2..78c18f5148 100644
--- a/apps/studio/components/layouts/AppLayout/RestrictionBanner.tsx
+++ b/apps/studio/components/layouts/AppLayout/RestrictionBanner.tsx
@@ -3,7 +3,7 @@ import Link from 'next/link'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
-import { AlertTitle_Shadcn_, Alert_Shadcn_, Button, CriticalIcon, WarningIcon } from 'ui'
+import { AlertTitle_Shadcn_, Alert_Shadcn_, Button, CriticalIcon, WarningIcon, cn } from 'ui'
/**
* Shown on projects in organization which are above their qouta
@@ -18,7 +18,10 @@ export const RestrictionBanner = () => {
return (
{currentOrg.restriction_status === 'restricted' ? : }
diff --git a/apps/studio/middleware.ts b/apps/studio/middleware.ts
index b3ae6f5214..11f161156b 100644
--- a/apps/studio/middleware.ts
+++ b/apps/studio/middleware.ts
@@ -17,10 +17,10 @@ const HOSTED_SUPPORTED_API_URLS = [
'/ai/sql/cron',
'/ai/docs',
'/get-ip-address',
+ '/get-utc-time',
]
export function middleware(request: NextRequest) {
- const url = request.url
if (IS_PLATFORM && !HOSTED_SUPPORTED_API_URLS.some((url) => request.url.endsWith(url))) {
return Response.json(
{ success: false, message: 'Endpoint not supported on hosted' },
diff --git a/apps/studio/pages/api/get-utc-time.ts b/apps/studio/pages/api/get-utc-time.ts
new file mode 100644
index 0000000000..0945a10c1f
--- /dev/null
+++ b/apps/studio/pages/api/get-utc-time.ts
@@ -0,0 +1,11 @@
+import { NextApiRequest, NextApiResponse } from 'next'
+
+// Returns the current UTC time in ISO format. Used to check if the client and server time are skewed. Clock skew causes
+// issues with JWT verification.
+const handler = async (req: NextApiRequest, res: NextApiResponse) => {
+ const utcTime = new Date().toISOString()
+
+ return res.status(200).json({ utcTime })
+}
+
+export default handler