diff --git a/apps/docs/content/guides/platform/migrating-within-supabase/dashboard-restore.mdx b/apps/docs/content/guides/platform/migrating-within-supabase/dashboard-restore.mdx index 476c4a9aa87ce..3e59cdeb9a75c 100644 --- a/apps/docs/content/guides/platform/migrating-within-supabase/dashboard-restore.mdx +++ b/apps/docs/content/guides/platform/migrating-within-supabase/dashboard-restore.mdx @@ -6,6 +6,12 @@ breadcrumb: 'Migrations' ## Before you begin + + +Dashboard backups are only available for older projects that still use logical backups. Projects that use physical backups should follow steps in [Backup and Restore using the CLI](/docs/guides/platform/migrating-within-supabase/backup-restore). + + + void + onCancel?: () => void + visible: boolean +} + +type CancellationFlowState = + | 'closed' + | 'show-downgrade-modal' + | 'show-downgrade-error' + | 'show-exit-survey' + +export const CancellationFlow = (props: CancellationFlowProps) => { + const { slug } = useParams() + + const [flowState, setFlowState] = useState( + props.visible ? 'show-downgrade-modal' : 'closed' + ) + useEffect(() => { + setFlowState(props.visible ? 'show-downgrade-modal' : 'closed') + }, [props.visible]) + + const { data: membersExceededLimit, isLoading: projectLimitQueryLoading } = + useFreeProjectLimitCheckQuery({ slug }, { enabled: flowState !== 'closed' }) + + const { data: orgProjectsData, isLoading: orgProjectsQueryLoading } = useOrgProjectsInfiniteQuery( + { slug }, + { enabled: flowState !== 'closed' } + ) + + const { data: subscription, isLoading: orgSubscriptionQueryLoading } = useOrgSubscriptionQuery({ + orgSlug: slug, + }) + + const isLoading = + projectLimitQueryLoading || orgProjectsQueryLoading || orgSubscriptionQueryLoading + + const orgProjects = + useMemo( + () => orgProjectsData?.pages.flatMap((page) => page.projects), + [orgProjectsData?.pages] + ) || [] + + // [Joshen] Note that orgProjects is paginated so there's a chance this may omit certain projects + // Although I don't foresee this affecting a majority of users. Ideally perhaps we could return + // this data from the organization query + const hasRunningProjects = + orgProjects.filter((it) => it.status !== 'INACTIVE' && it.status !== 'GOING_DOWN').length > 0 + + const hasMembersExceedingFreeTierLimit = + (membersExceededLimit || []).length > 0 && hasRunningProjects + + const onConfirmDowngrade = () => { + if (hasMembersExceedingFreeTierLimit) { + setFlowState('show-downgrade-error') + } else { + setFlowState('show-exit-survey') + } + } + + const onCancelFlow = () => { + setFlowState('closed') + props.onCancel?.() + } + + return ( + <> + + + + + { + if (success) { + setFlowState('closed') + props.onDowngrade?.() + } else { + onCancelFlow() + } + }} + /> + + ) +} + +type InitiateCancellationFlowButtonProps = Pick< + ComponentProps, + 'children' | 'type' +> + +export const InitiateCancellationFlowButton = (props: InitiateCancellationFlowButtonProps) => { + const { slug } = useParams() + const track = useTrack() + + const { data: selectedOrganization } = useSelectedOrganizationQuery() + const isAwsManaged = selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE + + const { can: canUpdateSubscription } = useAsyncCheckPermissions( + PermissionAction.BILLING_WRITE, + 'stripe.subscriptions' + ) + + const { data: subscription } = useOrgSubscriptionQuery({ + orgSlug: slug, + }) + + const isDowngradeablePlan = !subscription || ['pro', 'team'].includes(subscription.plan.id) + + const [visible, setVisible] = useState(false) + + const tooltipText = [ + [isAwsManaged, 'You cannot change the plan for an organization managed by AWS Marketplace.'], + [!isDowngradeablePlan, 'Reach out to us via support to update your plan.'], + [!canUpdateSubscription, "You need additional permissions to change your organization's plan."], + ].find(([cond]) => cond)?.[1] + + return ( + <> + { + track('studio_billing_cancel_subscription_clicked', { + currentPlan: subscription?.plan.id || 'free', + }) + setVisible(true) + }} + > + {props.children ?? 'Cancel Subscription'} + + + setVisible(false)} + onDowngrade={() => setVisible(false)} + visible={visible} + /> + + ) +} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/DowngradeModal.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/DowngradeModal.tsx index 149f7c27486d4..194495f8bf5cd 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/DowngradeModal.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/DowngradeModal.tsx @@ -23,6 +23,7 @@ export interface DowngradeModalProps { onClose: () => void onConfirm: () => void projects: OrgProject[] + confirmDisabled?: boolean } const ProjectDowngradeListItem = ({ projectAddon }: { projectAddon: ProjectAddon }) => { @@ -64,6 +65,7 @@ export const DowngradeModal = ({ onClose, onConfirm, projects, + confirmDisabled, }: DowngradeModalProps) => { const selectedPlan = useMemo(() => subscriptionsPlans.find((tier) => tier.id === 'tier_free'), []) @@ -183,7 +185,14 @@ export const DowngradeModal = ({ Cancel - + { + e.preventDefault() + onConfirm() + }} + > Confirm diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx index 18a449016810b..523119d7df701 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx @@ -201,7 +201,7 @@ export const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalP disabled={subscriptionUpdateDisabled || isSubmitting} onClick={onSubmit} > - Confirm downgrade + Downgrade Now diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx index 59a65fc5b234b..22645551a3f6c 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx @@ -10,10 +10,8 @@ import { plans as subscriptionsPlans } from 'shared-data/plans' import { Button, cn, SidePanel } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' -import { DowngradeModal } from './DowngradeModal' +import { CancellationFlow } from './CancellationFlow' import { EnterpriseCard } from './EnterpriseCard' -import { ExitSurveyModal } from './ExitSurveyModal' -import MembersExceedLimitModal from './MembersExceedLimitModal' import { SubscriptionPlanUpdateDialog } from './SubscriptionPlanUpdateDialog' import UpgradeSurveyModal from './UpgradeModal' import { STRIPE_PROJECTS_DOCS_URL } from '@/components/interfaces/Billing/Payment/PaymentMethods/StripePaymentConnection' @@ -21,7 +19,6 @@ import { getPlanChangeType } from '@/components/interfaces/Billing/Subscription/ import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import PartnerManagedResource from '@/components/ui/PartnerManagedResource' import { RequestUpgradeToBillingOwners } from '@/components/ui/RequestUpgradeToBillingOwners' -import { useFreeProjectLimitCheckQuery } from '@/data/organizations/free-project-limit-check-query' import { isPartnerBillingOrganization } from '@/data/organizations/managed-by-utils' import { useOrganizationBillingSubscriptionPreview } from '@/data/organizations/organization-billing-subscription-preview' import { useOrganizationQuery } from '@/data/organizations/organization-query' @@ -72,9 +69,7 @@ export const PlanUpdateSidePanel = () => { const originalPlanRef = useRef(undefined) - const [showExitSurvey, setShowExitSurvey] = useState(false) const [showUpgradeSurvey, setShowUpgradeSurvey] = useState(false) - const [showDowngradeError, setShowDowngradeError] = useState(false) const [selectedTier, setSelectedTier] = useState<'tier_free' | 'tier_pro' | 'tier_team'>() const [latestAddress, setLatestAddress] = useState() const [latestTaxId, setLatestTaxId] = useState() @@ -130,10 +125,6 @@ export const PlanUpdateSidePanel = () => { { orgSlug: slug }, { enabled: visible } ) - const { data: membersExceededLimit } = useFreeProjectLimitCheckQuery( - { slug }, - { enabled: visible } - ) const subscriptionPreviewData = useOrganizationBillingSubscriptionPreview({ tier: selectedTier, @@ -143,12 +134,6 @@ export const PlanUpdateSidePanel = () => { }) const availablePlans: OrgPlan[] = plans?.plans ?? [] - const hasMembersExceedingFreeTierLimit = - (membersExceededLimit || []).length > 0 && - // [Joshen] Note that orgProjects is paginated so there's a chance this may omit certain projects - // Although I don't foresee this affecting a majority of users. Ideally perhaps we could return - // this data from the organization query - orgProjects.filter((it) => it.status !== 'INACTIVE' && it.status !== 'GOING_DOWN').length > 0 const onPanelOpened = useEffectEvent( (properties: StudioPricingSidePanelOpenedEvent['properties']) => { @@ -182,15 +167,6 @@ export const PlanUpdateSidePanel = () => { } }, [visible, isSuccessSubscription, subscription?.plan.id]) - const onConfirmDowngrade = () => { - setSelectedTier(undefined) - if (hasMembersExceedingFreeTierLimit) { - setShowDowngradeError(true) - } else { - setShowExitSurvey(true) - } - } - const planMeta = selectedTier ? availablePlans.find((p) => p.id === selectedTier.split('tier_')[1]) : null @@ -395,12 +371,10 @@ export const PlanUpdateSidePanel = () => { - setSelectedTier(undefined)} - onConfirm={onConfirmDowngrade} - projects={orgProjects} + onCancel={() => setSelectedTier(undefined)} + onDowngrade={() => setSelectedTier(undefined)} /> { onUseAsDefaultBillingAddressChange={handleUseAsDefaultBillingAddressChange} /> - setShowDowngradeError(false)} - /> - - { - setShowExitSurvey(false) - if (success) onClose() - }} - /> - { {isError && } {isSuccess && ( -
-
-

