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.
sk_...) in client-side code.Installation
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.
Step 1: Create API Proxy Route
Create an API route that proxies requests to Stack0 with your secret key:
// app/api/integrations/[...path]/route.tsimport { NextRequest, NextResponse } from 'next/server'import { auth } from '@/lib/auth' // Your auth libraryconst 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 authenticatedconst 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:
'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><IntegrationsConnectbaseUrl="/api/integrations" // Points to your proxyonConnected={(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:
// Show only CRM integrations<IntegrationsConnectbaseUrl="/api/integrations"category="crm"/>// Available categories: 'crm', 'storage', 'communication', 'productivity'
Filter by Connector
Show only specific connectors:
// Show only HubSpot and Salesforce<IntegrationsConnectbaseUrl="/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:
<IntegrationsConnectbaseUrl="/api/integrations"onConnected={(connection) => {// Called when user successfully connects an accountconsole.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 accountconsole.log('Removed connection:', connectionId)}}onConnectionError={(error) => {// Called if OAuth flow failsconsole.error('Connection failed:', error.message)toast.error('Failed to connect. Please try again.')}}/>
Custom Styling
Customize the appearance with variants and className:
// Different variants<IntegrationsConnectbaseUrl="/api/integrations"variant="default" // 'default' | 'minimal'/>// Custom className for container<IntegrationsConnectbaseUrl="/api/integrations"className="rounded-xl border shadow-lg"/>// Group by category or show flat list<IntegrationsConnectbaseUrl="/api/integrations"groupByCategory={true}/>
With useIntegrations Hook
For more control, use the hook directly to build your own UI:
'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
| Prop | Type | Description |
|---|---|---|
| baseUrl | string | URL of your API proxy route (required) |
| category | string | Filter by category: crm, storage, communication, productivity |
| connectors | string[] | Filter by specific connector slugs |
| variant | 'default' | 'minimal' | Visual style variant |
| groupByCategory | boolean | Group connectors by category (default: false) |
| showCategoryFilter | boolean | Show category filter tabs (default: false) |
| className | string | Additional CSS classes |
| onConnected | (connection) => void | Called when connection succeeds |
| onDisconnected | (id) => void | Called when connection is removed |
| onConnectionError | (error) => void | Called if OAuth flow fails |
Full Example
Complete settings page with integrations:
'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><IntegrationsConnectbaseUrl="/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>)}