diff --git a/apps/studio/components/interfaces/Integrations/Integration/BuildBySection.tsx b/apps/studio/components/interfaces/Integrations/Integration/BuildBySection.tsx index f2e84a6f64527..31cef5ce34a18 100644 --- a/apps/studio/components/interfaces/Integrations/Integration/BuildBySection.tsx +++ b/apps/studio/components/interfaces/Integrations/Integration/BuildBySection.tsx @@ -1,16 +1,17 @@ import { Book } from 'lucide-react' import Link from 'next/link' -import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react' +import { ComponentPropsWithoutRef, ElementRef, forwardRef, ReactNode } from 'react' import { cn } from 'ui' import { IntegrationDefinition } from '../Landing/Integrations.constants' interface BuiltBySectionProps extends ComponentPropsWithoutRef<'div'> { integration: IntegrationDefinition + status?: string | ReactNode } export const BuiltBySection = forwardRef, BuiltBySectionProps>( - ({ integration, className, ...props }, ref) => { + ({ integration, status, className, ...props }, ref) => { const { docsUrl } = integration const { name, websiteUrl } = integration?.author ?? {} @@ -22,6 +23,12 @@ export const BuiltBySection = forwardRef, BuiltBySectionProps> className={cn('flex flex-wrap items-center gap-8 md:gap-10 px-4 md:px-10', className)} {...props} > + {status && ( +
+
STATUS
+
{status}
+
+ )} {name && (
BUILT BY
diff --git a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx index 88a3e09f16066..78c379c1c82bd 100644 --- a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx @@ -1,11 +1,9 @@ -import { useRouter } from 'next/router' import { PropsWithChildren, ReactNode } from 'react' import { useParams } from 'common' -import { Markdown } from 'components/interfaces/Markdown' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { Alert_Shadcn_, AlertDescription_Shadcn_, Badge, Separator } from 'ui' +import { Badge, Card, CardContent, cn, Separator } from 'ui' import { INTEGRATIONS } from '../Landing/Integrations.constants' import { BuiltBySection } from './BuildBySection' import { MarkdownContent } from './MarkdownContent' @@ -13,14 +11,17 @@ import { MissingExtensionAlert } from './MissingExtensionAlert' interface IntegrationOverviewTabProps { actions?: ReactNode + status?: string | ReactNode + alert?: ReactNode } export const IntegrationOverviewTab = ({ actions, + alert, + status, children, }: PropsWithChildren) => { const { id } = useParams() - const router = useRouter() const { data: project } = useSelectedProjectQuery() const integration = INTEGRATIONS.find((i) => i.id === id) @@ -47,43 +48,69 @@ export const IntegrationOverviewTab = ({ return (
- + + {alert &&
{alert}
} + + {dependsOnExtension && (
- - - - Supabase - Postgres Module - - `\`${x}\``).join(', ')} - extension${integration.requiredExtensions.length > 1 ? 's' : ''} directly in your Postgres database. - ${hasToInstallExtensions && !hasMissingExtensions ? `Install ${integration.requiredExtensions.length > 1 ? 'these' : 'this'} database extension${integration.requiredExtensions.length > 1 ? 's' : ''} to use ${integration.name} in your project.` : ''} - `} - /> +

Required extensions

+ + +
    + {(integration.requiredExtensions ?? []).map((requiredExtension, idx) => { + const extension = (extensions ?? []).find((ext) => ext.name === requiredExtension) + const isInstalled = !!extension?.installed_version + const isLastRow = idx === (integration.requiredExtensions?.length ?? 0) - 1 + + return ( +
  • +
    + + {requiredExtension} + +
    + +
    + {extension ? ( + isInstalled ? ( + Installed + ) : ( + + ) + ) : ( + Unavailable + )} +
    +
  • + ) + })} +
- {hasMissingExtensions ? ( - integration.missingExtensionsAlert - ) : ( -
- {installableExtensions.map((extension) => ( - - ))} -
+ {hasMissingExtensions && ( +
{integration.missingExtensionsAlert}
)} -
-
+ + +
+ )} + {!!actions && ( +
+ {actions}
)} - {!!actions && !hasToInstallExtensions &&
{actions}
} - - {children}
) diff --git a/apps/studio/components/interfaces/Integrations/Integration/MarkdownContent.tsx b/apps/studio/components/interfaces/Integrations/Integration/MarkdownContent.tsx index 24b4bec4d400a..120d31fa4b7bb 100644 --- a/apps/studio/components/interfaces/Integrations/Integration/MarkdownContent.tsx +++ b/apps/studio/components/interfaces/Integrations/Integration/MarkdownContent.tsx @@ -6,9 +6,15 @@ import { cn } from 'ui' const CHAR_LIMIT = 500 // Adjust this number as needed -export const MarkdownContent = ({ integrationId }: { integrationId: string }) => { +export const MarkdownContent = ({ + integrationId, + initiallyExpanded, +}: { + integrationId: string + initiallyExpanded?: boolean +}) => { const [content, setContent] = useState('') - const [isExpanded, setIsExpanded] = useState(false) + const [isExpanded, setIsExpanded] = useState(initiallyExpanded ?? false) useEffect(() => { import(`static-data/integrations/${integrationId}/overview.md`) diff --git a/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx b/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx index 71e865542a728..8dff7c8b05973 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx @@ -1,4 +1,4 @@ -import { Clock5, Layers, Timer, Vault, Webhook } from 'lucide-react' +import { Clock5, Layers, Timer, Vault, Webhook, Receipt } from 'lucide-react' import dynamic from 'next/dynamic' import Image from 'next/image' import { ComponentType, ReactNode } from 'react' @@ -51,7 +51,7 @@ const authorSupabase = { websiteUrl: 'https://supabase.com', } -const supabaseIntegrations: IntegrationDefinition[] = [ +const SUPABASE_INTEGRATIONS: IntegrationDefinition[] = [ { id: 'queues', type: 'postgres_extension' as const, @@ -315,7 +315,7 @@ const supabaseIntegrations: IntegrationDefinition[] = [ }, ] as const -const wrapperIntegrations: IntegrationDefinition[] = WRAPPERS.map((w) => { +const WRAPPER_INTEGRATIONS: IntegrationDefinition[] = WRAPPERS.map((w) => { return { id: w.name, type: 'wrapper' as const, @@ -366,7 +366,66 @@ const wrapperIntegrations: IntegrationDefinition[] = WRAPPERS.map((w) => { } }) +const TEMPLATE_INTEGRATIONS: IntegrationDefinition[] = [ + { + id: 'stripe_sync_engine', + type: 'custom' as const, + requiredExtensions: ['pgmq', 'supabase_vault', 'pg_cron', 'pg_net'], + missingExtensionsAlert: , + name: `Stripe Sync Engine`, + status: 'alpha', + icon: ({ className, ...props } = {}) => ( + {'Stripe + ), + description: + 'Continuously sync your payments, customer, and other data from Stripe to your Postgres database', + docsUrl: 'https://github.com/stripe-experiments/sync-engine/', + author: { + name: 'Stripe', + websiteUrl: 'https://www.stripe.com', + }, + navigation: [ + { + route: 'overview', + label: 'Overview', + }, + { + route: 'settings', + label: 'Settings', + }, + ], + navigate: (_id: string, pageId: string = 'overview', _childId: string | undefined) => { + switch (pageId) { + case 'overview': + return dynamic( + () => + import( + 'components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview' + ).then((mod) => mod.StripeSyncInstallationPage), + { loading: Loading } + ) + case 'settings': + return dynamic( + () => + import( + 'components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage' + ).then((mod) => mod.StripeSyncSettingsPage), + { loading: Loading } + ) + } + return null + }, + }, +] + export const INTEGRATIONS: IntegrationDefinition[] = [ - ...wrapperIntegrations, - ...supabaseIntegrations, + ...WRAPPER_INTEGRATIONS, + ...SUPABASE_INTEGRATIONS, + ...TEMPLATE_INTEGRATIONS, ] diff --git a/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx b/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx index 1c1b2d34d920e..14e92904770d9 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx @@ -3,23 +3,36 @@ import { useMemo } from 'react' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useSchemasQuery } from 'data/database/schemas-query' import { useFDWsQuery } from 'data/fdw/fdws-query' +import { useFlag } from 'common' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { EMPTY_ARR } from 'lib/void' +import { + INSTALLATION_INSTALLED_SUFFIX, + STRIPE_SCHEMA_COMMENT_PREFIX, +} from 'stripe-experiment-sync/supabase' import { wrapperMetaComparator } from '../Wrappers/Wrappers.utils' import { INTEGRATIONS } from './Integrations.constants' export const useInstalledIntegrations = () => { const { data: project } = useSelectedProjectQuery() const { integrationsWrappers } = useIsFeatureEnabled(['integrations:wrappers']) + const stripeSyncEnabled = useFlag('enableStripeSyncEngineIntegration') const allIntegrations = useMemo(() => { - if (integrationsWrappers) { - return INTEGRATIONS - } else { - return INTEGRATIONS.filter((integration) => !integration.id.endsWith('_wrapper')) - } - }, [integrationsWrappers]) + return INTEGRATIONS.filter((integration) => { + if ( + !integrationsWrappers && + (integration.type === 'wrapper' || integration.id.endsWith('_wrapper')) + ) { + return false + } + if (!stripeSyncEnabled && integration.id === 'stripe_sync_engine') { + return false + } + return true + }) + }, [integrationsWrappers, stripeSyncEnabled]) const { data, @@ -58,16 +71,23 @@ export const useInstalledIntegrations = () => { const installedIntegrations = useMemo(() => { return allIntegrations - .filter((i) => { + .filter((integration) => { // special handling for supabase webhooks - if (i.id === 'webhooks') { + if (integration.id === 'webhooks') { return isHooksEnabled } - if (i.type === 'wrapper') { - return wrappers.find((w) => wrapperMetaComparator(i.meta, w)) + if (integration.id === 'stripe_sync_engine') { + const stripeSchema = schemas?.find(({ name }) => name === 'stripe') + return ( + !!stripeSchema?.comment?.startsWith(STRIPE_SCHEMA_COMMENT_PREFIX) && + !!stripeSchema.comment?.includes(INSTALLATION_INSTALLED_SUFFIX) + ) + } + if (integration.type === 'wrapper') { + return wrappers.find((w) => wrapperMetaComparator(integration.meta, w)) } - if (i.type === 'postgres_extension') { - return i.requiredExtensions.every((extName) => { + if (integration.type === 'postgres_extension') { + return integration.requiredExtensions.every((extName) => { const foundExtension = (extensions ?? []).find((ext) => ext.name === extName) return !!foundExtension?.installed_version }) @@ -75,13 +95,13 @@ export const useInstalledIntegrations = () => { return false }) .sort((a, b) => a.name.localeCompare(b.name)) - }, [wrappers, extensions, isHooksEnabled]) + }, [allIntegrations, wrappers, extensions, schemas, isHooksEnabled]) // available integrations are all integrations that can be installed. If an integration can't be installed (needed // extensions are not available on this DB image), the UI will provide a tooltip explaining why. const availableIntegrations = useMemo( () => allIntegrations.sort((a, b) => a.name.localeCompare(b.name)), - [] + [allIntegrations] ) const error = fdwError || extensionsError || schemasError diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview.tsx new file mode 100644 index 0000000000000..eb80da18dae5d --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview.tsx @@ -0,0 +1,403 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { useStripeSyncInstallMutation } from 'data/database-integrations/stripe/stripe-sync-install-mutation' +import { useStripeSyncUninstallMutation } from 'data/database-integrations/stripe/stripe-sync-uninstall-mutation' +import { useStripeSyncingState } from 'data/database-integrations/stripe/sync-state-query' +import { useSchemasQuery } from 'data/database/schemas-query' +import { formatRelative } from 'date-fns' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { useTrack } from 'lib/telemetry/track' +import { AlertCircle, BadgeCheck, Check, ExternalLink, RefreshCwIcon } from 'lucide-react' +import Link from 'next/link' +import { useEffect, useMemo, useRef, useState } from 'react' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' +import { + INSTALLATION_ERROR_SUFFIX, + INSTALLATION_INSTALLED_SUFFIX, + INSTALLATION_STARTED_SUFFIX, + STRIPE_SCHEMA_COMMENT_PREFIX, +} from 'stripe-experiment-sync/supabase' +import { + Button, + FormControl_Shadcn_, + FormField_Shadcn_, + Form_Shadcn_, + Sheet, + SheetContent, + SheetFooter, + SheetHeader, + SheetSection, + SheetTitle, +} from 'ui' +import { Admonition } from 'ui-patterns' +import { Input } from 'ui-patterns/DataInputs/Input' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import * as z from 'zod' +import { IntegrationOverviewTab } from '../../Integration/IntegrationOverviewTab' +import { StripeSyncChangesCard } from './StripeSyncChangesCard' + +const installFormSchema = z.object({ + stripeSecretKey: z.string().min(1, 'Stripe API key is required'), +}) + +export const StripeSyncInstallationPage = () => { + const { data: project } = useSelectedProjectQuery() + const track = useTrack() + const hasTrackedInstallFailed = useRef(false) + + const [shouldShowInstallSheet, setShouldShowInstallSheet] = useState(false) + const [isInstallInitiated, setIsInstallInitiated] = useState(false) + + const formId = 'stripe-sync-install-form' + const form = useForm>({ + resolver: zodResolver(installFormSchema), + defaultValues: { + stripeSecretKey: '', + }, + mode: 'onSubmit', + }) + + const { data: schemas } = useSchemasQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const { + mutate: installStripeSync, + isPending: isInstalling, + error: installError, + reset: resetInstallError, + } = useStripeSyncInstallMutation({ + onSuccess: () => { + toast.success('Stripe Sync installation started') + setShouldShowInstallSheet(false) + form.reset() + setIsInstallInitiated(true) + }, + }) + + const { mutate: uninstallStripeSync, isPending: isUninstalling } = useStripeSyncUninstallMutation( + { + onSuccess: () => { + toast.success('Stripe Sync uninstallation started') + }, + } + ) + + const stripeSchema = schemas?.find((s) => s.name === 'stripe') + + // Determine installation status from schema description + const isInstalled = + stripeSchema && + stripeSchema.comment?.startsWith(STRIPE_SCHEMA_COMMENT_PREFIX) && + stripeSchema.comment.includes(INSTALLATION_INSTALLED_SUFFIX) + + const schemaShowsInProgress = + stripeSchema && + stripeSchema.comment?.startsWith(STRIPE_SCHEMA_COMMENT_PREFIX) && + stripeSchema.comment?.includes(INSTALLATION_STARTED_SUFFIX) + + const setupInProgress = schemaShowsInProgress || isInstalling || isInstallInitiated + + const setupError = + stripeSchema && + stripeSchema.comment?.startsWith(STRIPE_SCHEMA_COMMENT_PREFIX) && + stripeSchema.comment?.includes(INSTALLATION_ERROR_SUFFIX) + + useEffect(() => { + if (!setupError) { + hasTrackedInstallFailed.current = false + return + } + + if (!hasTrackedInstallFailed.current) { + hasTrackedInstallFailed.current = true + // This isn't ideal because it will fire on every page load while in error state + // in the future we should connect this in the backend to track accurately + track('integration_install_failed', { + integrationName: 'stripe_sync_engine', + }) + } + }, [setupError, track]) + + useEffect(() => { + // Clear the install initiated flag once we detect completion or error from the schema + if (isInstallInitiated && (isInstalled || setupError)) { + setIsInstallInitiated(false) + } + }, [isInstallInitiated, isInstalled, setupError]) + + // Check if there's an existing stripe schema that wasn't created by this integration + const hasConflictingSchema = + stripeSchema && !stripeSchema.comment?.startsWith(STRIPE_SCHEMA_COMMENT_PREFIX) + + const canInstall = !hasConflictingSchema && !isInstalled && !setupInProgress + + // Sync state query - only enabled when installed + const { data: syncState } = useStripeSyncingState( + { + projectRef: project?.ref!, + connectionString: project?.connectionString, + }, + { + refetchInterval: 4000, + enabled: !!isInstalled, + } + ) + + // Poll for schema changes during installation + useSchemasQuery( + { + projectRef: project?.ref, + connectionString: project?.connectionString, + }, + { + refetchInterval: setupInProgress ? 5000 : false, + } + ) + + const isSyncing = !!syncState && !syncState.closed_at && syncState.status === 'running' + + const handleUninstall = () => { + if (!project?.ref) return + + uninstallStripeSync({ + projectRef: project.ref, + }) + } + + const handleOpenInstallSheet = () => { + resetInstallError() + setShouldShowInstallSheet(true) + } + + const handleCloseInstallSheet = (isOpen: boolean) => { + if (isInstalling) return + + setShouldShowInstallSheet(isOpen) + if (!isOpen) { + form.reset() + resetInstallError() + } + } + + const tableEditorUrl = `/project/${project?.ref}/editor?schema=stripe` + + const alert = useMemo(() => { + if (setupError) { + return ( + +
+ There was an error during the installation of the Stripe Sync Engine. Please try + reinstalling the integration. If the problem persists, contact support. +
+
+ + +
+
+ ) + } + + if (syncState) { + return ( + +
+ {isSyncing ? ( + <> +
+ +
Sync in progress...
+
+
+ Started {formatRelative(new Date(syncState.started_at!), new Date())} +
+ + ) : ( + <> +
+ +
All up to date
+ +
+
+ Last synced {formatRelative(new Date(syncState.closed_at!), new Date())} +
+ + )} +
+
+ ) + } + + return null + }, [ + setupError, + setupInProgress, + syncState, + isSyncing, + isUninstalling, + handleOpenInstallSheet, + handleUninstall, + ]) + + const status = useMemo(() => { + if (isInstalled) { + return ( + + Installed + + ) + } + if (setupInProgress) { + return ( + + + Installing... + + ) + } + if (syncState) { + return ( + + + Sync in progress... + + ) + } + if (setupError) { + return ( + + + Installation error + + ) + } + return ( + Not installed + ) + }, [isInstalled, setupInProgress, syncState, setupError]) + + return ( + setShouldShowInstallSheet(true)} + /> + ) : null + } + > + + + +
{ + if (!project?.ref) return + installStripeSync({ projectRef: project.ref, stripeSecretKey }) + })} + className="overflow-auto flex-grow px-0 flex flex-col" + > + + Install Stripe Sync Engine + + + +

Configuration

+ {installError && ( + +
+ +
+

Installation failed

+

{installError.message}

+
+
+
+ )} + + ( + + + field.onChange(e.target.value)} + /> + + + )} + /> + +
+ + +
+
+ + + + +
+
+
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx new file mode 100644 index 0000000000000..6ca75a277a553 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx @@ -0,0 +1,72 @@ +import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { EdgeFunctions } from 'icons' +import { Layers, Table } from 'lucide-react' +import { Card, CardContent, CardFooter, cn } from 'ui' + +type StripeSyncChangesCardProps = { + className?: string + canInstall?: boolean + onInstall?: () => void +} + +export const StripeSyncChangesCard = ({ + className, + canInstall = true, + onInstall, +}: StripeSyncChangesCardProps) => { + const showInstallButton = typeof onInstall === 'function' + + return ( +
+

This integration will modify your Supabase project:

+ + +
    +
  • + + + Creates a new database schema named stripe + + +
  • +
  • + + Creates tables and views in the stripe schema for synced Stripe data + + +
  • + + Deploys Edge Functions to handle incoming webhooks from Stripe +
  • +
  • + + Schedules automatic Stripe data syncs using Supabase Queues +
  • + + + {showInstallButton && ( + + + Install integration + + + )} + + + ) +} diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage.tsx new file mode 100644 index 0000000000000..6d5632832105c --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage.tsx @@ -0,0 +1,187 @@ +import { useSchemasQuery } from 'data/database/schemas-query' +import { useStripeSyncUninstallMutation } from 'data/database-integrations/stripe/stripe-sync-uninstall-mutation' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { Loader2, Table2 } from 'lucide-react' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' +import { toast } from 'sonner' +import { + INSTALLATION_INSTALLED_SUFFIX, + STRIPE_SCHEMA_COMMENT_PREFIX, +} from 'stripe-experiment-sync/supabase' +import { + AlertDescription_Shadcn_, + AlertTitle_Shadcn_, + Alert_Shadcn_, + Button, + Card, + CardContent, + WarningIcon, +} from 'ui' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { PageContainer } from 'ui-patterns/PageContainer' +import { + PageSection, + PageSectionContent, + PageSectionDescription, + PageSectionMeta, + PageSectionSummary, + PageSectionTitle, +} from 'ui-patterns/PageSection' + +export const StripeSyncSettingsPage = () => { + const router = useRouter() + const { data: project } = useSelectedProjectQuery() + const [showUninstallModal, setShowUninstallModal] = useState(false) + const [isUninstallInitiated, setIsUninstallInitiated] = useState(false) + + const { data: schemas, isLoading: isSchemasLoading } = useSchemasQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const stripeSchema = schemas?.find((s) => s.name === 'stripe') + + const isInstalled = + stripeSchema && + stripeSchema.comment?.startsWith(STRIPE_SCHEMA_COMMENT_PREFIX) && + stripeSchema.comment.includes(INSTALLATION_INSTALLED_SUFFIX) + + // Redirect to overview if not installed + useEffect(() => { + if (!isSchemasLoading && !isInstalled && project?.ref) { + router.push(`/project/${project.ref}/integrations/stripe_sync_engine/overview`) + } + }, [isSchemasLoading, isInstalled, project?.ref, router]) + + const { mutate: uninstallStripeSync, isPending: isUninstalling } = useStripeSyncUninstallMutation( + { + onSuccess: () => { + toast.success('Stripe Sync uninstallation started') + setShowUninstallModal(false) + setIsUninstallInitiated(true) + // Redirect to overview after uninstall + if (project?.ref) { + router.push(`/project/${project.ref}/integrations/stripe_sync_engine/overview`) + } + }, + } + ) + + const handleUninstall = () => { + if (!project?.ref) return + uninstallStripeSync({ projectRef: project.ref }) + } + + const tableEditorUrl = `/project/${project?.ref}/editor?schema=stripe` + + // Show loading state while checking installation status + if (isSchemasLoading || !isInstalled) { + return ( +
    + +
    + ) + } + + return ( + <> + + + + + Stripe Schema + + Access and manage the synced Stripe data in your database. + + + + + + +
    + +
    +
    +
    Open Stripe schema in Table Editor
    +

    + The Stripe Sync Engine stores all synced data in the stripe{' '} + schema. You can view and query this data directly in the Table Editor. +

    +
    + +
    +
    +
    +
    +
    +
    + + + + + Uninstall Stripe Sync Engine + + Remove the integration and all synced data from your project. + + + + + +
    + +
    +
    +
    + + Uninstalling will remove the stripe schema and all synced data. + + This action cannot be undone. +
    +
    + +
    +
    +
    +
    +
    +
    + + setShowUninstallModal(false)} + onConfirm={handleUninstall} + > +

    + Are you sure you want to uninstall the Stripe Sync Engine? This will: +

    +
      +
    • + Remove the stripe schema and all tables +
    • +
    • Delete all synced Stripe data
    • +
    • Remove the associated Edge Functions
    • +
    • Remove the scheduled sync jobs
    • +
    +

    + This action cannot be undone. +

    +
    + + ) +} diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx index e2de14c371723..b881e15ff8217 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx @@ -11,7 +11,6 @@ import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/Lay import ResizableAIWidget from 'components/ui/AIEditor/ResizableAIWidget' import { GridFooter } from 'components/ui/GridFooter' import { useSqlTitleGenerateMutation } from 'data/ai/sql-title-mutation' -import { useEntityDefinitionsQuery } from 'data/database/entity-definitions-query' import { constructHeaders, isValidConnString } from 'data/fetchers' import { lintKeys } from 'data/lint/keys' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' @@ -19,7 +18,6 @@ import { useExecuteSqlMutation } from 'data/sql/execute-sql-mutation' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { isError } from 'data/utils/error-check' import { useOrgAiOptInLevel } from 'hooks/misc/useOrgOptedIntoAi' -import { useSchemasForAi } from 'hooks/misc/useSchemasForAi' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { generateUuid } from 'lib/api/snippets.browser' @@ -94,8 +92,7 @@ export const SQLEditor = () => { const snapV2 = useSqlEditorV2StateSnapshot() const getImpersonatedRoleState = useGetImpersonatedRoleState() const databaseSelectorState = useDatabaseSelectorStateSnapshot() - const { includeSchemaMetadata, isHipaaProjectDisallowed } = useOrgAiOptInLevel() - const [selectedSchemas] = useSchemasForAi(project?.ref!) + const { isHipaaProjectDisallowed } = useOrgAiOptInLevel() const { sourceSqlDiff, @@ -149,16 +146,6 @@ export const SQLEditor = () => { { enabled: isValidConnString(project?.connectionString) } ) - const { data, refetch: refetchEntityDefinitions } = useEntityDefinitionsQuery( - { - schemas: selectedSchemas, - projectRef: project?.ref, - connectionString: project?.connectionString, - }, - { enabled: isValidConnString(project?.connectionString) && includeSchemaMetadata } - ) - const entityDefinitions = includeSchemaMetadata ? data?.map((def) => def.sql.trim()) : undefined - /* React query mutations */ const { mutateAsync: generateSqlTitle } = useSqlTitleGenerateMutation() const { mutate: sendEvent } = useSendEventMutation() @@ -166,9 +153,6 @@ export const SQLEditor = () => { onSuccess(data, vars) { if (id) snapV2.addResult(id, data.result, vars.autoLimit) - // Refetching instead of invalidating since invalidate doesn't work with `enabled` flag - refetchEntityDefinitions() - // revalidate lint query queryClient.invalidateQueries({ queryKey: lintKeys.lint(ref) }) }, @@ -410,7 +394,7 @@ export const SQLEditor = () => { } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [entityDefinitions, id, snapV2.results, snapV2.snippets]) + }, [id, snapV2.results, snapV2.snippets]) const acceptAiHandler = useCallback(async () => { try { diff --git a/apps/studio/data/database-integrations/stripe/keys.ts b/apps/studio/data/database-integrations/stripe/keys.ts new file mode 100644 index 0000000000000..d00b8f5b230fe --- /dev/null +++ b/apps/studio/data/database-integrations/stripe/keys.ts @@ -0,0 +1,6 @@ +export const stripeSyncKeys = { + all: ['stripe-sync'] as const, + syncState: (projectRef: string) => [...stripeSyncKeys.all, 'sync-state', projectRef] as const, + installationStatus: (projectRef: string) => + [...stripeSyncKeys.all, 'installation-status', projectRef] as const, +} diff --git a/apps/studio/data/database-integrations/stripe/stripe-sync-install-mutation.ts b/apps/studio/data/database-integrations/stripe/stripe-sync-install-mutation.ts new file mode 100644 index 0000000000000..a3157d3813844 --- /dev/null +++ b/apps/studio/data/database-integrations/stripe/stripe-sync-install-mutation.ts @@ -0,0 +1,91 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' + +import { getAccessToken } from 'common' +import { databaseKeys } from 'data/database/keys' +import { BASE_PATH } from 'lib/constants' +import { useTrack } from 'lib/telemetry/track' +import type { ResponseError } from 'types' +import { stripeSyncKeys } from './keys' + +export type StripeSyncInstallVariables = { + projectRef: string + stripeSecretKey: string +} + +export type StripeSyncInstallResponse = { + data: { message: string } | null + error: { message: string } | null +} + +export async function installStripeSync({ + projectRef, + stripeSecretKey, +}: StripeSyncInstallVariables): Promise { + const accessToken = await getAccessToken() + + const response = await fetch(`${BASE_PATH}/api/integrations/stripe-sync`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + projectRef, + stripeSecretKey, + }), + }) + + const result = await response.json() + + if (!response.ok) { + throw new Error(result.error?.message || 'Failed to install Stripe Sync') + } + + return result +} + +type StripeSyncInstallData = Awaited> + +export const useStripeSyncInstallMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + const track = useTrack() + + return useMutation({ + mutationFn: (vars) => installStripeSync(vars), + async onSuccess(data, variables, context) { + const { projectRef } = variables + + track('integration_install_started', { + integrationName: 'stripe_sync_engine', + }) + + // Invalidate schemas query to refresh installation status + await queryClient.invalidateQueries({ + queryKey: databaseKeys.schemas(projectRef), + }) + + // Also invalidate any stripe sync related queries + await queryClient.invalidateQueries({ + queryKey: stripeSyncKeys.all, + }) + + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to install Stripe Sync: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + }) +} diff --git a/apps/studio/data/database-integrations/stripe/stripe-sync-uninstall-mutation.ts b/apps/studio/data/database-integrations/stripe/stripe-sync-uninstall-mutation.ts new file mode 100644 index 0000000000000..d9830e22482b0 --- /dev/null +++ b/apps/studio/data/database-integrations/stripe/stripe-sync-uninstall-mutation.ts @@ -0,0 +1,88 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' + +import { getAccessToken } from 'common' +import { databaseKeys } from 'data/database/keys' +import { BASE_PATH } from 'lib/constants' +import { useTrack } from 'lib/telemetry/track' +import type { ResponseError } from 'types' +import { stripeSyncKeys } from './keys' + +export type StripeSyncUninstallVariables = { + projectRef: string +} + +export type StripeSyncUninstallResponse = { + data: { message: string } | null + error: { message: string } | null +} + +export async function uninstallStripeSync({ + projectRef, +}: StripeSyncUninstallVariables): Promise { + const accessToken = await getAccessToken() + + const response = await fetch(`${BASE_PATH}/api/integrations/stripe-sync`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + projectRef, + }), + }) + + const result = await response.json() + + if (!response.ok) { + throw new Error(result.error?.message || 'Failed to uninstall Stripe Sync') + } + + return result +} + +type StripeSyncUninstallData = Awaited> + +export const useStripeSyncUninstallMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + const track = useTrack() + + return useMutation({ + mutationFn: (vars) => uninstallStripeSync(vars), + async onSuccess(data, variables, context) { + const { projectRef } = variables + + track('integration_uninstall_started', { + integrationName: 'stripe_sync_engine', + }) + + // Invalidate schemas query to refresh installation status + await queryClient.invalidateQueries({ + queryKey: databaseKeys.schemas(projectRef), + }) + + // Also invalidate any stripe sync related queries + await queryClient.invalidateQueries({ + queryKey: stripeSyncKeys.all, + }) + + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to uninstall Stripe Sync: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + }) +} diff --git a/apps/studio/data/database-integrations/stripe/sync-state-query.ts b/apps/studio/data/database-integrations/stripe/sync-state-query.ts new file mode 100644 index 0000000000000..4e0f0db09abe4 --- /dev/null +++ b/apps/studio/data/database-integrations/stripe/sync-state-query.ts @@ -0,0 +1,64 @@ +import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query' +import { z } from 'zod' + +import { executeSql, ExecuteSqlError } from 'data/sql/execute-sql-query' +import { stripeSyncKeys } from './keys' + +export type DbConnection = { + projectRef: string + connectionString?: string | null +} + +const StripeSyncStateSchema = z + .object({ + started_at: z.string().nullable(), + closed_at: z.string().nullable(), + status: z.enum(['running', 'pending', 'complete', 'error']).nullable(), + }) + .nullable() + +export type StripeSyncState = z.infer + +export type StripeSyncStateData = z.infer +export type StripeSyncStateError = ExecuteSqlError + +export async function getStripeSyncState( + { projectRef, connectionString }: DbConnection, + signal?: AbortSignal +) { + const { result } = await executeSql( + { + projectRef, + connectionString, + sql: ` + SELECT started_at, closed_at, status FROM stripe.sync_runs WHERE status != 'pending' ORDER BY started_at DESC LIMIT 1; + `, + queryKey: stripeSyncKeys.syncState(projectRef), + }, + signal + ) + + return result.length > 0 ? StripeSyncStateSchema.parse(result[0]) : null +} + +export const useStripeSyncingState = ( + { projectRef, connectionString }: DbConnection, + { + enabled = true, + ...options + }: Omit< + UseQueryOptions, + 'queryKey' | 'queryFn' + > = {} +) => { + return useQuery({ + queryKey: stripeSyncKeys.syncState(projectRef), + queryFn: ({ signal }) => getStripeSyncState({ projectRef, connectionString }, signal), + enabled: enabled && typeof projectRef !== 'undefined', + ...options, + }) +} + +export function invalidateStripeSyncStateQuery(client: QueryClient, projectRef: string) { + return client.invalidateQueries({ queryKey: stripeSyncKeys.syncState(projectRef) }) +} diff --git a/apps/studio/package.json b/apps/studio/package.json index 46216f4cbabf0..aafb78a3d81c9 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -136,6 +136,7 @@ "sql-formatter": "^15.0.0", "sqlstring": "^2.3.2", "streamdown": "^1.3.0", + "stripe-experiment-sync": "^1.0.18", "tus-js-client": "^4.1.0", "ui": "workspace:*", "ui-patterns": "workspace:*", diff --git a/apps/studio/pages/api/integrations/stripe-sync.ts b/apps/studio/pages/api/integrations/stripe-sync.ts new file mode 100644 index 0000000000000..e88c53f5b28d2 --- /dev/null +++ b/apps/studio/pages/api/integrations/stripe-sync.ts @@ -0,0 +1,141 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { z } from 'zod' +import { install, uninstall } from 'stripe-experiment-sync/supabase' +import { VERSION } from 'stripe-experiment-sync' +import { waitUntil } from '@vercel/functions' + +const ENABLE_FLAG_KEY = 'enableStripeSyncEngineIntegration' + +const InstallBodySchema = z.object({ + projectRef: z.string().min(1), + stripeSecretKey: z.string().min(1), +}) + +const UninstallBodySchema = z.object({ + projectRef: z.string().min(1), +}) + +async function isStripeSyncEnabled() { + // The ConfigClient doesn't seem to work properly so we'll just gate access from the frontend + // for now + return true +} + +function getBearerToken(req: NextApiRequest) { + const authHeader = req.headers.authorization + if (!authHeader || Array.isArray(authHeader)) return null + const match = authHeader.match(/^Bearer\s+(.+)$/i) + return match?.[1]?.trim() ?? null +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + // Hide endpoint if the integration is disabled. + if (!(await isStripeSyncEnabled())) { + return res.status(404).json({ data: null, error: { message: 'Not Found' } }) + } + + const { method } = req + switch (method) { + case 'POST': + return handleSetupStripeSyncInstall(req, res) + case 'DELETE': + return handleDeleteStripeSyncInstall(req, res) + default: + return res + .status(405) + .json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +async function handleDeleteStripeSyncInstall(req: NextApiRequest, res: NextApiResponse) { + const supabaseToken = getBearerToken(req) + if (!supabaseToken) { + return res + .status(401) + .json({ data: null, error: { message: 'Unauthorized: Invalid Authorization header' } }) + } + + const parsed = UninstallBodySchema.safeParse(req.body) + if (!parsed.success) { + return res + .status(400) + .json({ data: null, error: { message: 'Bad Request: Invalid request body' } }) + } + const { projectRef } = parsed.data + + waitUntil( + uninstall({ + supabaseAccessToken: supabaseToken, + supabaseProjectRef: projectRef, + baseProjectUrl: process.env.NEXT_PUBLIC_CUSTOMER_DOMAIN, + supabaseManagementUrl: process.env.NEXT_PUBLIC_API_DOMAIN, + }).catch((error) => { + console.error('Stripe Sync Engine uninstallation failed.', error) + throw error + }) + ) + + return res + .status(200) + .json({ data: { message: 'Stripe Sync uninstallation initiated' }, error: null }) +} + +async function handleSetupStripeSyncInstall(req: NextApiRequest, res: NextApiResponse) { + const supabaseToken = getBearerToken(req) + if (!supabaseToken) { + return res + .status(401) + .json({ data: null, error: { message: 'Unauthorized: Invalid Authorization header' } }) + } + + const parsed = InstallBodySchema.safeParse(req.body) + if (!parsed.success) { + return res + .status(400) + .json({ data: null, error: { message: 'Bad Request: Invalid request body' } }) + } + const { projectRef, stripeSecretKey } = parsed.data + + // Validate the Stripe API key before proceeding with installation + try { + const stripeResponse = await fetch('https://api.stripe.com/v1/account', { + method: 'GET', + headers: { + Authorization: `Bearer ${stripeSecretKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }) + + if (!stripeResponse.ok) { + const errorData = await stripeResponse.json() + const errorMessage = + errorData.error?.message || `Invalid Stripe API key (HTTP ${stripeResponse.status})` + return res.status(400).json({ + data: null, + error: { message: `Invalid Stripe API key: ${errorMessage}` }, + }) + } + } catch (error: any) { + return res.status(400).json({ + data: null, + error: { message: `Failed to validate Stripe API key: ${error.message}` }, + }) + } + waitUntil( + install({ + supabaseAccessToken: supabaseToken, + supabaseProjectRef: projectRef, + stripeKey: stripeSecretKey, + baseProjectUrl: process.env.NEXT_PUBLIC_CUSTOMER_DOMAIN, + supabaseManagementUrl: process.env.NEXT_PUBLIC_API_DOMAIN, + packageVersion: VERSION, + }).catch((error) => { + console.error('Stripe Sync Engine installation failed.', error) + throw error + }) + ) + + return res + .status(200) + .json({ data: { message: 'Stripe Sync setup initiated', version: VERSION }, error: null }) +} diff --git a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx index 4577175c6159a..8ca9eb94dde55 100644 --- a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx @@ -2,7 +2,7 @@ import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useMemo } from 'react' -import { useParams } from 'common' +import { useFlag, useParams } from 'common' import { INTEGRATIONS } from 'components/interfaces/Integrations/Landing/Integrations.constants' import { useInstalledIntegrations } from 'components/interfaces/Integrations/Landing/useInstalledIntegrations' import { DefaultLayout } from 'components/layouts/DefaultLayout' @@ -41,6 +41,7 @@ const IntegrationPage: NextPageWithLayout = () => { const router = useRouter() const { ref, id, pageId, childId } = useParams() const { integrationsWrappers } = useIsFeatureEnabled(['integrations:wrappers']) + const stripeSyncEnabled = useFlag('enableStripeSyncEngineIntegration') const { installedIntegrations: installedIntegrations, isLoading: isIntegrationsLoading } = useInstalledIntegrations() @@ -135,6 +136,10 @@ const IntegrationPage: NextPageWithLayout = () => { return null } + if (id === 'stripe_sync_engine' && !stripeSyncEnabled) { + return + } + if (!integrationsWrappers && id?.endsWith('_wrapper')) { return } diff --git a/apps/studio/pages/project/[ref]/integrations/index.tsx b/apps/studio/pages/project/[ref]/integrations/index.tsx index 2788918925a8d..4e77700840332 100644 --- a/apps/studio/pages/project/[ref]/integrations/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/index.tsx @@ -25,14 +25,16 @@ import { PageHeaderTitle, } from 'ui-patterns/PageHeader' import { PageSection, PageSectionContent, PageSectionMeta } from 'ui-patterns/PageSection' +import { useFlag } from 'common' -const FEATURED_INTEGRATIONS = ['cron', 'queues', 'stripe_wrapper'] +const FEATURED_INTEGRATIONS = ['cron', 'queues'] // + either stripe_sync_engine or stripe_wrapper depending on feature flag // Featured integration images const FEATURED_INTEGRATION_IMAGES: Record = { cron: 'img/integrations/covers/cron-cover.webp', queues: 'img/integrations/covers/queues-cover.png', stripe_wrapper: 'img/integrations/covers/stripe-cover.png', + stripe_sync_engine: 'img/integrations/covers/stripe-cover.png', } const IntegrationsPage: NextPageWithLayout = () => { @@ -45,6 +47,16 @@ const IntegrationsPage: NextPageWithLayout = () => { parseAsString.withDefault('').withOptions({ clearOnDefault: true }) ) + const isStripeSyncEngineEnabled = useFlag('enableStripeSyncEngineIntegration') + + const featuredIntegrationIds = useMemo(() => { + if (isStripeSyncEngineEnabled) { + return FEATURED_INTEGRATIONS.concat(['stripe_sync_engine']) + } else { + return FEATURED_INTEGRATIONS.concat(['stripe_wrapper']) + } + }, [isStripeSyncEngineEnabled]) + const { availableIntegrations, installedIntegrations, error, isError, isLoading, isSuccess } = useInstalledIntegrations() @@ -105,8 +117,9 @@ const IntegrationsPage: NextPageWithLayout = () => { } const featured = filteredAndSortedIntegrations.filter((i) => - FEATURED_INTEGRATIONS.includes(i.id) + featuredIntegrationIds.includes(i.id) ) + const allIntegrations = filteredAndSortedIntegrations // Include all integrations, including featured return { diff --git a/apps/studio/proxy.ts b/apps/studio/proxy.ts index 395a9d4661ff2..08380bd1bd1af 100644 --- a/apps/studio/proxy.ts +++ b/apps/studio/proxy.ts @@ -25,6 +25,7 @@ const HOSTED_SUPPORTED_API_URLS = [ '/edge-functions/body', '/generate-attachment-url', '/incident-status', + '/api/integrations/stripe-sync', ] export function proxy(request: NextRequest) { diff --git a/apps/studio/static-data/integrations/stripe_sync_engine/overview.md b/apps/studio/static-data/integrations/stripe_sync_engine/overview.md new file mode 100644 index 0000000000000..129ba58920e44 --- /dev/null +++ b/apps/studio/static-data/integrations/stripe_sync_engine/overview.md @@ -0,0 +1,3 @@ +[Stripe](https://stripe.com/) is an API driven payment processing and subscription management platform. + +The Stripe Sync Engine will sync data from your Stripe account including customers, subscriptions, invoices, and payments into Postgres tables in your Supabase account. diff --git a/apps/www/_blog/2025-12-19-stripe-sync-engine-integration.mdx b/apps/www/_blog/2025-12-19-stripe-sync-engine-integration.mdx new file mode 100644 index 0000000000000..4cc82b336a9f7 --- /dev/null +++ b/apps/www/_blog/2025-12-19-stripe-sync-engine-integration.mdx @@ -0,0 +1,148 @@ +--- +title: 'Sync Stripe data to your Supabase database in one click' +description: 'Announcing official support for the Stripe Sync Engine in the Supabase Dashboard. Get a one-click integration that syncs your Stripe data directly into your Supabase database.' +categories: + - product +tags: + - stripe + - integrations +date: '2025-12-19' +toc_depth: 3 +author: matt_linkous,prashant +image: 2025-12-19-stripe-sync-engine-integration/og.png +thumb: 2025-12-19-stripe-sync-engine-integration/thumb.png +--- + +Today we are announcing a partnership with Stripe and official support for the [Stripe Sync Engine](/blog/stripe-engine-as-sync-library) in the Supabase Dashboard. Now, you get a one-click integration that syncs your Stripe data directly into your Supabase database. Query your customers, subscriptions, invoices, and payments using standard SQL. + +This integration is the result of a collaboration between Supabase and Stripe. Stripe engineers contributed significant improvements to the open-source sync engine, including incremental sync, flexible JSONB storage, and a new CLI. + +## Why sync Stripe data to your database? + +Any application that makes money has to incorporate payments into their application and has billing data worth exploring. Understanding your revenue means joining Stripe data with your application data: Which customers are on which plan? What features do paying users actually use? Which accounts are at risk of churning? + +Traditionally, developers face two options: + +1. **Make API calls on demand.** Hit the Stripe API whenever you need billing data. This works until you need to join data across tables, aggregate across thousands of records, or run complex analytics. +2. **Build a sync pipeline.** Write code to fetch Stripe data, transform it, handle pagination, manage webhooks, deal with rate limits, and keep everything in sync. This typically takes days or weeks to build correctly. + +The Stripe Sync Engine gives you a third option: click a button and query your Stripe data in SQL within minutes. + +## How it works + +The sync engine keeps your Stripe data current through two mechanisms: webhooks for real-time updates and scheduled backfills for historical data. When you enable the integration, Supabase automatically configures both. + +Once your data is synced, you can query it like any other Postgres table. Here are three scenarios we expect to be especially common. + +### Find users who signed up but never converted + +Join your Stripe customers with your auth users to identify accounts that created a login but never started a paid subscription. This is the kind of query that's impractical with API calls but trivial with local data. + +```sql +select + users.email, + users.created_at as signed_up, + now() - users.created_at as days_since_signup +from + auth.users +left join + stripe.customers on customers.email = users.email +left join + stripe.subscriptions on subscriptions.customer = customers.id +where + subscriptions.id is null + and users.created_at < now() - interval '7 days' +order by + users.created_at; +``` + +### Calculate MRR by plan + +Aggregate your subscription data to see revenue broken down by product. With local data, this query runs in milliseconds regardless of how many subscriptions you have. + +```sql +select + products.name as plan, + count(*) as subscribers, + sum(prices.unit_amount) / 100.0 as mrr +from stripe.subscriptions as subscriptions +join stripe.prices as prices + on prices.id = (subscriptions.plan::json->>'id')::text +join stripe.products as products + on products.id = prices.product +where subscriptions.status = 'active' +group by products.name +order by mrr desc; +``` + +### Identify at-risk accounts + +Combine billing data with application usage to find paying customers who have gone quiet. This query joins three data sources that would require separate API calls and manual correlation without the sync engine. + +```sql +select + customers.email, + subscriptions.current_period_end as renewal_date, + max(user_events.created_at) as last_active +from stripe.customers as customers +join stripe.subscriptions as subscriptions + on subscriptions.customer = customers.id +join public.user_events as user_events + on user_events.user_id = customers.metadata->>'user_id' +where subscriptions.status = 'active' +group by customers.email, subscriptions.current_period_end +having max(user_events.created_at) < now() - interval '30 days' +order by subscriptions.current_period_end; +``` + +## Stripe Sync Engine vs Foreign Data Wrapper + +Supabase already offers a [Stripe Foreign Data Wrapper](/blog/postgres-foreign-data-wrappers-with-wasm) that lets you query Stripe data using SQL. The FDW is a thin layer that translates your SQL queries into Stripe API calls. When you run `select * from stripe.customers`, the FDW makes an API request to Stripe, transforms the JSON response into rows, and returns the results. + +This works well for simple lookups. Need to check a customer's subscription status? The FDW handles it elegantly. But the abstraction breaks down when your queries get more complex or for queries that are critical to your application. + +Consider a query that joins customers with subscriptions and filters by plan type. The FDW must make separate API calls for each table, fetch all the data into Postgres, and then perform the join locally. A query that takes milliseconds on local data can take seconds or minutes through the FDW. And if you're running this query frequently, you'll hit Stripe's rate limits. + +The Sync Engine takes a fundamentally different approach. Instead of translating queries to API calls, it copies your Stripe data into actual Postgres tables. Your queries run against local, indexed data. Joins are fast. Aggregations are fast. There are no rate limits because you're not hitting an external API. + +Ultimately, the FDW is great for occasional lookups and simple queries. The Sync Engine is built for applications that need billing data as a first-class part of their database. + +## Reliable syncing with Supabase Queues + +There can be a lot of data to import from your Stripe account to your database, especially when you're setting up syncing with an existing account. To perform this backfilling process efficiently and reliably, the sync engine uses [Supabase Queues](/blog/supabase-queues) (powered by pgmq) to sync data incrementally and in batches. + +When you install the integration, the backfilling process will automatically start and spread the work across Edge Functions that can concurrently fetch data from the Stripe API and retry if there are any failures or rate limits. + +## Getting started + +Enable the Stripe Sync Engine from your Supabase dashboard: + +1. Navigate to **Integrations** in your project dashboard +2. Find **Stripe Sync Engine** and click **Install** +3. Enter your Stripe API key (we recommend a restricted key with read-only access) +4. Syncing will start automatically after the installation completes + +![Stripe Sync Engine integration in the Supabase Dashboard](/images/blog/2025-12-19-stripe-sync-engine-integration/dashboard.png) + +The initial backfill will sync your historical data. Depending on your Stripe account size, this may take a few minutes to a few hours. Webhooks begin processing immediately, so new events are captured in real-time. + +## Open source collaboration + +The Stripe Sync Engine is open source. Stripe engineers contributed improvements including: + +- **JSONB storage with generated columns** for flexibility without sacrificing query performance +- **Incremental sync** using Stripe's cursor-based pagination and Supabase Queues +- **Multi-account support** for Stripe Connect platforms +- **New CLI** with proper event ordering + +View the source and contribute: [github.com/stripe-experiments/sync-engine/](https://github.com/stripe-experiments/sync-engine/) + +## What's next + +We're working with Stripe on additional improvements: + +- Dashboard UI for detailed monitoring of the sync engine +- Pre-built SQL views for common metrics (MRR, churn, LTV) +- Support for additional Stripe objects and other customizations + +Start syncing your Stripe data today. Enable the integration from your [Supabase dashboard](https://supabase.com/dashboard). diff --git a/apps/www/lib/authors.json b/apps/www/lib/authors.json index 0f825a0e8bd9d..acc223b3e8b4d 100644 --- a/apps/www/lib/authors.json +++ b/apps/www/lib/authors.json @@ -808,5 +808,13 @@ "position": "AI Tooling Engineer", "author_url": "https://github.com/Rodriguespn", "author_image_url": "https://github.com/Rodriguespn.png" + }, + { + "author_id": "matt_linkous", + "author": "Matt Linkous", + "username": "matlin", + "position": "Head of Integrations", + "author_url": "https://github.com/matlin", + "author_image_url": "https://github.com/matlin.png" } ] diff --git a/apps/www/public/images/blog/2025-12-19-stripe-sync-engine-integration/dashboard.png b/apps/www/public/images/blog/2025-12-19-stripe-sync-engine-integration/dashboard.png new file mode 100644 index 0000000000000..56eeeaa9af5c6 Binary files /dev/null and b/apps/www/public/images/blog/2025-12-19-stripe-sync-engine-integration/dashboard.png differ diff --git a/apps/www/public/images/blog/2025-12-19-stripe-sync-engine-integration/og.png b/apps/www/public/images/blog/2025-12-19-stripe-sync-engine-integration/og.png new file mode 100644 index 0000000000000..abbb7b8b8658d Binary files /dev/null and b/apps/www/public/images/blog/2025-12-19-stripe-sync-engine-integration/og.png differ diff --git a/apps/www/public/images/blog/2025-12-19-stripe-sync-engine-integration/thumb.png b/apps/www/public/images/blog/2025-12-19-stripe-sync-engine-integration/thumb.png new file mode 100644 index 0000000000000..abbb7b8b8658d Binary files /dev/null and b/apps/www/public/images/blog/2025-12-19-stripe-sync-engine-integration/thumb.png differ diff --git a/apps/www/public/rss.xml b/apps/www/public/rss.xml index 2d7e4f1c9c124..6703a0c9d6c68 100644 --- a/apps/www/public/rss.xml +++ b/apps/www/public/rss.xml @@ -4,9 +4,16 @@ https://supabase.com Latest news from Supabase en - Wed, 17 Dec 2025 00:00:00 -0700 + Fri, 19 Dec 2025 00:00:00 -0700 + https://supabase.com/blog/stripe-sync-engine-integration + One-click Stripe data in your Supabase database + https://supabase.com/blog/stripe-sync-engine-integration + Announcing official support for the Stripe Sync Engine in the Supabase Dashboard. Get a one-click integration that syncs your Stripe data directly into your Supabase database. + Fri, 19 Dec 2025 00:00:00 -0700 + + https://supabase.com/blog/building-chatgpt-apps-with-supabase Building ChatGPT Apps with Supabase Edge Functions and mcp-use https://supabase.com/blog/building-chatgpt-apps-with-supabase diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts index 3a41d04f115d4..4887b9b4a2417 100644 --- a/packages/common/telemetry-constants.ts +++ b/packages/common/telemetry-constants.ts @@ -2601,6 +2601,98 @@ export interface RequestUpgradeSubmittedEvent { groups: Omit } +/** + * User succesfully installed an integration via the integrations marketplace in the dashboard. + * Note: This excludes Wrappers and Postgres Extensions. + * + * @group Events + * @source studio + * @page /dashboard/project/{ref}/integrations/{integration_slug} + */ +export interface IntegrationInstalledEvent { + action: 'integration_installed' + properties: { + /** + * The name of the integration installed + */ + integrationName: string + } + groups: TelemetryGroups +} + +/** + * User started installing an integration via the integrations marketplace. + * + * @group Events + * @source studio + * @page /dashboard/project/{ref}/integrations/{integration_slug} + */ +export interface IntegrationInstallStartedEvent { + action: 'integration_install_started' + properties: { + /** + * The name of the integration being installed + */ + integrationName: string + } + groups: TelemetryGroups +} + +/** + * User started uninstalling an integration via the integrations marketplace. + * + * @group Events + * @source studio + * @page /dashboard/project/{ref}/integrations/{integration_slug} + */ +export interface IntegrationUninstallStartedEvent { + action: 'integration_uninstall_started' + properties: { + /** + * The name of the integration being uninstalled + */ + integrationName: string + } + groups: TelemetryGroups +} + +/** + * Installation failed for an integration. + * + * @group Events + * @source studio + * @page /dashboard/project/{ref}/integrations/{integration_slug} + */ +export interface IntegrationInstallFailedEvent { + action: 'integration_install_failed' + properties: { + /** + * The name of the integration whose installation failed + */ + integrationName: string + } + groups: TelemetryGroups +} + +/** + * User uninstalled an integration via the integrations marketplace in the dashboard. + * Note: This excludes Wrappers and Postgres Extensions. + * + * @group Events + * @source studio + * @page /dashboard/project/{ref}/integrations/{integration_slug} + */ +export interface IntegrationUninstalledEvent { + action: 'integration_uninstalled' + properties: { + /** + * The name of the integration installed + */ + integrationName: string + } + groups: TelemetryGroups +} + /** * @hidden */ @@ -2746,3 +2838,8 @@ export type TelemetryEvent = | QueryPerformanceAIExplanationButtonClickedEvent | RequestUpgradeModalOpenedEvent | RequestUpgradeSubmittedEvent + | IntegrationInstalledEvent + | IntegrationInstallStartedEvent + | IntegrationUninstallStartedEvent + | IntegrationInstallFailedEvent + | IntegrationUninstalledEvent diff --git a/packages/pg-meta/src/pg-meta-schemas.ts b/packages/pg-meta/src/pg-meta-schemas.ts index 5c2452bacd329..f66506244c7f7 100644 --- a/packages/pg-meta/src/pg-meta-schemas.ts +++ b/packages/pg-meta/src/pg-meta-schemas.ts @@ -7,6 +7,7 @@ const pgSchemaZod = z.object({ id: z.number(), name: z.string(), owner: z.string(), + comment: z.string().nullable(), }) const pgSchemaArrayZod = z.array(pgSchemaZod) const pgSchemaOptionalZod = z.optional(pgSchemaZod) diff --git a/packages/pg-meta/src/sql/schemas.ts b/packages/pg-meta/src/sql/schemas.ts index f48af16c72c8f..092e78495b82c 100644 --- a/packages/pg-meta/src/sql/schemas.ts +++ b/packages/pg-meta/src/sql/schemas.ts @@ -4,7 +4,8 @@ export const SCHEMAS_SQL = /* SQL */ ` select n.oid as id, n.nspname as name, - u.rolname as owner + u.rolname as owner, + obj_description(n.oid, 'pg_namespace') AS comment from pg_namespace n, pg_roles u diff --git a/packages/pg-meta/test/schemas.test.ts b/packages/pg-meta/test/schemas.test.ts index 8e87e3c4ef09b..a973770295c02 100644 --- a/packages/pg-meta/test/schemas.test.ts +++ b/packages/pg-meta/test/schemas.test.ts @@ -33,11 +33,12 @@ withTestDatabase('list with system schemas', async ({ executeQuery }) => { { id: expect.any(Number) }, ` { + "comment": "system catalog schema", "id": Any, "name": "pg_catalog", "owner": "postgres", } - ` + ` ) }) @@ -51,11 +52,12 @@ withTestDatabase('list without system schemas', async ({ executeQuery }) => { { id: expect.any(Number) }, ` { + "comment": "standard public schema", "id": Any, "name": "public", "owner": "postgres", } - ` + ` ) }) @@ -71,11 +73,12 @@ withTestDatabase('retrieve, create, update, delete', async ({ executeQuery }) => { id: expect.any(Number) }, ` { + "comment": null, "id": Any, "name": "s", "owner": "postgres", } - ` + ` ) // Retrieve schema again to verify @@ -87,11 +90,12 @@ withTestDatabase('retrieve, create, update, delete', async ({ executeQuery }) => { id: expect.any(Number) }, ` { + "comment": null, "id": Any, "name": "s", "owner": "postgres", } - ` + ` ) // Update schema @@ -113,11 +117,12 @@ withTestDatabase('retrieve, create, update, delete', async ({ executeQuery }) => { id: expect.any(Number) }, ` { + "comment": null, "id": Any, "name": "ss", "owner": "postgres", } - ` + ` ) // Delete schema diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1fcfa507cb345..1febf67dc7381 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -489,7 +489,7 @@ importers: version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nuqs: specifier: ^1.19.1 - version: 1.19.1(next@15.5.9(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)) + version: 1.19.1(next@15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)) openai: specifier: ^4.75.1 version: 4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76) @@ -1034,6 +1034,9 @@ importers: streamdown: specifier: ^1.3.0 version: 1.3.0(@types/react@18.3.3)(react@18.3.1)(supports-color@8.1.1) + stripe-experiment-sync: + specifier: ^1.0.18 + version: 1.0.18(@types/node@22.13.14)(supports-color@8.1.1) tus-js-client: specifier: ^4.1.0 version: 4.1.0 @@ -4430,6 +4433,28 @@ packages: cpu: [x64] os: [win32] + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/confirm@5.1.8': resolution: {integrity: sha512-dNLWCYZvXDjO3rnQfk2iuJNL4Ivwz/T2+C3+WnNfJKsNGSuOs3wAo2F6e0p946gtSAk31nZMfW+MRmYaplPKsg==} engines: {node: '>=18'} @@ -4448,10 +4473,122 @@ packages: '@types/node': optional: true + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/figures@1.0.11': resolution: {integrity: sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==} engines: {node: '>=18'} + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/type@3.0.5': resolution: {integrity: sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==} engines: {node: '>=18'} @@ -4903,6 +5040,91 @@ packages: cpu: [x64] os: [win32] + '@ngrok/ngrok-android-arm64@1.6.0': + resolution: {integrity: sha512-gOfVVjYq7ruPMgH108j99uaBUkwD1DxDpnk+RufQHzxRKL72MZad7QfaRwc1v8iRMyOh3EKYYvU2y8HSXDbSkA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@ngrok/ngrok-darwin-arm64@1.6.0': + resolution: {integrity: sha512-YdKlEkz0PHHnoy2UlEH2XMrupetmT6K6bLrF+MFWrKYKpIbOuGV4HcVioh1iu0NTmn7xJwrAbo/Mwx2E95/qdg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@ngrok/ngrok-darwin-universal@1.6.0': + resolution: {integrity: sha512-yiUCkg10+vUKgvkD2rAZHlzvZmINUNF7mna7iL+ydh4pOx6jamSwDQ2QRseVAyxC9eZVy/rKFYLVnz2Ci+0D9w==} + engines: {node: '>= 10'} + os: [darwin] + + '@ngrok/ngrok-darwin-x64@1.6.0': + resolution: {integrity: sha512-a6LbwODDDCRpriILe+1lq+zuIjGbj+pR1A0KThTP8riVTQ538DMqR09kv5Oz7ZqcclS/IK+3ZLKkxHuPX9ob3g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@ngrok/ngrok-freebsd-x64@1.6.0': + resolution: {integrity: sha512-t77ayWr0zPrW75DstzvoE6Hwj7h1mpQSFKQ3I0B3iC9o7XZhcNkwFpX8QLbmQNkeHxz5UhlBtzOYPdzDJY+3uw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@ngrok/ngrok-linux-arm-gnueabihf@1.6.0': + resolution: {integrity: sha512-NBCJnPAbl44Ua3dpO+kOkyz4cGGLu2FiDcEHBES7OARiSscoui+DyNPLe5auZFjag5/0B3ZNiDgCcdRBAi+KNQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@ngrok/ngrok-linux-arm64-gnu@1.6.0': + resolution: {integrity: sha512-i65WIaeImO2/oyTfwUAVW1j/poLMzX/PAYsDRLXOac6/1DV6kIQX1kmr+CEs6NtmKgD76QiApbqtksd9MDlncg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@ngrok/ngrok-linux-arm64-musl@1.6.0': + resolution: {integrity: sha512-7osqE43Pl6HiTeLZSEnSTtgbNQ1zqYovYjZ7lGgeKJHlgWXE6RvE7qhdjDw17ebUuT0IS8JPGZFnlyF6J0UKPQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@ngrok/ngrok-linux-x64-gnu@1.6.0': + resolution: {integrity: sha512-jP3enLStnTGTKLBXqctVt3wIVvGLP/BkLKYuXwrU1EqchtvDcsAhpspeUeX23SAtLXUgbX9vN1B6IvsAxWcyxw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@ngrok/ngrok-linux-x64-musl@1.6.0': + resolution: {integrity: sha512-hrBrXCofgmY9pc36N0nfDS+bfomu5hOqt0N5IQkL4n0UaQcl48tKddIex2eR+mU3QBYh6p1XKxAwyAVjwplD2w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@ngrok/ngrok-win32-arm64-msvc@1.6.0': + resolution: {integrity: sha512-EXJXFaXh/cNRaTqs02EmysILtcSImFWwC98mjG6PU5MEG+gqNz8AddnqFytY0RRxkR9SxwnC+7QAV3qYOSU6kA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@ngrok/ngrok-win32-ia32-msvc@1.6.0': + resolution: {integrity: sha512-R+5WB6ONT4ynHFusxRw8kbXx2SBYftl6XUiNQNmwwCdwdNO7fPzdwhbmbzgfCEjQrYQrXD1vs5J+zJJeSaAicw==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@ngrok/ngrok-win32-x64-msvc@1.6.0': + resolution: {integrity: sha512-4QViW0GSPsjmghYPExO0Jt9DtIybK23MXPP0BltOuoFP0iyA15vb03QXAI4223zlSLWSWYnOx+qnCuzV1qT0lQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@ngrok/ngrok@1.6.0': + resolution: {integrity: sha512-TWC6nh4Kl16HjhG/bVhpYNP5UIyJc9oZetCrnO4r5+gq95eIxZKoLXzuAyC0gF/J+NEY3c1euFAET5Sn9wOWaw==} + engines: {node: '>= 10'} + '@noble/ciphers@1.3.0': resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} engines: {node: ^14.21.3 || >=16} @@ -10040,6 +10262,9 @@ packages: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + array-includes@3.1.8: resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} engines: {node: '>= 0.4'} @@ -10245,6 +10470,10 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + body-parser@1.20.4: + resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.1: resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} engines: {node: '>=18'} @@ -10436,6 +10665,9 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + charenc@0.0.2: resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} @@ -10677,6 +10909,10 @@ packages: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + commander@14.0.1: resolution: {integrity: sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==} engines: {node: '>=20'} @@ -10795,6 +11031,9 @@ packages: cookie-es@2.0.0: resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==} + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -12177,6 +12416,10 @@ packages: peerDependencies: express: ^4.11 || 5 || ^5.0.0-beta.1 + express@4.22.1: + resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + engines: {node: '>= 0.10.0'} + express@5.1.0: resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} engines: {node: '>= 18'} @@ -12375,6 +12618,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + finalhandler@2.1.0: resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} engines: {node: '>= 0.8'} @@ -13277,6 +13524,15 @@ packages: react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 + inquirer@12.11.1: + resolution: {integrity: sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + inquirer@8.2.6: resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} engines: {node: '>=12.0.0'} @@ -15041,7 +15297,6 @@ packages: next@15.5.9: resolution: {integrity: sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} - deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -15506,6 +15761,9 @@ packages: openapi-fetch@0.13.8: resolution: {integrity: sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ==} + openapi-fetch@0.6.2: + resolution: {integrity: sha512-Faj29Kzh7oCbt1bz6vAGNKtRJlV/GolOQTx87eYUnfCK7eVXdN9jQVojroc7tcJ5OQgyhbeOqD7LS/8UtGBnMQ==} + openapi-sampler@1.6.1: resolution: {integrity: sha512-s1cIatOqrrhSj2tmJ4abFYZQK6l5v+V4toO5q1Pa0DyN8mtyqy2I+Qrj5W9vOELEtybIMQs/TBZGVO/DtTFK8w==} @@ -15722,6 +15980,9 @@ packages: resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} engines: {node: 20 || >=22} + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -15804,6 +16065,11 @@ packages: resolution: {integrity: sha512-NoSsPqXxbkD8RIe+peQCqiea4QzXgosdTKY8p7PsbbGsh2F8TifDj/vJxfuR8qJwNYrijdSs7uf0tAe6WOyCsQ==} engines: {node: '>=12.0.0'} + pg-node-migrations@0.0.8: + resolution: {integrity: sha512-44cMl9umOmCv0hzZyEcvjEq8Bm8u7mrzggZ06qXTJVSsMMB4j2OsjG+rSp+uzeKWyP2Vu0K9Ye2wKtjFUJwrdw==} + engines: {node: '>10.17.0'} + hasBin: true + pg-numeric@1.0.2: resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} engines: {node: '>=4'} @@ -16452,6 +16718,10 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + raw-body@3.0.2: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} @@ -17176,6 +17446,10 @@ packages: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} + run-async@4.0.6: + resolution: {integrity: sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==} + engines: {node: '>=0.12.0'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -17185,6 +17459,9 @@ packages: rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -17651,6 +17928,10 @@ packages: sql-parser-cst@0.24.0: resolution: {integrity: sha512-38NNWamo0PKv80Zn8r1iXgatkWEB04CPhun8nEQhEJlYBMSIV4OOt4g3diynjVHJSunNSQqAjKcwmhM0EeLCtg==} + sql-template-strings@2.2.2: + resolution: {integrity: sha512-UXhXR2869FQaD+GMly8jAMCRZ94nU5KcrFetZfWEMd+LVVG6y0ExgHAhatEcKZ/wk8YcKPdi+hiD2wm75lq3/Q==} + engines: {node: '>=4.0.0'} + sqlstring@2.3.3: resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} engines: {node: '>= 0.6'} @@ -17846,6 +18127,14 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + stripe-experiment-sync@1.0.18: + resolution: {integrity: sha512-I1LGtpHghu/6uNDOGzsJt+bVZP5jnQyDIXlW9xq7YiZf5ik9MbKcUGMcq64g5A+dWSV2mhY65UgZIrI8VBDjOA==} + hasBin: true + + stripe@17.7.0: + resolution: {integrity: sha512-aT2BU9KkizY9SATf14WhhYVv2uOapBWX0OFWF4xvcj1mPaNotlSc2CsxpS4DS46ZueSppmCF5BX1sNYBtwBvfw==} + engines: {node: '>=12.*'} + strnum@1.1.2: resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} @@ -17919,6 +18208,10 @@ packages: engines: {node: '>=8'} hasBin: true + supabase-management-js@0.1.6: + resolution: {integrity: sha512-TDQg6Nh7e2DaLmMjqpJt7Wwh2Au5loVPH2gwKMkD89e3pb5F/zMQGWt22asEXme031jEmhjazixFH0lwMgbRJA==} + engines: {node: '>=18.0.0'} + supabase@2.58.5: resolution: {integrity: sha512-mYZSkUIePTdmwlHd26Pff8wpmjfre8gcuWzrc5QqhZgZvCXugVzAQQhcjaQisw5kusbPQWNIjUwcHYEKmejhPw==} engines: {npm: '>=8'} @@ -18818,6 +19111,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + uuid@10.0.0: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true @@ -19360,6 +19657,9 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yesql@7.0.0: + resolution: {integrity: sha512-sosfr7agy4ibLM7BvXBkM6BpBmKMGuBO8DUYQEuey+QqaqrgW+2bsSg6D050ocBYIz0PuHxUyehyzEztZTU4pw==} + yjs@13.6.27: resolution: {integrity: sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} @@ -19376,6 +19676,10 @@ packages: resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} engines: {node: '>=18'} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + yoctocolors@2.1.2: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} @@ -22356,6 +22660,25 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@inquirer/ansi@1.0.2': {} + + '@inquirer/checkbox@4.3.2(@types/node@22.13.14)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.13.14) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/confirm@5.1.21(@types/node@22.13.14)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/type': 3.0.10(@types/node@22.13.14) + optionalDependencies: + '@types/node': 22.13.14 + '@inquirer/confirm@5.1.8(@types/node@22.13.14)': dependencies: '@inquirer/core': 10.1.9(@types/node@22.13.14) @@ -22366,7 +22689,7 @@ snapshots: '@inquirer/core@10.1.9(@types/node@22.13.14)': dependencies: '@inquirer/figures': 1.0.11 - '@inquirer/type': 3.0.5(@types/node@22.13.14) + '@inquirer/type': 3.0.10(@types/node@22.13.14) ansi-escapes: 4.3.2 cli-width: 4.1.0 mute-stream: 2.0.0 @@ -22376,8 +22699,114 @@ snapshots: optionalDependencies: '@types/node': 22.13.14 + '@inquirer/core@10.3.2(@types/node@22.13.14)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.13.14) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/editor@4.2.23(@types/node@22.13.14)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/external-editor': 1.0.3(@types/node@22.13.14) + '@inquirer/type': 3.0.10(@types/node@22.13.14) + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/expand@4.0.23(@types/node@22.13.14)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/type': 3.0.10(@types/node@22.13.14) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/external-editor@1.0.3(@types/node@22.13.14)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.0 + optionalDependencies: + '@types/node': 22.13.14 + '@inquirer/figures@1.0.11': {} + '@inquirer/figures@1.0.15': {} + + '@inquirer/input@4.3.1(@types/node@22.13.14)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/type': 3.0.10(@types/node@22.13.14) + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/number@3.0.23(@types/node@22.13.14)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/type': 3.0.10(@types/node@22.13.14) + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/password@4.0.23(@types/node@22.13.14)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/type': 3.0.10(@types/node@22.13.14) + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/prompts@7.10.1(@types/node@22.13.14)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@22.13.14) + '@inquirer/confirm': 5.1.21(@types/node@22.13.14) + '@inquirer/editor': 4.2.23(@types/node@22.13.14) + '@inquirer/expand': 4.0.23(@types/node@22.13.14) + '@inquirer/input': 4.3.1(@types/node@22.13.14) + '@inquirer/number': 3.0.23(@types/node@22.13.14) + '@inquirer/password': 4.0.23(@types/node@22.13.14) + '@inquirer/rawlist': 4.1.11(@types/node@22.13.14) + '@inquirer/search': 3.2.2(@types/node@22.13.14) + '@inquirer/select': 4.4.2(@types/node@22.13.14) + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/rawlist@4.1.11(@types/node@22.13.14)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/type': 3.0.10(@types/node@22.13.14) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/search@3.2.2(@types/node@22.13.14)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.13.14) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/select@4.4.2(@types/node@22.13.14)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.13.14) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/type@3.0.10(@types/node@22.13.14)': + optionalDependencies: + '@types/node': 22.13.14 + '@inquirer/type@3.0.5(@types/node@22.13.14)': optionalDependencies: '@types/node': 22.13.14 @@ -22963,6 +23392,61 @@ snapshots: '@next/swc-win32-x64-msvc@16.0.10': optional: true + '@ngrok/ngrok-android-arm64@1.6.0': + optional: true + + '@ngrok/ngrok-darwin-arm64@1.6.0': + optional: true + + '@ngrok/ngrok-darwin-universal@1.6.0': + optional: true + + '@ngrok/ngrok-darwin-x64@1.6.0': + optional: true + + '@ngrok/ngrok-freebsd-x64@1.6.0': + optional: true + + '@ngrok/ngrok-linux-arm-gnueabihf@1.6.0': + optional: true + + '@ngrok/ngrok-linux-arm64-gnu@1.6.0': + optional: true + + '@ngrok/ngrok-linux-arm64-musl@1.6.0': + optional: true + + '@ngrok/ngrok-linux-x64-gnu@1.6.0': + optional: true + + '@ngrok/ngrok-linux-x64-musl@1.6.0': + optional: true + + '@ngrok/ngrok-win32-arm64-msvc@1.6.0': + optional: true + + '@ngrok/ngrok-win32-ia32-msvc@1.6.0': + optional: true + + '@ngrok/ngrok-win32-x64-msvc@1.6.0': + optional: true + + '@ngrok/ngrok@1.6.0': + optionalDependencies: + '@ngrok/ngrok-android-arm64': 1.6.0 + '@ngrok/ngrok-darwin-arm64': 1.6.0 + '@ngrok/ngrok-darwin-universal': 1.6.0 + '@ngrok/ngrok-darwin-x64': 1.6.0 + '@ngrok/ngrok-freebsd-x64': 1.6.0 + '@ngrok/ngrok-linux-arm-gnueabihf': 1.6.0 + '@ngrok/ngrok-linux-arm64-gnu': 1.6.0 + '@ngrok/ngrok-linux-arm64-musl': 1.6.0 + '@ngrok/ngrok-linux-x64-gnu': 1.6.0 + '@ngrok/ngrok-linux-x64-musl': 1.6.0 + '@ngrok/ngrok-win32-arm64-msvc': 1.6.0 + '@ngrok/ngrok-win32-ia32-msvc': 1.6.0 + '@ngrok/ngrok-win32-x64-msvc': 1.6.0 + '@noble/ciphers@1.3.0': {} '@noble/curves@1.9.7': @@ -29505,6 +29989,8 @@ snapshots: call-bound: 1.0.4 is-array-buffer: 3.0.5 + array-flatten@1.1.1: {} + array-includes@3.1.8: dependencies: call-bind: 1.0.8 @@ -29733,6 +30219,23 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + body-parser@1.20.4(supports-color@8.1.1): + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9(supports-color@8.1.1) + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + body-parser@2.2.1(supports-color@8.1.1): dependencies: bytes: 3.1.2 @@ -30006,6 +30509,8 @@ snapshots: chardet@0.7.0: {} + chardet@2.1.1: {} + charenc@0.0.2: {} check-error@2.1.1: {} @@ -30245,6 +30750,8 @@ snapshots: commander@11.1.0: {} + commander@12.1.0: {} + commander@14.0.1: {} commander@2.20.3: {} @@ -30380,6 +30887,8 @@ snapshots: cookie-es@2.0.0: {} + cookie-signature@1.0.7: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} @@ -31829,6 +32338,42 @@ snapshots: dependencies: express: 5.1.0(supports-color@8.1.1) + express@4.22.1(supports-color@8.1.1): + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.4(supports-color@8.1.1) + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9(supports-color@8.1.1) + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2(supports-color@8.1.1) + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0(supports-color@8.1.1) + serve-static: 1.16.2(supports-color@8.1.1) + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + express@5.1.0(supports-color@8.1.1): dependencies: accepts: 2.0.0 @@ -32079,6 +32624,18 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@1.3.2(supports-color@8.1.1): + dependencies: + debug: 2.6.9(supports-color@8.1.1) + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + finalhandler@2.1.0(supports-color@8.1.1): dependencies: debug: 4.4.3(supports-color@8.1.1) @@ -33185,6 +33742,18 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + inquirer@12.11.1(@types/node@22.13.14): + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/prompts': 7.10.1(@types/node@22.13.14) + '@inquirer/type': 3.0.10(@types/node@22.13.14) + mute-stream: 2.0.0 + run-async: 4.0.6 + rxjs: 7.8.2 + optionalDependencies: + '@types/node': 22.13.14 + inquirer@8.2.6: dependencies: ansi-escapes: 4.3.2 @@ -35951,7 +36520,7 @@ snapshots: number-flow@0.3.7: {} - nuqs@1.19.1(next@15.5.9(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)): + nuqs@1.19.1(next@15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)): dependencies: mitt: 3.0.1 next: 15.5.9(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) @@ -36289,6 +36858,8 @@ snapshots: dependencies: openapi-typescript-helpers: 0.0.15 + openapi-fetch@0.6.2: {} + openapi-sampler@1.6.1: dependencies: '@types/json-schema': 7.0.15 @@ -36582,6 +37153,8 @@ snapshots: lru-cache: 11.0.1 minipass: 7.1.2 + path-to-regexp@0.1.12: {} + path-to-regexp@6.3.0: {} path-to-regexp@8.1.0: {} @@ -36678,6 +37251,13 @@ snapshots: pg-minify@1.6.3: {} + pg-node-migrations@0.0.8: + dependencies: + pg: 8.16.3 + sql-template-strings: 2.2.2 + transitivePeerDependencies: + - pg-native + pg-numeric@1.0.2: {} pg-pool@3.10.1(pg@8.16.3): @@ -37325,6 +37905,13 @@ snapshots: range-parser@1.2.1: {} + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + raw-body@3.0.2: dependencies: bytes: 3.1.2 @@ -38327,6 +38914,8 @@ snapshots: run-async@2.4.1: {} + run-async@4.0.6: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -38337,6 +38926,10 @@ snapshots: dependencies: tslib: 2.8.1 + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + sade@1.8.1: dependencies: mri: 1.2.0 @@ -38975,6 +39568,8 @@ snapshots: sql-parser-cst@0.24.0: {} + sql-template-strings@2.2.2: {} + sqlstring@2.3.3: {} sse.js@2.2.0: {} @@ -39207,6 +39802,33 @@ snapshots: dependencies: js-tokens: 9.0.1 + stripe-experiment-sync@1.0.18(@types/node@22.13.14)(supports-color@8.1.1): + dependencies: + '@ngrok/ngrok': 1.6.0 + chalk: 5.4.1 + commander: 12.1.0 + dotenv: 16.5.0 + express: 4.22.1(supports-color@8.1.1) + inquirer: 12.11.1(@types/node@22.13.14) + papaparse: 5.4.1 + pg: 8.16.3 + pg-node-migrations: 0.0.8 + stripe: 17.7.0 + supabase-management-js: 0.1.6 + ws: 8.18.3 + yesql: 7.0.0 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - pg-native + - supports-color + - utf-8-validate + + stripe@17.7.0: + dependencies: + '@types/node': 22.13.14 + qs: 6.14.0 + strnum@1.1.2: {} strtok3@8.1.0: @@ -39295,6 +39917,10 @@ snapshots: pirates: 4.0.6 ts-interface-checker: 0.1.13 + supabase-management-js@0.1.6: + dependencies: + openapi-fetch: 0.6.2 + supabase@2.58.5(supports-color@8.1.1): dependencies: bin-links: 6.0.0 @@ -40239,6 +40865,8 @@ snapshots: util-deprecate@1.0.2: {} + utils-merge@1.0.1: {} + uuid@10.0.0: {} uuid@11.1.0: {} @@ -40938,6 +41566,8 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yesql@7.0.0: {} + yjs@13.6.27: dependencies: lib0: 0.2.108 @@ -40949,6 +41579,8 @@ snapshots: yoctocolors-cjs@2.1.2: {} + yoctocolors-cjs@2.1.3: {} + yoctocolors@2.1.2: {} yoga-wasm-web@0.3.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c4344ed67163e..423c8e85fb15d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -50,6 +50,7 @@ minimumReleaseAgeExclude: - supabase - iceberg-js - '@vitejs/plugin-rsc' + - stripe-experiment-sync # TODO(matlin) remove, temp just to unblock launch onlyBuiltDependencies: - supabase