feat: new marketplace db (#44574)

This PR integrates with the new marketplace db to allow Grafana (and
other partners) OAuth apps to install from the integrations page. A demo
of this working locally is available here:
https://supabase.slack.com/archives/C01GN60J0BS/p1775551752479709. End
to end flow is documented here:
https://www.notion.so/supabase/Grafana-Integration-Flow-33a5004b775f80eeaf91c098beb8071f.

TODO:

- [ ] Make sure `NEXT_PUBLIC_MARKETPLACE_API_URL` variable is set to the
new marketplace db.
- [x] Test with the `marketplaceIntegrations` enabled and disabled in
staging once https://github.com/supabase/platform/pull/31298 is merged
and available in staging.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Add OAuth "Install integration" button that detects installed
integrations and supports GET/POST install flows
* Marketplace listings now include install links, installation method,
partner info, and listing assets/logos

* **Infrastructure**
* Allow marketplace API origin for images and content in security and
image config
* Centralize marketplace types and switch marketplace data source for
more reliable listings
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
Raminder SinghandJoshen Lim authored and GitHub committed 2026-04-16 17:57:12 +08:00
1 parent 1cb548b2dc
commit 0cb71a2497
15 files changed
+651 -553

No files matched your search

@@ -1,28 +1,43 @@
import { useState } from 'react'
import { cn } from 'ui'
import { cn, Dialog, DialogContent } from 'ui'
export const FilesViewer = ({ files }: { files: string[] }) => {
const [selected, setSelected] = useState(files[0])
const [showDialog, setShowDialog] = useState(false)
return (
<div className="flex flex-col gap-y-4">
<img alt={selected} src={selected} className="rounded-md border" />
{files.length > 1 && (
<div className="grid grid-cols-10 gap-x-2">
{files.map((x) => (
<button key={x} onClick={() => setSelected(x)}>
<img
alt={x}
src={x}
className={cn(
'col-span-1 bg-surface-100 rounded-md object-cover aspect-square border transition',
selected === x ? 'border-stronger' : 'border-secondary'
)}
/>
</button>
))}
</div>
)}
</div>
<>
<div className="flex flex-col gap-y-4">
<button onClick={() => setShowDialog(true)}>
<img
alt={selected}
src={selected}
className="rounded-md border object-cover aspect-video"
/>
</button>
{files.length > 1 && (
<div className="grid grid-cols-10 gap-x-2">
{files.map((x) => (
<button key={x} onClick={() => setSelected(x)}>
<img
alt={x}
src={x}
className={cn(
'col-span-1 bg-surface-100 rounded-md object-cover aspect-square border transition',
selected === x ? 'border-button-hover' : 'border-secondary'
)}
/>
</button>
))}
</div>
)}
</div>
<Dialog open={showDialog} onOpenChange={setShowDialog}>
<DialogContent size="xxlarge">
<img alt={selected} src={selected} className="rounded-md border" />
</DialogContent>
</Dialog>
</>
)
}
@@ -0,0 +1,81 @@
import { useParams } from 'common'
import { useMemo } from 'react'
import { toast } from 'sonner'
import { Button } from 'ui'
import type { IntegrationDefinition } from '@/components/interfaces/Integrations/Landing/Integrations.constants'
import { useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
import { useInstallOAuthIntegrationMutation } from '@/data/marketplace/install-oauth-integration-mutation'
interface InstallOAuthIntegrationButtonProps {
integration: IntegrationDefinition
}
export function InstallOAuthIntegrationButton({ integration }: InstallOAuthIntegrationButtonProps) {
const { ref: projectRef } = useParams()
const { data: apiKeys, isLoading: isApiKeysLoading } = useAPIKeysQuery(
{ projectRef, reveal: false },
{ enabled: !!projectRef }
)
const { mutate: installOAuthIntegration, isPending: isInstalling } =
useInstallOAuthIntegrationMutation({
onSuccess: (data) => {
if ('redirectUrl' in data) {
if (!data.redirectUrl) {
toast.error('Failed to redirect because redirect URL is invalid')
return
}
window.location.href = data.redirectUrl
} else {
toast.error('Failed to start integration installation')
}
},
})
const isLoading =
integration.installIdentificationMethod === 'secret_key_prefix' && isApiKeysLoading
const isIntegrationInstalled = useMemo(() => {
if (!integration) return false
const prefix = integration.secretKeyPrefix
if (integration.installIdentificationMethod !== 'secret_key_prefix' || !prefix) return false
if (isApiKeysLoading || !apiKeys) return false
return apiKeys.some((k) => k.type === 'secret' && k.name.startsWith(prefix))
}, [apiKeys, integration, isApiKeysLoading])
const handleInstallClick = async () => {
if (!integration || !projectRef) return
if (integration.installUrlType === 'post') {
if (!integration.listingId) return toast.error('Listing ID is required')
installOAuthIntegration({ projectRef, id: integration.listingId })
} else {
window.location.href = integration.installUrl ?? '/'
}
}
return (
<>
{isIntegrationInstalled ? (
<Button disabled type="outline" className="shrink-0">
Installed
</Button>
) : (
<Button
type="primary"
className="shrink-0"
loading={isInstalling || isLoading}
disabled={isLoading}
onClick={handleInstallClick}
>
Install integration
</Button>
)}
</>
)
}
@@ -46,6 +46,10 @@ type IntegrationStep = {
description?: string
}
type InstallUrlType = 'get' | 'post'
type InstallIdentificationMethod = 'secret_key_prefix'
/**
* [Joshen] For marketplace, we probably need to revisit this definition
* What properties are obsolete, what properties we need from remote source
@@ -97,6 +101,13 @@ export type IntegrationDefinition = {
inputs?: IntegrationInputs
/** Purely visual, just to show what are the changes on the project from installing the integration */
steps?: IntegrationStep[]
/** These are for OAuth Integrations */
installUrl?: string | null
installUrlType?: InstallUrlType
installIdentificationMethod?: InstallIdentificationMethod
secretKeyPrefix?: string
listingId?: string
} & (
| { type: 'wrapper'; meta: WrapperMeta }
| { type: 'postgres_extension' | 'custom' | 'oauth' | 'template' }
@@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query'
import { FeatureFlagContext, IS_PLATFORM, useFlag } from 'common'
import { Boxes } from 'lucide-react'
import dynamic from 'next/dynamic'
import Image from 'next/image'
import { useContext, useMemo } from 'react'
import { cn } from 'ui'
@@ -10,6 +11,11 @@ import { marketplaceIntegrationsQueryOptions } from '@/data/marketplace/integrat
import { useCLIReleaseVersionQuery } from '@/data/misc/cli-release-version-query'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
const fullImageUrl = (imagePath: string) => {
const API_URL = process.env.NEXT_PUBLIC_MARKETPLACE_API_URL || ''
return `${API_URL}${imagePath}`
}
/**
* [Joshen] Returns a combination of
* - Marketplace integrations retrieved remotely (Only if feature flag enabled)
@@ -33,69 +39,93 @@ export const useAvailableIntegrations = () => {
// [Joshen] Format marketplace integrations into existing ones for now
// Likely that we might need to change, but can look into separately
const marketplaceIntegrations: IntegrationDefinition[] = (data ?? [])?.map((integration) => {
const {
id,
type,
categories,
title: name,
summary: description,
documentation_url: docsUrl,
url: siteUrl,
content,
files,
} = integration
const marketplaceIntegrations: IntegrationDefinition[] = useMemo(
() =>
(data ?? [])?.map((integration) => {
const {
id: listingId,
slug,
categories,
title,
description,
documentation_url: docsUrl,
website_url: siteUrl,
installation_url: installUrl,
installation_url_type: installUrlType,
installation_identification_method: installMethod,
secret_key_prefix: secretKeyPrefix,
images,
content,
partner_name: authorName,
listing_logo: listingLogo,
} = integration
const status = undefined
const author = { name: '', websiteUrl: '' }
const status = undefined
const author = { name: authorName ?? '', websiteUrl: '' }
return {
id: id.toString(),
name,
status,
type,
categories: categories.map((x) => x.slug),
content,
files,
description,
docsUrl,
siteUrl,
author,
requiredExtensions: [],
icon: ({ className, ...props } = {}) => (
<Boxes className={cn('inset-0 p-2 text-black w-full h-full', className)} {...props} />
),
navigation: [
{
route: 'overview',
label: 'Overview',
},
],
navigate: ({ pageId = 'overview' }) => {
switch (pageId) {
case 'overview':
return dynamic(
() =>
import('@/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/index').then(
(mod) => mod.IntegrationOverviewTabV2
),
{
loading: Loading,
}
)
case 'secrets':
return dynamic(
() =>
import('../Vault/Secrets/SecretsManagement').then((mod) => mod.SecretsManagement),
{
loading: Loading,
}
)
return {
id: slug ?? '',
name: title ?? '',
status,
type: 'oauth' as const, // Currently marketplace only supports oauth apps
categories: Array.isArray(categories)
? (categories as Array<{ slug: string }>).map((x) => x.slug)
: [],
content,
files: images?.map((image) => fullImageUrl(image)),
description,
docsUrl,
siteUrl,
installUrl,
installUrlType: installUrlType ?? undefined,
installIdentificationMethod: installMethod ?? undefined,
secretKeyPrefix: secretKeyPrefix ?? undefined,
listingId: listingId ?? undefined,
author,
requiredExtensions: [],
icon: ({ className, ...props } = {}) => (
<div className="relative w-full h-full">
{listingLogo ? (
<Image
fill
src={fullImageUrl(listingLogo)}
alt=""
className={cn('p-2', className)}
{...props}
/>
) : (
<Boxes
className={cn('inset-0 p-2 text-black w-full h-full', className)}
{...props}
/>
)}
</div>
),
navigation: [
{
route: 'overview',
label: 'Overview',
},
],
navigate: ({ pageId = 'overview' }) => {
switch (pageId) {
case 'overview':
return dynamic(
() =>
import('@/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/index').then(
(mod) => mod.IntegrationOverviewTabV2
),
{
loading: Loading,
}
)
}
return null
},
}
return null
},
}
})
}),
[data]
)
// [Joshen] Existing integrations that are defined within studio
// Available integrations are all integrations that can be installed. If an integration can't be installed (needed
@@ -117,13 +147,14 @@ export const useAvailableIntegrations = () => {
})
}, [integrationsWrappers, isCLI])
const availableIntegrations = useMemo(
() => allIntegrations.sort((a, b) => a.name.localeCompare(b.name)),
[allIntegrations]
)
const dataWithMarketplace = useMemo(() => {
return [...marketplaceIntegrations, ...allIntegrations].sort((a, b) =>
a.name.localeCompare(b.name)
)
}, [marketplaceIntegrations, allIntegrations])
return {
data: [...marketplaceIntegrations, ...availableIntegrations],
data: dataWithMarketplace,
error,
isPending,
isSuccess,
@@ -85,8 +85,8 @@ const IntegrationCategoriesMenu = ({ page }: { page: string }) => {
items: [],
},
...categories.map((category) => ({
name: category.title,
key: category.slug,
name: category.name ?? '',
key: category.slug ?? '',
url: `/project/${ref}/integrations?category=${category.slug}`,
items: [],
})),
+6
View File
@@ -5,6 +5,10 @@ const SUPABASE_URL = process.env.SUPABASE_URL ? new URL(process.env.SUPABASE_URL
const GOTRUE_URL = process.env.NEXT_PUBLIC_GOTRUE_URL
? new URL(process.env.NEXT_PUBLIC_GOTRUE_URL).origin
: ''
const MARKETPLACE_API_URL = process.env.NEXT_PUBLIC_MARKETPLACE_API_URL
? new URL(process.env.NEXT_PUBLIC_MARKETPLACE_API_URL).origin
: ''
const SUPABASE_PROJECTS_URL = 'https://*.supabase.co https://*.storage.supabase.co'
const SUPABASE_PROJECTS_URL_WS = 'wss://*.supabase.co'
@@ -85,6 +89,7 @@ export function getCSP() {
API_URL,
SUPABASE_URL,
GOTRUE_URL,
MARKETPLACE_API_URL,
SUPABASE_LOCAL_PROJECTS_URL_WS,
SUPABASE_PROJECTS_URL,
SUPABASE_PROJECTS_URL_WS,
@@ -130,6 +135,7 @@ export function getCSP() {
USERCENTRICS_APP_URL,
STAPE_URL,
USERCENTRICS_URLS,
MARKETPLACE_API_URL,
...(!!NIMBUS_PROD_PROJECTS_URL ? [NIMBUS_PROD_PROJECTS_URL, NIMBUS_PROD_PROJECTS_URL_WS] : []),
]
const STYLE_SRC_URLS = [CLOUDFLARE_CDN_URL, SUPABASE_ASSETS_URL]
@@ -0,0 +1,52 @@
import { useMutation } from '@tanstack/react-query'
import { toast } from 'sonner'
import { handleError, post } from '@/data/fetchers'
import type { ResponseError, UseCustomMutationOptions } from '@/types'
export type OAuthIntegrationInstallVariables = {
projectRef: string
id: string
}
export async function installOAuthIntegration({
projectRef,
id,
}: OAuthIntegrationInstallVariables) {
const { data, error } = await post('/platform/integrations/partners/{ref}/{listing_id}', {
params: { path: { ref: projectRef, listing_id: id } },
})
if (error) handleError(error)
return data
}
type OAuthIntegrationInstallData = Awaited<ReturnType<typeof installOAuthIntegration>>
export const useInstallOAuthIntegrationMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseCustomMutationOptions<
OAuthIntegrationInstallData,
ResponseError,
OAuthIntegrationInstallVariables
>,
'mutationFn'
> = {}) => {
return useMutation<OAuthIntegrationInstallData, ResponseError, OAuthIntegrationInstallVariables>({
mutationFn: (vars) => installOAuthIntegration(vars),
async onSuccess(data, variables, context) {
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to start OAuth integration installation: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
})
}
@@ -1,12 +1,12 @@
import { queryOptions } from '@tanstack/react-query'
import { createMarketplaceClient } from 'common/marketplace-client'
import { marketplaceIntegrationsKeys } from './keys'
import { createMarketplaceClient } from './marketplace-client'
import { handleError } from '@/data/fetchers'
async function getMarketplaceCategories() {
const client = createMarketplaceClient()
const { data, error } = await client.from('categories').select('*')
const marketplaceClient = createMarketplaceClient()
const { data, error } = await marketplaceClient.from('categories').select('*')
if (error) handleError(error)
return data ?? []
@@ -1,14 +1,15 @@
import { queryOptions } from '@tanstack/react-query'
import { createMarketplaceClient } from 'common/marketplace-client'
import { marketplaceIntegrationsKeys } from './keys'
import { createMarketplaceClient } from './marketplace-client'
import { handleError } from '@/data/fetchers'
async function getMarketplaceIntegrations() {
const client = createMarketplaceClient()
const { data, error } = await client
.from('items')
.select('*, categories:category_items(...categories(slug, title))')
const marketplaceClient = createMarketplaceClient()
const { data, error } = await marketplaceClient
.from('listings')
.select('*')
.or('publish_location.eq.dashboard,publish_location.eq.both')
if (error) handleError(error)
return data ?? []
@@ -1,453 +0,0 @@
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
export type Database = {
// Allows to automatically instantiate createClient with right options
// instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY)
__InternalSupabase: {
PostgrestVersion: '14.4'
}
graphql_public: {
Tables: {
[_ in never]: never
}
Views: {
[_ in never]: never
}
Functions: {
graphql: {
Args: {
extensions?: Json
operationName?: string
query?: string
variables?: Json
}
Returns: Json
}
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
public: {
Tables: {
categories: {
Row: {
created_at: string
description: string | null
id: number
slug: string
title: string
updated_at: string
}
Insert: {
created_at?: string
description?: string | null
id?: never
slug: string
title: string
updated_at?: string
}
Update: {
created_at?: string
description?: string | null
id?: never
slug?: string
title?: string
updated_at?: string
}
Relationships: []
}
category_items: {
Row: {
category_id: number
created_at: string
item_id: number
}
Insert: {
category_id: number
created_at?: string
item_id: number
}
Update: {
category_id?: number
created_at?: string
item_id?: number
}
Relationships: [
{
foreignKeyName: 'category_items_category_id_fkey'
columns: ['category_id']
isOneToOne: false
referencedRelation: 'categories'
referencedColumns: ['id']
},
{
foreignKeyName: 'category_items_item_id_fkey'
columns: ['item_id']
isOneToOne: false
referencedRelation: 'items'
referencedColumns: ['id']
},
]
}
item_reviews: {
Row: {
created_at: string
featured: boolean
item_id: number
published_at: string | null
review_notes: string | null
reviewed_at: string | null
reviewed_by: string | null
status: Database['public']['Enums']['marketplace_review_status']
updated_at: string
}
Insert: {
created_at?: string
featured?: boolean
item_id: number
published_at?: string | null
review_notes?: string | null
reviewed_at?: string | null
reviewed_by?: string | null
status?: Database['public']['Enums']['marketplace_review_status']
updated_at?: string
}
Update: {
created_at?: string
featured?: boolean
item_id?: number
published_at?: string | null
review_notes?: string | null
reviewed_at?: string | null
reviewed_by?: string | null
status?: Database['public']['Enums']['marketplace_review_status']
updated_at?: string
}
Relationships: [
{
foreignKeyName: 'item_reviews_item_id_fkey'
columns: ['item_id']
isOneToOne: true
referencedRelation: 'items'
referencedColumns: ['id']
},
]
}
items: {
Row: {
content: string | null
created_at: string
documentation_url: string | null
files: string[]
id: number
partner_id: number
published: boolean
registry_item_url: string | null
slug: string
submitted_by: string | null
summary: string | null
title: string
type: Database['public']['Enums']['marketplace_item_type']
updated_at: string
url: string | null
}
Insert: {
content?: string | null
created_at?: string
documentation_url?: string | null
files?: string[]
id?: never
partner_id: number
published?: boolean
registry_item_url?: string | null
slug: string
submitted_by?: string | null
summary?: string | null
title: string
type: Database['public']['Enums']['marketplace_item_type']
updated_at?: string
url?: string | null
}
Update: {
content?: string | null
created_at?: string
documentation_url?: string | null
files?: string[]
id?: never
partner_id?: number
published?: boolean
registry_item_url?: string | null
slug?: string
submitted_by?: string | null
summary?: string | null
title?: string
type?: Database['public']['Enums']['marketplace_item_type']
updated_at?: string
url?: string | null
}
Relationships: [
{
foreignKeyName: 'items_partner_id_fkey'
columns: ['partner_id']
isOneToOne: false
referencedRelation: 'partners'
referencedColumns: ['id']
},
]
}
partner_members: {
Row: {
created_at: string
partner_id: number
role: string
user_id: string
}
Insert: {
created_at?: string
partner_id: number
role?: string
user_id: string
}
Update: {
created_at?: string
partner_id?: number
role?: string
user_id?: string
}
Relationships: [
{
foreignKeyName: 'partner_members_partner_id_fkey'
columns: ['partner_id']
isOneToOne: false
referencedRelation: 'partners'
referencedColumns: ['id']
},
]
}
partners: {
Row: {
created_at: string
created_by: string | null
description: string | null
id: number
logo_url: string | null
role: Database['public']['Enums']['marketplace_partner_role']
slug: string
title: string
updated_at: string
website: string | null
}
Insert: {
created_at?: string
created_by?: string | null
description?: string | null
id?: never
logo_url?: string | null
role?: Database['public']['Enums']['marketplace_partner_role']
slug: string
title: string
updated_at?: string
website?: string | null
}
Update: {
created_at?: string
created_by?: string | null
description?: string | null
id?: never
logo_url?: string | null
role?: Database['public']['Enums']['marketplace_partner_role']
slug?: string
title?: string
updated_at?: string
website?: string | null
}
Relationships: []
}
}
Views: {
[_ in never]: never
}
Functions: {
add_partner_member: {
Args: {
target_email: string
target_partner_id: number
target_role?: string
}
Returns: {
created_at: string
partner_id: number
role: string
user_id: string
}
SetofOptions: {
from: '*'
to: 'partner_members'
isOneToOne: true
isSetofReturn: false
}
}
before_user_created_hook: { Args: { event: Json }; Returns: Json }
is_admin_member: { Args: never; Returns: boolean }
is_partner_admin: {
Args: { target_partner_id: number }
Returns: boolean
}
is_partner_member: {
Args: { target_partner_id: number }
Returns: boolean
}
is_review_manager_member: { Args: never; Returns: boolean }
is_reviewer_member: { Args: never; Returns: boolean }
item_latest_review_is_approved: {
Args: { target_item_id: number }
Returns: boolean
}
storage_object_item_id: { Args: { object_name: string }; Returns: number }
storage_object_partner_id: {
Args: { object_name: string }
Returns: number
}
}
Enums: {
marketplace_item_type: 'oauth' | 'template'
marketplace_partner_role: 'partner' | 'reviewer' | 'admin'
marketplace_review_status: 'draft' | 'pending_review' | 'approved' | 'rejected'
}
CompositeTypes: {
[_ in never]: never
}
}
}
type DatabaseWithoutInternals = Omit<Database, '__InternalSupabase'>
type DefaultSchema = DatabaseWithoutInternals[Extract<keyof Database, 'public'>]
export type Tables<
DefaultSchemaTableNameOrOptions extends
| keyof (DefaultSchema['Tables'] & DefaultSchema['Views'])
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Views'])
: never = never,
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Views'])[TableName] extends {
Row: infer R
}
? R
: never
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema['Tables'] & DefaultSchema['Views'])
? (DefaultSchema['Tables'] & DefaultSchema['Views'])[DefaultSchemaTableNameOrOptions] extends {
Row: infer R
}
? R
: never
: never
export type TablesInsert<
DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema['Tables']
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables']
: never = never,
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
Insert: infer I
}
? I
: never
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables']
? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends {
Insert: infer I
}
? I
: never
: never
export type TablesUpdate<
DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema['Tables']
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables']
: never = never,
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
Update: infer U
}
? U
: never
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables']
? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends {
Update: infer U
}
? U
: never
: never
export type Enums<
DefaultSchemaEnumNameOrOptions extends
| keyof DefaultSchema['Enums']
| { schema: keyof DatabaseWithoutInternals },
EnumName extends DefaultSchemaEnumNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions['schema']]['Enums']
: never = never,
> = DefaultSchemaEnumNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions['schema']]['Enums'][EnumName]
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums']
? DefaultSchema['Enums'][DefaultSchemaEnumNameOrOptions]
: never
export type CompositeTypes<
PublicCompositeTypeNameOrOptions extends
| keyof DefaultSchema['CompositeTypes']
| { schema: keyof DatabaseWithoutInternals },
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes']
: never = never,
> = PublicCompositeTypeNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName]
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema['CompositeTypes']
? DefaultSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions]
: never
export const Constants = {
graphql_public: {
Enums: {},
},
public: {
Enums: {
marketplace_item_type: ['oauth', 'template'],
marketplace_partner_role: ['partner', 'reviewer', 'admin'],
marketplace_review_status: ['draft', 'pending_review', 'approved', 'rejected'],
},
},
} as const
+21
View File
@@ -29,6 +29,17 @@ function getAssetPrefix() {
return `${SUPABASE_ASSETS_URL}/${process.env.SITE_NAME}/${process.env.VERCEL_GIT_COMMIT_SHA?.substring(0, 12) ?? 'unknown'}`
}
const marketplaceApiUrl = process.env.NEXT_PUBLIC_MARKETPLACE_API_URL
? new URL(process.env.NEXT_PUBLIC_MARKETPLACE_API_URL)
: null
const marketplaceApiProtocol: 'http' | 'https' | null =
marketplaceApiUrl?.protocol === 'https:'
? 'https'
: marketplaceApiUrl?.protocol === 'http:'
? 'http'
: null
// Use `satisfies` instead of `: NextConfig` so TypeScript preserves narrow
// inferred types (e.g. async headers → Promise). This avoids TS2345 when
// wrapper functions (bundle-analyzer, sentry) resolve their `next` peer
@@ -576,6 +587,16 @@ const nextConfig = {
port: '',
pathname: '**',
},
...(marketplaceApiUrl
? [
{
...(marketplaceApiProtocol ? { protocol: marketplaceApiProtocol } : {}),
hostname: marketplaceApiUrl.hostname,
port: marketplaceApiUrl.port,
pathname: '**',
},
]
: []),
],
},
transpilePackages: [
@@ -29,6 +29,7 @@ import {
import ShimmeringLoader, { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { InstallIntegrationSheet } from '@/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/InstallIntegrationSheet/InstallIntegrationSheet'
import { InstallOAuthIntegrationButton } from '@/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/InstallIntegrationSheet/InstallOAuthIntegrationButton'
import { useAvailableIntegrations } from '@/components/interfaces/Integrations/Landing/useAvailableIntegrations'
import { useInstalledIntegrations } from '@/components/interfaces/Integrations/Landing/useInstalledIntegrations'
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
@@ -208,11 +209,7 @@ const IntegrationPage: NextPageWithLayout = () => {
</PageHeaderSummary>
{integration?.type === 'oauth' ? (
<Button asChild type="primary" className="shrink-0">
<a target="_blank" rel="noreferrer" href={integration.siteUrl ?? '/'}>
Install integration
</a>
</Button>
<InstallOAuthIntegrationButton integration={integration} />
) : isMarketplaceEnabled && !!integration && !isInstalled ? (
<InstallIntegrationSheet integration={integration} />
) : isMarketplaceEnabled && isInstalled ? (
+93
View File
@@ -670,6 +670,23 @@ export interface paths {
patch?: never
trace?: never
}
'/platform/integrations/partners/{ref}/{listing_id}': {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
get?: never
put?: never
/** Creates a partner integration and returns the redirect URL */
post: operations['PartnerIntegrationsController_createIntegration']
delete?: never
options?: never
head?: never
patch?: never
trace?: never
}
'/platform/integrations/private-link/{slug}': {
parameters: {
query?: never
@@ -7918,6 +7935,23 @@ export interface components {
organization_id: number
overdue_invoice_count: number
}
PartnerIntegrationsResponse: {
/**
* Format: date-time
* @description When the integration link expires (1 hour from creation). The user must begin the flow before this time.
*/
expiresAt: string
/**
* Format: uuid
* @description Unique identifier for the integration record
*/
integrationId: string
/**
* Format: uri
* @description URL to redirect the user's browser to
*/
redirectUrl: string
}
PauseStatusResponse: {
can_restore: boolean
last_paused_on: string | null
@@ -13210,6 +13244,65 @@ export interface operations {
}
}
}
PartnerIntegrationsController_createIntegration: {
parameters: {
query?: never
header?: never
path: {
/** @description The id of the listing in the marketplace database */
listing_id: string
/** @description Supabase project ref */
ref: string
}
cookie?: never
}
requestBody?: never
responses: {
201: {
headers: {
[name: string]: unknown
}
content: {
'application/json': components['schemas']['PartnerIntegrationsResponse']
}
}
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown
}
content?: never
}
/** @description Forbidden action */
403: {
headers: {
[name: string]: unknown
}
content?: never
}
/** @description Project or listing not found */
404: {
headers: {
[name: string]: unknown
}
content?: never
}
/** @description Rate limit exceeded */
429: {
headers: {
[name: string]: unknown
}
content?: never
}
/** @description Failed to retrieve listing or project, or failed to create partner integration */
500: {
headers: {
[name: string]: unknown
}
content?: never
}
}
}
PrivateLinkController_getPrivateLinkConfig: {
parameters: {
query?: never
+243
View File
@@ -0,0 +1,243 @@
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
export type Database = {
public: {
Tables: {
v_secret_id: {
Row: {
create_secret: string | null
}
Insert: {
create_secret?: string | null
}
Update: {
create_secret?: string | null
}
Relationships: []
}
}
Views: {
categories: {
Row: {
description: string | null
id: string | null
name: string | null
slug: string | null
}
Insert: {
description?: string | null
id?: string | null
name?: string | null
slug?: string | null
}
Update: {
description?: string | null
id?: string | null
name?: string | null
slug?: string | null
}
Relationships: []
}
listings: {
Row: {
aud: string | null
categories: Json | null
content: string | null
description: string | null
documentation_url: string | null
featured: boolean | null
id: string | null
images: string[] | null
installation_identification_method: 'secret_key_prefix' | null
installation_url: string | null
installation_url_type: 'get' | 'post' | null
listing_logo: string | null
marketplace_url: string | null
partner_logo: string | null
partner_name: string | null
partner_slug: string | null
publish_location: 'marketplace' | 'dashboard' | 'both' | null
secret_key_prefix: string | null
slug: string | null
title: string | null
website_url: string | null
youtube_id: string | null
}
Relationships: []
}
partners: {
Row: {
country: string | null
description: string | null
id: string | null
logo: string | null
name: string | null
num_of_employees: number | null
slug: string | null
type: 'technology' | 'expert' | null
website: string | null
}
Insert: {
country?: string | null
description?: string | null
id?: string | null
logo?: never
name?: string | null
num_of_employees?: number | null
slug?: string | null
type?: 'technology' | 'expert' | null
website?: string | null
}
Update: {
country?: string | null
description?: string | null
id?: string | null
logo?: never
name?: string | null
num_of_employees?: number | null
slug?: string | null
type?: 'technology' | 'expert' | null
website?: string | null
}
Relationships: []
}
}
Functions: {
get_redirect_url: {
Args: {
p_listing_id: string
p_organization_slug: string
p_project_id: string
}
Returns: Json
}
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
}
type DatabaseWithoutInternals = Omit<Database, '__InternalSupabase'>
type DefaultSchema = DatabaseWithoutInternals[Extract<keyof Database, 'public'>]
export type Tables<
DefaultSchemaTableNameOrOptions extends
| keyof (DefaultSchema['Tables'] & DefaultSchema['Views'])
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Views'])
: never = never,
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Views'])[TableName] extends {
Row: infer R
}
? R
: never
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema['Tables'] & DefaultSchema['Views'])
? (DefaultSchema['Tables'] & DefaultSchema['Views'])[DefaultSchemaTableNameOrOptions] extends {
Row: infer R
}
? R
: never
: never
export type TablesInsert<
DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema['Tables']
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables']
: never = never,
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
Insert: infer I
}
? I
: never
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables']
? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends {
Insert: infer I
}
? I
: never
: never
export type TablesUpdate<
DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema['Tables']
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables']
: never = never,
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
Update: infer U
}
? U
: never
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables']
? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends {
Update: infer U
}
? U
: never
: never
export type Enums<
DefaultSchemaEnumNameOrOptions extends
| keyof DefaultSchema['Enums']
| { schema: keyof DatabaseWithoutInternals },
EnumName extends DefaultSchemaEnumNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions['schema']]['Enums']
: never = never,
> = DefaultSchemaEnumNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions['schema']]['Enums'][EnumName]
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums']
? DefaultSchema['Enums'][DefaultSchemaEnumNameOrOptions]
: never
export type CompositeTypes<
PublicCompositeTypeNameOrOptions extends
| keyof DefaultSchema['CompositeTypes']
| { schema: keyof DatabaseWithoutInternals },
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes']
: never = never,
> = PublicCompositeTypeNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName]
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema['CompositeTypes']
? DefaultSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions]
: never
export const Constants = {
public: {
Enums: {},
},
} as const