IntegrationsConnect

A pre-built React component that allows your users to connect their third-party accounts via OAuth. Drop it into your settings page or onboarding flow.

Important: This component requires a backend proxy to keep your API key secure. Never expose your secret API key (sk_...) in client-side code.

Installation

terminal
npm install @stack0/elements

Architecture

The IntegrationsConnect component needs to communicate with the Stack0 API securely. Since you shouldn't expose your API key in the browser, you'll create a backend proxy that the component calls instead.

BrowserYour API RouteStack0 API
Your API route adds the secret API key server-side

Step 1: Create API Proxy Route

Create an API route that proxies requests to Stack0 with your secret key:

app/api/integrations/[...path]/route.ts
// app/api/integrations/[...path]/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/lib/auth' // Your auth library
const STACK0_API_URL = 'https://api.stack0.dev/v1'
const STACK0_API_KEY = process.env.STACK0_API_KEY! // sk_...
export async function GET(
request: NextRequest,
{ params }: { params: { path: string[] } }
) {
// Verify user is authenticated
const session = await auth()
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const path = params.path.join('/')
const searchParams = request.nextUrl.searchParams.toString()
const url = `${STACK0_API_URL}/integrations/${path}${searchParams ? '?' + searchParams : ''}`
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${STACK0_API_KEY}`,
'Content-Type': 'application/json',
},
})
const data = await response.json()
return NextResponse.json(data, { status: response.status })
}
export async function POST(
request: NextRequest,
{ params }: { params: { path: string[] } }
) {
const session = await auth()
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const path = params.path.join('/')
const body = await request.json()
const response = await fetch(`${STACK0_API_URL}/integrations/${path}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${STACK0_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
...body,
endUserId: session.user.id, // Add user ID server-side
}),
})
const data = await response.json()
return NextResponse.json(data, { status: response.status })
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { path: string[] } }
) {
const session = await auth()
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const path = params.path.join('/')
const response = await fetch(`${STACK0_API_URL}/integrations/${path}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${STACK0_API_KEY}`,
},
})
if (response.status === 204) {
return new NextResponse(null, { status: 204 })
}
const data = await response.json()
return NextResponse.json(data, { status: response.status })
}

Step 2: Use the Component

Now use the IntegrationsConnect component pointing to your proxy:

settings-page.tsx
'use client'
import { IntegrationsConnect } from '@stack0/elements'
import { toast } from 'sonner'
export default function SettingsPage() {
return (
<div className="max-w-2xl mx-auto p-6">
<h1>Connected Integrations</h1>
<IntegrationsConnect
baseUrl="/api/integrations" // Points to your proxy
onConnected={(connection) => {
toast.success(`Connected to ${connection.connectorSlug}`)
}}
onDisconnected={(connectionId) => {
toast.info('Integration disconnected')
}}
onConnectionError={(error) => {
toast.error(`Connection failed: ${error.message}`)
}}
/>
</div>
)
}

Filter by Category

Show only specific categories of integrations:

filter-categories.tsx
// Show only CRM integrations
<IntegrationsConnect
baseUrl="/api/integrations"
category="crm"
/>
// Available categories: 'crm', 'storage', 'communication', 'productivity'

Filter by Connector

Show only specific connectors:

filter-connectors.tsx
// Show only HubSpot and Salesforce
<IntegrationsConnect
baseUrl="/api/integrations"
connectors={['hubspot', 'salesforce']}
/>
// Available connectors:
// CRM: hubspot, salesforce, pipedrive
// Storage: google-drive, dropbox, onedrive
// Communication: slack, discord
// Productivity: notion, google-sheets, airtable

Event Handlers

Handle connection lifecycle events:

event-handlers.tsx
<IntegrationsConnect
baseUrl="/api/integrations"
onConnected={(connection) => {
// Called when user successfully connects an account
console.log('New connection:', {
id: connection.id,
connector: connection.connectorSlug,
accountName: connection.externalAccountName,
})
// Refresh your data, show success message, etc.
toast.success(`Connected to ${connection.connectorSlug}`)
}}
onDisconnected={(connectionId) => {
// Called when user disconnects an account
console.log('Removed connection:', connectionId)
}}
onConnectionError={(error) => {
// Called if OAuth flow fails
console.error('Connection failed:', error.message)
toast.error('Failed to connect. Please try again.')
}}
/>

Custom Styling

Customize the appearance with variants and className:

custom-styling.tsx
// Different variants
<IntegrationsConnect
baseUrl="/api/integrations"
variant="default" // 'default' | 'minimal'
/>
// Custom className for container
<IntegrationsConnect
baseUrl="/api/integrations"
className="rounded-xl border shadow-lg"
/>
// Group by category or show flat list
<IntegrationsConnect
baseUrl="/api/integrations"
groupByCategory={true}
/>

With useIntegrations Hook

For more control, use the hook directly to build your own UI:

use-integrations.tsx
'use client'
import { useIntegrations } from '@stack0/elements'
export default function CustomIntegrationsUI() {
const {
connectors,
connections,
isLoading,
connect,
disconnect,
refresh,
} = useIntegrations({
baseUrl: '/api/integrations',
category: 'crm',
onConnected: (connection) => {
console.log('Connected:', connection)
},
})
if (isLoading) return <div>Loading...</div>
return (
<div>
<h2>Available Connectors</h2>
{connectors.map((connector) => {
const connection = connections.find(
(c) => c.connectorSlug === connector.slug
)
return (
<div key={connector.slug}>
<span>{connector.name}</span>
{connection ? (
<button onClick={() => disconnect(connection.id)}>
Disconnect
</button>
) : (
<button onClick={() => connect(connector.slug)}>
Connect
</button>
)}
</div>
)
})}
</div>
)
}

Props Reference

PropTypeDescription
baseUrlstringURL of your API proxy route (required)
categorystringFilter by category: crm, storage, communication, productivity
connectorsstring[]Filter by specific connector slugs
variant'default' | 'minimal'Visual style variant
groupByCategorybooleanGroup connectors by category (default: false)
showCategoryFilterbooleanShow category filter tabs (default: false)
classNamestringAdditional CSS classes
onConnected(connection) => voidCalled when connection succeeds
onDisconnected(id) => voidCalled when connection is removed
onConnectionError(error) => voidCalled if OAuth flow fails

Full Example

Complete settings page with integrations:

integrations-settings-page.tsx
'use client'
import { IntegrationsConnect } from '@stack0/elements'
import { toast } from 'sonner'
export default function IntegrationsSettingsPage() {
return (
<div className="max-w-3xl mx-auto py-8 px-4">
<div className="mb-8">
<h1 className="text-2xl font-bold">Integrations</h1>
<p className="text-muted-foreground">
Connect your accounts to sync data with our platform.
</p>
</div>
<IntegrationsConnect
baseUrl="/api/integrations"
category="crm"
groupByCategory={true}
showCategoryFilter={true}
onConnected={(connection) => {
toast.success(`Connected to ${connection.connectorSlug}`)
}}
onDisconnected={(id) => {
toast.info('Integration disconnected')
}}
onConnectionError={(error) => {
toast.error(`Connection failed: ${error.message}`)
}}
className="rounded-lg border bg-card"
/>
</div>
)
}