{currentPlan?.name ?? 'Unknown'} Plan

-
+
+
+

+ {currentPlan?.name ?? 'Unknown'} Plan +

-
- {canChangeTier ? ( - + {canChangeTier && ( +
- - ) : projectUpdateDisabled ? ( - - ) : ( - - - Contact support - - - } - /> + {currentPlan && currentPlan.id !== 'free' && ( + + Cancel Subscription + + )} +
)}
+ {!canChangeTier && ( +
+ {projectUpdateDisabled ? ( + + ) : ( + + + Contact support + + + } + /> + )} +
+ )} + {!subscription?.usage_billing_enabled && ( {

You can review a static PDF version of our latest DPA document{' '} track('dpa_pdf_opened', { source: 'studio' })} > here diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/ProjectSettings.Commands.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/ProjectSettings.Commands.tsx index 7a638bfbe79ba..5635eb1acc61d 100644 --- a/apps/studio/components/layouts/ProjectSettingsLayout/ProjectSettings.Commands.tsx +++ b/apps/studio/components/layouts/ProjectSettingsLayout/ProjectSettings.Commands.tsx @@ -16,12 +16,20 @@ export function useProjectSettingsGotoCommands(options?: CommandOptions) { ref ||= '_' const hasOrgSlug = typeof slug === 'string' && slug.length > 0 && slug !== '_' - const { projectSettingsLogDrains, projectSettingsCustomDomains, authenticationSignInProviders } = - useIsFeatureEnabled([ - 'project_settings:log_drains', - 'project_settings:custom_domains', - 'authentication:sign_in_providers', - ]) + const { + logsAll, + projectSettingsLogDrains, + projectSettingsCustomDomains, + authenticationSignInProviders, + } = useIsFeatureEnabled([ + 'logs:all', + 'project_settings:log_drains', + 'project_settings:custom_domains', + 'authentication:sign_in_providers', + ]) + + // Log drains depend on the analytics backend, gated by logs:all (see SettingsMenu.utils). + const showLogDrains = logsAll && projectSettingsLogDrains useRegisterCommands( COMMAND_MENU_SECTIONS.NAVIGATE, @@ -166,7 +174,7 @@ export function useProjectSettingsGotoCommands(options?: CommandOptions) { route: `/project/${ref}/database/settings#banned-ips`, defaultHidden: true, }, - ...(projectSettingsLogDrains + ...(showLogDrains ? [ { id: 'nav-project-settings-log-drains', @@ -177,6 +185,6 @@ export function useProjectSettingsGotoCommands(options?: CommandOptions) { ] : []), ], - { ...options, deps: [platformWebhooksEnabled, ref, slug] } + { ...options, deps: [platformWebhooksEnabled, showLogDrains, ref, slug] } ) } diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.selfhosted.test.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.selfhosted.test.tsx index f044991a6b4b8..1b5d40e403775 100644 --- a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.selfhosted.test.tsx +++ b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.selfhosted.test.tsx @@ -1,7 +1,8 @@ import { renderHook } from '@testing-library/react' -import { describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { useGenerateSettingsMenu } from './SettingsMenu.utils' +import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import { SHORTCUT_IDS } from '@/state/shortcuts/registry' const getShortcutId = (item: unknown) => (item as { shortcutId?: string } | undefined)?.shortcutId @@ -28,9 +29,7 @@ vi.mock('@/hooks/misc/useSelectedProject', () => ({ })) vi.mock('@/hooks/misc/useIsFeatureEnabled', () => ({ - useIsFeatureEnabled: vi - .fn() - .mockReturnValue({ projectSettingsLegacyJwtKeys: false, billingAll: true }), + useIsFeatureEnabled: vi.fn(), })) vi.mock('@/components/interfaces/App/FeaturePreview/FeaturePreviewContext', () => ({ @@ -38,6 +37,15 @@ vi.mock('@/components/interfaces/App/FeaturePreview/FeaturePreviewContext', () = })) describe('useGenerateSettingsMenu (self-hosted)', () => { + beforeEach(() => { + vi.mocked(useIsFeatureEnabled).mockReturnValue({ + projectSettingsLegacyJwtKeys: false, + billingAll: true, + logsAll: true, + projectSettingsLogDrains: true, + } as any) + }) + it('includes General, API Keys, JWT Keys, and Log Drains in self-hosted mode', () => { const { result } = renderHook(() => useGenerateSettingsMenu()) const configGroup = result.current.find((group) => group.title === 'Configuration') @@ -51,6 +59,34 @@ describe('useGenerateSettingsMenu (self-hosted)', () => { ) }) + it('hides Log Drains in self-hosted mode when logs:all is disabled', () => { + vi.mocked(useIsFeatureEnabled).mockReturnValue({ + projectSettingsLegacyJwtKeys: false, + billingAll: true, + logsAll: false, + projectSettingsLogDrains: true, + } as any) + + const { result } = renderHook(() => useGenerateSettingsMenu()) + const configGroup = result.current.find((group) => group.title === 'Configuration') + + expect(configGroup?.items.some((item) => item.key === 'log-drains')).toBe(false) + }) + + it('hides Log Drains in self-hosted mode when project_settings:log_drains is disabled', () => { + vi.mocked(useIsFeatureEnabled).mockReturnValue({ + projectSettingsLegacyJwtKeys: false, + billingAll: true, + logsAll: true, + projectSettingsLogDrains: false, + } as any) + + const { result } = renderHook(() => useGenerateSettingsMenu()) + const configGroup = result.current.find((group) => group.title === 'Configuration') + + expect(configGroup?.items.some((item) => item.key === 'log-drains')).toBe(false) + }) + it('includes Data API and Vault integrations in self-hosted mode', () => { const { result } = renderHook(() => useGenerateSettingsMenu()) const integrationsGroup = result.current.find((group) => group.title === 'Integrations') diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.test.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.test.tsx index 077fd882a4a0c..1c6a6b17f167a 100644 --- a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.test.tsx +++ b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.test.tsx @@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { useGenerateSettingsMenu } from './SettingsMenu.utils' import { useIsPlatformWebhooksEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' +import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import { SHORTCUT_IDS } from '@/state/shortcuts/registry' const getShortcutId = (item: unknown) => (item as { shortcutId?: string } | undefined)?.shortcutId @@ -30,9 +31,7 @@ vi.mock('@/hooks/misc/useSelectedProject', () => ({ })) vi.mock('@/hooks/misc/useIsFeatureEnabled', () => ({ - useIsFeatureEnabled: vi - .fn() - .mockReturnValue({ projectSettingsLegacyJwtKeys: false, billingAll: true }), + useIsFeatureEnabled: vi.fn(), })) vi.mock('@/components/interfaces/App/FeaturePreview/FeaturePreviewContext', () => ({ @@ -43,6 +42,12 @@ describe('useGenerateSettingsMenu', () => { beforeEach(() => { vi.mocked(useFlag).mockReturnValue(false) vi.mocked(useIsPlatformWebhooksEnabled).mockReturnValue(true) + vi.mocked(useIsFeatureEnabled).mockReturnValue({ + projectSettingsLegacyJwtKeys: false, + billingAll: true, + logsAll: true, + projectSettingsLogDrains: true, + } as any) }) it('includes webhooks when platformWebhooks feature is enabled', () => { @@ -97,6 +102,41 @@ describe('useGenerateSettingsMenu', () => { expect(configurationGroup?.items.some((item) => item.name === 'Dashboard')).toBe(false) }) + it('includes log drains when logs:all and project_settings:log_drains are enabled', () => { + const { result } = renderHook(() => useGenerateSettingsMenu()) + const configurationGroup = result.current.find((group) => group.title === 'Configuration') + + expect(configurationGroup?.items.some((item) => item.key === 'log-drains')).toBe(true) + }) + + it('hides log drains when logs:all is disabled', () => { + vi.mocked(useIsFeatureEnabled).mockReturnValue({ + projectSettingsLegacyJwtKeys: false, + billingAll: true, + logsAll: false, + projectSettingsLogDrains: true, + } as any) + + const { result } = renderHook(() => useGenerateSettingsMenu()) + const configurationGroup = result.current.find((group) => group.title === 'Configuration') + + expect(configurationGroup?.items.some((item) => item.key === 'log-drains')).toBe(false) + }) + + it('hides log drains when project_settings:log_drains is disabled', () => { + vi.mocked(useIsFeatureEnabled).mockReturnValue({ + projectSettingsLegacyJwtKeys: false, + billingAll: true, + logsAll: true, + projectSettingsLogDrains: false, + } as any) + + const { result } = renderHook(() => useGenerateSettingsMenu()) + const configurationGroup = result.current.find((group) => group.title === 'Configuration') + + expect(configurationGroup?.items.some((item) => item.key === 'log-drains')).toBe(false) + }) + it('adds shortcuts to eligible configuration settings items', () => { vi.mocked(useFlag).mockReturnValue(true) diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx index 880b84e66e93b..ef22a78dcd79b 100644 --- a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx +++ b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx @@ -16,8 +16,21 @@ export const useGenerateSettingsMenu = () => { const platformWebhooksEnabled = useIsPlatformWebhooksEnabled() - const { projectSettingsLegacyJwtKeys: legacyJwtKeysEnabled, billingAll: billingEnabled } = - useIsFeatureEnabled(['project_settings:legacy_jwt_keys', 'billing:all']) + const { + projectSettingsLegacyJwtKeys: legacyJwtKeysEnabled, + billingAll: billingEnabled, + logsAll, + projectSettingsLogDrains, + } = useIsFeatureEnabled([ + 'project_settings:legacy_jwt_keys', + 'billing:all', + 'logs:all', + 'project_settings:log_drains', + ]) + + // Log drains rely on the analytics backend (gated by logs:all) and on the dedicated + // log_drains flag. Keep this in sync with ProjectSettings.Commands.tsx. + const showLogDrains = logsAll && projectSettingsLogDrains const isProjectActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY @@ -46,13 +59,17 @@ export const useGenerateSettingsMenu = () => { : `/project/${ref}/settings/jwt/signing-keys`, items: [], }, - { - name: `Log Drains`, - key: `log-drains`, - url: `/project/${ref}/settings/log-drains`, - items: [], - shortcutId: SHORTCUT_IDS.NAV_PROJECT_SETTINGS_LOG_DRAINS, - }, + ...(showLogDrains + ? [ + { + name: `Log Drains`, + key: `log-drains`, + url: `/project/${ref}/settings/log-drains`, + items: [], + shortcutId: SHORTCUT_IDS.NAV_PROJECT_SETTINGS_LOG_DRAINS, + }, + ] + : []), ], }, { @@ -146,14 +163,18 @@ export const useGenerateSettingsMenu = () => { shortcutId: SHORTCUT_IDS.NAV_PROJECT_SETTINGS_JWT_KEYS, }, - { - name: `Log Drains`, - key: `log-drains`, - url: `/project/${ref}/settings/log-drains`, - items: [], - disabled: !isProjectActive, - shortcutId: SHORTCUT_IDS.NAV_PROJECT_SETTINGS_LOG_DRAINS, - }, + ...(showLogDrains + ? [ + { + name: `Log Drains`, + key: `log-drains`, + url: `/project/${ref}/settings/log-drains`, + items: [], + disabled: !isProjectActive, + shortcutId: SHORTCUT_IDS.NAV_PROJECT_SETTINGS_LOG_DRAINS, + }, + ] + : []), { name: 'Add-ons', key: 'addons', diff --git a/apps/studio/pages/project/[ref]/logs/explorer/index.tsx b/apps/studio/pages/project/[ref]/logs/explorer/index.tsx index 04eae9839a402..99bd536b04e61 100644 --- a/apps/studio/pages/project/[ref]/logs/explorer/index.tsx +++ b/apps/studio/pages/project/[ref]/logs/explorer/index.tsx @@ -203,7 +203,11 @@ export const LogsExplorerPage: NextPageWithLayout = () => { const handleRun = (value?: string | React.MouseEvent) => { track('log_explorer_query_run_button_clicked', { is_saved_query: !!queryId }) - const query = typeof value === 'string' ? value || editorValue : editorValue + // Read the latest value straight from the editor instance rather than from + // `editorValue` state, which can lag behind the most recent keystroke. This + // keeps the Run button consistent with the Cmd+Enter keybinding. + const liveValue = editorRef.current?.getValue() + const query = typeof value === 'string' ? value || editorValue : (liveValue ?? editorValue) const resolvedParams = buildLogQueryParams(datePickerValue, query) setSelectedLog(null) diff --git a/apps/www/_events/2026-06-25-supabase-perplexity-small-businesses.mdx b/apps/www/_events/2026-06-25-supabase-perplexity-small-businesses.mdx new file mode 100644 index 0000000000000..ae0a02682c92a --- /dev/null +++ b/apps/www/_events/2026-06-25-supabase-perplexity-small-businesses.mdx @@ -0,0 +1,58 @@ +--- +title: 'Workflows That Scale: Supabase + Perplexity Computer for small businesses' +meta_title: 'Workflows That Scale: Supabase + Perplexity Computer for small businesses' +subtitle: >- + See what a lean, high-leverage business looks like when it runs on Supabase + and Perplexity Computer together. +meta_description: >- + Join Supabase and Perplexity to see how growing teams use Perplexity Computer + workflows on top of their Supabase data: outbound, market research, feedback, + and more. Registered attendees get complimentary Perplexity Enterprise Pro and + Computer credits. +type: webinar +onDemand: false +date: '2026-06-25T09:00:00.000-07:00' +timezone: America/Los_Angeles +duration: 45 mins +company: + name: Perplexity + website_url: 'https://www.perplexity.ai/' + logo: >- + /images/events/webinars/supabase-perplexity-small-businesses/perplexity-white.png + logo_light: >- + /images/events/webinars/supabase-perplexity-small-businesses/perplexity.png +categories: + - webinar +main_cta: + url: 'https://attendee.gotowebinar.com/register/2836535946334786656' + target: _blank + label: Register now +speakers: 'natalie_roberge,david_dalmaso' +--- + +Founders are juggling outbound, market research, customer meetings, product feedback, investor updates, and operations. The tools exist, but the manual work between them is what slows growing teams down. And when the work does get done, it disappears into a Slack thread, a doc nobody finds, a tab someone closed. + +Small businesses are solving both problems at once: using AI to do the work between the tools, and keeping everything in one place so the whole team can build on it. + +Supabase is where a lot of growing businesses already keep their most important data: customers, signups, feedback, revenue. The next step is putting the data to work. + +Perplexity Computer makes that possible: the work that used to take a small army of operators now happens in a single workflow, running directly on top of the data you already have in Supabase. + +In this session, Natalie Roberge, Customer Solutions Architect from Supabase and David Dalmaso, Member of Technical Staff, from Perplexity will walk through what a growing business actually looks like when it runs on Perplexity Computer and Supabase together. + +**Registered attendees will receive complimentary Perplexity Enterprise Pro and Computer credits.** + +### What we'll cover + +- Using Supabase as the backend for all AI / Computer workflows to help democratize data +- How anyone on your team can run Perplexity Computer workflows autonomously, with Supabase as the consistent data layer that ties everything together across tools, teams, and systems. + +**Live demos** + +- Pull your top accounts by growth, enrich them, and draft personalized outreach, with every record saved back into Supabase +- Turn a week of customer feedback into a roadmap summary and weekly update your team can act on +- Go from market signals to a campaign brief, stored in Supabase so your marketing team can find and build on it + +You'll leave with a clear picture of what it looks like to run a lean, high-leverage business on Supabase and Perplexity Computer and the first workflows to try with your own data. + +Plus, bring your questions for the live Q&A. Can't make it? We'll send you the recording. diff --git a/apps/www/lib/authors.json b/apps/www/lib/authors.json index f4b76624b61ab..5ba696e803f54 100644 --- a/apps/www/lib/authors.json +++ b/apps/www/lib/authors.json @@ -969,5 +969,20 @@ "position": "Engineering", "author_url": "https://github.com/johnstonmatt", "author_image_url": "https://github.com/johnstonmatt.png" + }, + { + "author_id": "natalie_roberge", + "author": "Natalie Roberge", + "position": "Customer Solutions Architect", + "author_url": "https://www.linkedin.com/in/natalieroberge/", + "author_image_url": "/images/blog/avatars/natalie-roberge.png" + }, + { + "author_id": "david_dalmaso", + "author": "David Dalmaso", + "position": "Member of Technical Staff", + "company": "Perplexity", + "author_url": "https://www.linkedin.com/in/david-dalmaso-359791101/", + "author_image_url": "/images/blog/avatars/david-dalmaso.png" } ] diff --git a/apps/www/pages/legal/dpa.tsx b/apps/www/pages/legal/dpa.tsx index 0898360591f85..1d2506851e020 100644 --- a/apps/www/pages/legal/dpa.tsx +++ b/apps/www/pages/legal/dpa.tsx @@ -1,7 +1,7 @@ -import CTABanner from 'components/CTABanner/index' import Layout from '~/components/Layouts/Default' import SectionContainer from '~/components/Layouts/SectionContainer' import { useSendTelemetryEvent } from '~/lib/telemetry' +import CTABanner from 'components/CTABanner/index' const DPA = () => { const sendTelemetryEvent = useSendTelemetryEvent() @@ -19,7 +19,7 @@ const DPA = () => { part of this commitment, we have prepared a Data Processing Addendum ("DPA"). You can review a static PDF version of our latest DPA document{' '} +} + /** * User clicked on the CTA button on a plan in the pricing side panel in studio. * @@ -3341,6 +3359,7 @@ export type TelemetryEvent = | StoragePublicBucketSelectPolicyRemovedEvent | StoragePublicBucketSelectPolicyWarningDismissButtonClickedEvent | StudioPricingPlanCtaClickedEvent + | StudioBillingCancelSubscriptionClickedEvent | StudioPricingSidePanelOpenedEvent | ReportsDatabaseGrafanaBannerClickedEvent | MetricsAPIBannerCtaButtonClickedEvent