diff --git a/apps/docs/content/guides/platform/billing-on-supabase.mdx b/apps/docs/content/guides/platform/billing-on-supabase.mdx index 54293ddd95284..868a0f87059d2 100644 --- a/apps/docs/content/guides/platform/billing-on-supabase.mdx +++ b/apps/docs/content/guides/platform/billing-on-supabase.mdx @@ -64,9 +64,9 @@ The quota is applied to your entire organization, independent of how many projec | Usage Item | Free | Pro/Team | Enterprise | | -------------------------------- | ------------------------ | ------------------------------------------------------------------- | ---------- | | Egress | 5 GB | 250 GB included, then per GB | Custom | -| Database Size | 500 MB | 8 GB disk per project included, then per GB | Custom | +| Database Size | 500 MB per project | 8 GB disk per project included, then per GB | Custom | | Monthly Active Users | 50,000 MAU | 100,000 MAU included, then per MAU | Custom | -| Monthly Active Third-Party Users | 50 MAU | 50 MAU included, then per MAU | Custom | +| Monthly Active Third-Party Users | 50,000 MAU | 100,000 MAU included, then per MAU | Custom | | Monthly Active SSO Users | Unavailable on Free Plan | 50 MAU included, then per MAU | Custom | | Storage Size | 1 GB | 100 GB included, then per GB | Custom | | Storage Images Transformed | Unavailable on Free Plan | 100 included, then per 1000 | Custom | diff --git a/apps/docs/content/guides/platform/manage-your-usage/edge-function-invocations.mdx b/apps/docs/content/guides/platform/manage-your-usage/edge-function-invocations.mdx index db31cb0d45693..9109b2d3261a0 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/edge-function-invocations.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/edge-function-invocations.mdx @@ -5,7 +5,7 @@ title: 'Manage Edge Function Invocations usage' ## What you are charged for -You are charged for the number of times your functions get invoked, regardless of the response status code. +You are charged for the number of times your functions get invoked, regardless of the response status code. Preflight (OPTIONS) requests are not billed. ## How charges are calculated diff --git a/apps/docs/lib/userAuth.ts b/apps/docs/lib/userAuth.ts index 12fec885fe721..973c5eb8bb177 100644 --- a/apps/docs/lib/userAuth.ts +++ b/apps/docs/lib/userAuth.ts @@ -1,6 +1,11 @@ -import { gotrueClient } from 'common' +import * as Sentry from '@sentry/nextjs' +import { gotrueClient, setCaptureException } from 'common' import { useEffect } from 'react' +setCaptureException((e: any) => { + Sentry.captureException(e) +}) + export const auth = gotrueClient export async function getAccessToken() { diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreview.constants.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreview.constants.tsx index 9a56fd87b21e4..03d791ca8914e 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreview.constants.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreview.constants.tsx @@ -1,6 +1,13 @@ import { LOCAL_STORAGE_KEYS } from 'common' export const FEATURE_PREVIEWS = [ + { + key: LOCAL_STORAGE_KEYS.UI_PREVIEW_SECURITY_NOTIFICATIONS, + name: 'Security notification templates', + discussionsUrl: undefined, + isNew: true, + isPlatformOnly: true, + }, { key: LOCAL_STORAGE_KEYS.UI_PREVIEW_NEW_STORAGE_UI, name: 'New Storage interface', diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx index 885724aca13c0..bb2b449c4d512 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx @@ -115,6 +115,11 @@ export const useIsNewStorageUIEnabled = () => { return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_NEW_STORAGE_UI] } +export const useIsSecurityNotificationsEnabled = () => { + const { flags } = useFeaturePreviewContext() + return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_SECURITY_NOTIFICATIONS] +} + export const useFeaturePreviewModal = () => { const [featurePreviewModal, setFeaturePreviewModal] = useQueryState('featurePreviewModal') @@ -122,6 +127,7 @@ export const useFeaturePreviewModal = () => { const advisorRulesEnabled = useFlag('advisorRules') const isUnifiedLogsPreviewAvailable = useFlag('unifiedLogs') const isNewStorageUIAvailable = useFlag('storageAnalyticsVector') + const isSecurityNotificationsAvailable = useFlag('securityNotifications') const selectedFeatureKeyFromQuery = featurePreviewModal?.trim() ?? null const showFeaturePreviewModal = selectedFeatureKeyFromQuery !== null @@ -138,11 +144,19 @@ export const useFeaturePreviewModal = () => { return isUnifiedLogsPreviewAvailable case 'new-storage-ui': return isNewStorageUIAvailable + case 'security-notifications': + return isSecurityNotificationsAvailable default: return true } }, - [gitlessBranchingEnabled, advisorRulesEnabled, isUnifiedLogsPreviewAvailable] + [ + gitlessBranchingEnabled, + advisorRulesEnabled, + isUnifiedLogsPreviewAvailable, + isNewStorageUIAvailable, + isSecurityNotificationsAvailable, + ] ) const selectedFeatureKey = ( diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx index ec463be8a6477..3fa262e4b123e 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx @@ -16,6 +16,7 @@ import { useFeaturePreviewContext, useFeaturePreviewModal } from './FeaturePrevi import { InlineEditorPreview } from './InlineEditorPreview' import { NewStorageUIPreview } from './NewStorageUIPreview' import { UnifiedLogsPreview } from './UnifiedLogsPreview' +import { SecurityNotificationsPreview } from './SecurityNotificationsPreview' const FEATURE_PREVIEW_KEY_TO_CONTENT: { [key: string]: ReactNode @@ -27,6 +28,7 @@ const FEATURE_PREVIEW_KEY_TO_CONTENT: { [LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]: , [LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS]: , [LOCAL_STORAGE_KEYS.UI_PREVIEW_NEW_STORAGE_UI]: , + [LOCAL_STORAGE_KEYS.UI_PREVIEW_SECURITY_NOTIFICATIONS]: , } const FeaturePreviewModal = () => { diff --git a/apps/studio/components/interfaces/App/FeaturePreview/SecurityNotificationsPreview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/SecurityNotificationsPreview.tsx new file mode 100644 index 0000000000000..8857881dfa4fb --- /dev/null +++ b/apps/studio/components/interfaces/App/FeaturePreview/SecurityNotificationsPreview.tsx @@ -0,0 +1,46 @@ +import Image from 'next/image' + +import { useParams } from 'common' +import { InlineLink } from 'components/ui/InlineLink' +import { BASE_PATH } from 'lib/constants' +import { useIsSecurityNotificationsEnabled } from './FeaturePreviewContext' + +export const SecurityNotificationsPreview = () => { + const { ref } = useParams() + const isSecurityNotificationsEnabled = useIsSecurityNotificationsEnabled() + + return ( +
+ Security notifications preview +
+

+ Try out our expanded set of{' '} + + email templates + {' '} + with support for security-related notifications. +

+

Enabling this preview will:

+
    +
  • Add a dedicated sidebar section for contact methods like email and SMS
  • +
  • Add new email templates for security-related notifications
  • +
  • Move each (existing and new) template into its own dynamic route
  • +
+

+ These changes are necessary to support incoming security-related notification templates. + Given that the list of our email templates is doubling in size, this change requires some + wider interface changes. Ones that we think make for a clearer experience overall. Win + win! +

+
+
+ ) +} diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx index 52f5444c228a4..58b820518666c 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx @@ -705,6 +705,11 @@ export const SQLEditor = () => { padding: { top: 4 }, lineNumbersMinChars: 3, }} + // [Joshen] These ones are meant to solve a UI issue that seems to only be happening locally + // Happens when you use the inline assistant in the SQL Editor and accept the suggestion + // Error: TextModel got disposed before DiffEditorWidget model got reset + keepCurrentModifiedModel={true} + keepCurrentOriginalModel={true} /> {showWidget && ( { - const where = genWhereStatement(table, filters) + let where = genWhereStatement(table, filters) + // pg_cron logs are a subset of postgres logs + // to calculate the chart, we need to query postgres logs + if (table === LogsTableName.PG_CRON) { + table = LogsTableName.POSTGRES + where = basePgCronWhere + } const joins = genCrossJoinUnnests(table) return `SELECT count(*) as count FROM ${table} ${joins} ${where}` } @@ -320,7 +325,10 @@ const calcChartStart = (params: Partial): [Dayjs, string] => return [its.add(-extendValue, trunc), trunc] } -const basePgCronWhere = `where ( parsed.application_name = 'pg_cron' or regexp_contains(event_message, 'cron job') )` +// TODO(qiao): workaround for self-hosted cron logs error until logflare is fixed +const basePgCronWhere = IS_PLATFORM + ? `where ( parsed.application_name = 'pg_cron' or regexp_contains(event_message, 'cron job') )` + : `where ( parsed.application_name = 'pg_cron' or event_message::text LIKE '%cron job%' )` /** * * generates log event chart query diff --git a/apps/studio/components/interfaces/Support/CategoryAndSeverityInfo.tsx b/apps/studio/components/interfaces/Support/CategoryAndSeverityInfo.tsx index 7d612c2d650fb..ae10f3997b09f 100644 --- a/apps/studio/components/interfaces/Support/CategoryAndSeverityInfo.tsx +++ b/apps/studio/components/interfaces/Support/CategoryAndSeverityInfo.tsx @@ -83,7 +83,7 @@ function CategorySelector({ form }: CategorySelectorProps) { - {CATEGORY_OPTIONS.map((option) => ( + {CATEGORY_OPTIONS.filter((option) => !option.hidden).map((option) => ( {option.label} diff --git a/apps/studio/components/interfaces/Support/Support.constants.ts b/apps/studio/components/interfaces/Support/Support.constants.ts index 777d187c6507b..f7ddf76f92005 100644 --- a/apps/studio/components/interfaces/Support/Support.constants.ts +++ b/apps/studio/components/interfaces/Support/Support.constants.ts @@ -3,13 +3,14 @@ import { isFeatureEnabled } from 'common' const billingEnabled = isFeatureEnabled('billing:all') -export type ExtendedSupportCategories = SupportCategories | 'Plan_upgrade' +export type ExtendedSupportCategories = SupportCategories | 'Plan_upgrade' | 'Others' export const CATEGORY_OPTIONS: { value: ExtendedSupportCategories label: string description: string query?: string + hidden?: boolean }[] = [ { value: SupportCategories.PROBLEM, @@ -77,6 +78,13 @@ export const CATEGORY_OPTIONS: { query: undefined, }, ]), + { + value: 'Others' as const, + label: 'Others', + description: 'Issues that are not related to any of the other categories', + query: undefined, + hidden: true, + }, ] export const SEVERITY_OPTIONS = [ diff --git a/apps/studio/components/interfaces/Support/SupportFormV2.tsx b/apps/studio/components/interfaces/Support/SupportFormV2.tsx index e431ddd52115c..04b62436b1409 100644 --- a/apps/studio/components/interfaces/Support/SupportFormV2.tsx +++ b/apps/studio/components/interfaces/Support/SupportFormV2.tsx @@ -1,8 +1,9 @@ -import { type Dispatch, type MouseEventHandler } from 'react' +import { useEffect, type Dispatch, type MouseEventHandler } from 'react' import type { SubmitHandler, UseFormReturn } from 'react-hook-form' // End of third-party imports import { SupportCategories } from '@supabase/shared-types/out/constants' +import { useFlag } from 'common' import { CLIENT_LIBRARIES } from 'common/constants' import { getProjectAuthConfig } from 'data/auth/auth-config-query' import { useSendSupportTicketMutation } from 'data/feedback/support-ticket-send' @@ -33,6 +34,15 @@ import { NO_PROJECT_MARKER, } from './SupportForm.utils' +const useIsSimplifiedForm = (slug: string) => { + const simplifiedSupportForm = useFlag('simplifiedSupportForm') + if (typeof simplifiedSupportForm === 'string') { + const slugs = (simplifiedSupportForm as string).split(',').map((x) => x.trim()) + return slugs.includes(slug) + } + return false +} + interface SupportFormV2Props { form: UseFormReturn initialError: string | null @@ -45,6 +55,7 @@ export const SupportFormV2 = ({ form, initialError, state, dispatch }: SupportFo const respondToEmail = profile?.primary_email ?? 'your email' const { organizationSlug, projectRef, category, severity, subject, library } = form.watch() + const simplifiedSupportForm = useIsSimplifiedForm(organizationSlug) const selectedOrgSlug = organizationSlug === NO_ORG_MARKER ? null : organizationSlug const selectedProjectRef = projectRef === NO_PROJECT_MARKER ? null : projectRef @@ -85,6 +96,7 @@ export const SupportFormV2 = ({ form, initialError, state, dispatch }: SupportFo const payload = { ...values, + category, organizationSlug: values.organizationSlug ?? NO_ORG_MARKER, projectRef: values.projectRef ?? NO_PROJECT_MARKER, allowSupportAccess: SUPPORT_ACCESS_CATEGORIES.includes(values.category) @@ -134,6 +146,15 @@ export const SupportFormV2 = ({ form, initialError, state, dispatch }: SupportFo handleFormSubmit(event) } + useEffect(() => { + if (simplifiedSupportForm) { + form.setValue('category', 'Others') + } else { + form.setValue('category', '' as any) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [simplifiedSupportForm]) + return (
@@ -148,20 +169,26 @@ export const SupportFormV2 = ({ form, initialError, state, dispatch }: SupportFo subscriptionPlanId={subscriptionPlanId} category={category} /> - + {!simplifiedSupportForm && ( + + )}
- - + {!simplifiedSupportForm && ( + <> + + + + )}
diff --git a/apps/studio/components/layouts/AuthLayout/AuthLayout.tsx b/apps/studio/components/layouts/AuthLayout/AuthLayout.tsx index 70c4cfe7b3dac..5e869db10b5ba 100644 --- a/apps/studio/components/layouts/AuthLayout/AuthLayout.tsx +++ b/apps/studio/components/layouts/AuthLayout/AuthLayout.tsx @@ -1,20 +1,21 @@ import { useRouter } from 'next/router' import { PropsWithChildren } from 'react' -import { useParams } from 'common' +import { useFlag, useParams } from 'common' +import { useIsSecurityNotificationsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { ProductMenu } from 'components/ui/ProductMenu' import { useAuthConfigPrefetch } from 'data/auth/auth-config-query' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { withAuth } from 'hooks/misc/withAuth' import { ProjectLayout } from '../ProjectLayout/ProjectLayout' import { generateAuthMenu } from './AuthLayout.utils' -import { useFlag } from 'common' const AuthProductMenu = () => { const router = useRouter() const { ref: projectRef = 'default' } = useParams() const authenticationShowOverview = useFlag('authOverviewPage') + const authenticationShowSecurityNotifications = useIsSecurityNotificationsEnabled() const { authenticationSignInProviders, @@ -46,6 +47,7 @@ const AuthProductMenu = () => { authenticationAttackProtection, authenticationAdvanced, authenticationShowOverview, + authenticationShowSecurityNotifications, })} /> ) diff --git a/apps/studio/components/layouts/AuthLayout/AuthLayout.utils.ts b/apps/studio/components/layouts/AuthLayout/AuthLayout.utils.ts index 67bad8717a388..27f0a52e396c5 100644 --- a/apps/studio/components/layouts/AuthLayout/AuthLayout.utils.ts +++ b/apps/studio/components/layouts/AuthLayout/AuthLayout.utils.ts @@ -11,6 +11,7 @@ export const generateAuthMenu = ( authenticationAttackProtection: boolean authenticationAdvanced: boolean authenticationShowOverview: boolean + authenticationShowSecurityNotifications: boolean } ): ProductMenuGroup[] => { const { @@ -21,6 +22,7 @@ export const generateAuthMenu = ( authenticationAttackProtection, authenticationAdvanced, authenticationShowOverview, + authenticationShowSecurityNotifications, } = flags ?? {} return [ @@ -33,6 +35,26 @@ export const generateAuthMenu = ( { name: 'Users', key: 'users', url: `/project/${ref}/auth/users`, items: [] }, ], }, + ...(authenticationEmails && authenticationShowSecurityNotifications && IS_PLATFORM + ? [ + { + title: 'Notifications', + items: [ + ...(authenticationEmails + ? [ + { + name: 'Email', + key: 'email', + pages: ['templates', 'smtp'], + url: `/project/${ref}/auth/templates`, + items: [], + }, + ] + : []), + ], + }, + ] + : []), { title: 'Configuration', items: [ @@ -71,7 +93,7 @@ export const generateAuthMenu = ( }, ] : []), - ...(authenticationEmails + ...(authenticationEmails && !authenticationShowSecurityNotifications ? [ { name: 'Emails', diff --git a/apps/studio/lib/gotrue.ts b/apps/studio/lib/gotrue.ts index b51fdd29e7111..ab350710682dd 100644 --- a/apps/studio/lib/gotrue.ts +++ b/apps/studio/lib/gotrue.ts @@ -1,6 +1,11 @@ +import * as Sentry from '@sentry/nextjs' import type { JwtPayload } from '@supabase/supabase-js' import { getAccessToken, type User } from 'common/auth' -import { gotrueClient } from 'common/gotrue' +import { gotrueClient, setCaptureException } from 'common/gotrue' + +setCaptureException((e: any) => { + Sentry.captureException(e) +}) export const auth = gotrueClient export { getAccessToken } diff --git a/apps/studio/public/img/previews/security-notifications-preview.png b/apps/studio/public/img/previews/security-notifications-preview.png new file mode 100644 index 0000000000000..262ed16f4d957 Binary files /dev/null and b/apps/studio/public/img/previews/security-notifications-preview.png differ diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 0f470f2fa30e0..536ea8860d5bc 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -11,7 +11,7 @@ services: studio: container_name: supabase-studio - image: supabase/studio:2025.10.01-sha-8460121 + image: supabase/studio:2025.10.20-sha-5005fc6 restart: unless-stopped healthcheck: test: @@ -190,7 +190,7 @@ services: realtime: # This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain container_name: realtime-dev.supabase-realtime - image: supabase/realtime:v2.51.11 + image: supabase/realtime:v2.56.0 restart: unless-stopped depends_on: db: @@ -235,7 +235,7 @@ services: # To use S3 backed storage: docker compose -f docker-compose.yml -f docker-compose.s3.yml up storage: container_name: supabase-storage - image: supabase/storage-api:v1.28.0 + image: supabase/storage-api:v1.28.1 restart: unless-stopped volumes: - ./volumes/storage:/var/lib/storage:z @@ -300,7 +300,7 @@ services: meta: container_name: supabase-meta - image: supabase/postgres-meta:v0.91.6 + image: supabase/postgres-meta:v0.93.0 restart: unless-stopped depends_on: db: @@ -319,7 +319,7 @@ services: functions: container_name: supabase-edge-functions - image: supabase/edge-runtime:v1.69.6 + image: supabase/edge-runtime:v1.69.14 restart: unless-stopped volumes: - ./volumes/functions:/home/deno/functions:Z @@ -480,7 +480,7 @@ services: # Update the DATABASE_URL if you are using an external Postgres database supavisor: container_name: supabase-pooler - image: supabase/supavisor:2.7.0 + image: supabase/supavisor:2.7.3 restart: unless-stopped ports: - ${POSTGRES_PORT}:5432 diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts index c77786c766194..3784822e32b76 100644 --- a/packages/common/constants/local-storage.ts +++ b/packages/common/constants/local-storage.ts @@ -18,6 +18,7 @@ export const LOCAL_STORAGE_KEYS = { UI_PREVIEW_BRANCHING_2_0: 'supabase-ui-branching-2-0', UI_PREVIEW_ADVISOR_RULES: 'supabase-ui-advisor-rules', UI_PREVIEW_NEW_STORAGE_UI: 'new-storage-ui', + UI_PREVIEW_SECURITY_NOTIFICATIONS: 'security-notifications', NEW_LAYOUT_NOTICE_ACKNOWLEDGED: 'new-layout-notice-acknowledge', TABS_INTERFACE_ACKNOWLEDGED: 'tabs-interface-acknowledge', diff --git a/packages/common/gotrue.ts b/packages/common/gotrue.ts index 0456a464e56a2..6ab6fb2ec72b7 100644 --- a/packages/common/gotrue.ts +++ b/packages/common/gotrue.ts @@ -115,12 +115,72 @@ const logIndexedDB = (message: string, ...args: any[]) => { })() } +let captureException: ((e: any) => any) | null = null + +export function setCaptureException(fn: typeof captureException) { + captureException = fn +} + +async function debuggableNavigatorLock( + name: string, + acquireTimeout: number, + fn: () => Promise +): Promise { + let stackException: any + + try { + throw new Error('Lock is being held for over 2s here') + } catch (e: any) { + stackException = e + } + + const debugTimeout = setTimeout(() => { + ;(async () => { + const bc = new BroadcastChannel('who-is-holding-the-lock') + try { + bc.postMessage({}) + } finally { + bc.close() + } + + console.error( + `Waited for over 2s to acquire an Auth client lock`, + await navigator.locks.query(), + stackException + ) + })() + }, 2000) + + try { + return await navigatorLock(name, acquireTimeout, async () => { + clearTimeout(debugTimeout) + + const bc = new BroadcastChannel('who-is-holding-the-lock') + bc.addEventListener('message', () => { + console.error('Lock is held here', stackException) + + if (captureException) { + captureException(stackException) + } + }) + + try { + return await fn() + } finally { + bc.close() + } + }) + } finally { + clearTimeout(debugTimeout) + } +} + export const gotrueClient = new AuthClient({ url: process.env.NEXT_PUBLIC_GOTRUE_URL, storageKey: STORAGE_KEY, detectSessionInUrl: shouldDetectSessionInUrl, debug: debug ? (persistedDebug ? logIndexedDB : true) : false, - lock: navigatorLockEnabled ? navigatorLock : undefined, + lock: navigatorLockEnabled ? debuggableNavigatorLock : undefined, ...('localStorage' in globalThis ? { storage: globalThis.localStorage, userStorage: globalThis.localStorage }