Loading...
Search the Quiltt documentation
Loading...
Build a production-ready connector that uses multiple data aggregators (Finicity, MX, Plaid, Akoya) for better reliability and coverage.
Time: ~20 minutes Level: Intermediate Framework: Next.js
Single-aggregator approaches have limits:
Quiltt's unified API lets you use multiple aggregators while writing code once.
Quiltt supports four major aggregators, each with different strengths:
| Aggregator | Strengths |
|---|---|
| Finicity | Most OAuth connections |
| MX | Broadest coverage |
| Plaid | Strong fintech coverage |
| Akoya | OAuth-only coverage |
You don't need to pick one - Quiltt automatically chooses the best aggregator for each connection attempt based on the products you need, and your code stays the same.
Create a Next.js project and install the SDK:
pnpm create next-app my-quiltt-app --typescript --tailwind --app
cd my-quiltt-app
pnpm add @quiltt/react
Add credentials from the Dashboard:
# .env.local
NEXT_PUBLIC_QUILTT_CONNECTOR_ID=your_connector_id_here
QUILTT_API_KEY_SECRET=your_api_key_secret_here
Never commit QUILTT_API_KEY_SECRET to Git. It's for server-side use only.
Start the dev server:
pnpm dev
Configure your connector in the Quiltt Dashboard to enable the aggregators and products you need:
Connect section:
ACCOUNT_BALANCES_AND_TRANSACTIONS)That's all the setup required - authentication is handled server-side in the next step.
Session tokens authenticate your users for the Connector and GraphQL API. Issue them server-side so your API key secret never reaches the browser.
Create a route that exchanges your API key secret for a Session token:
// app/api/quiltt-session/route.ts
import { NextResponse } from 'next/server'
interface CurrentUser {
quilttProfileId: string
}
export async function POST() {
const user = await getCurrentUserFromYourAuthProvider()
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
if (!process.env.QUILTT_API_KEY_SECRET) {
return NextResponse.json({ error: 'Missing QUILTT_API_KEY_SECRET' }, { status: 500 })
}
const response = await fetch('https://auth.quiltt.io/v1/users/sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.QUILTT_API_KEY_SECRET}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ userId: user.quilttProfileId })
})
const session = await response.json()
return NextResponse.json(session, { status: response.status })
}
async function getCurrentUserFromYourAuthProvider(): Promise<CurrentUser | null> {
// Replace this with your app's server-side auth lookup.
return null
}
The Profile ID must come from the authenticated user, not from request body data sent by the browser.
Then wrap your app with QuilttAuthProvider, which manages the token and configures the GraphQL client:
// app/providers.tsx
'use client'
import { useEffect, useState } from 'react'
import type { PropsWithChildren } from 'react'
import { QuilttAuthProvider } from '@quiltt/react'
interface SessionResponse {
token: string
expiresAt: string
}
export function Providers({ profileId, children }: PropsWithChildren<{ profileId: string }>) {
const [token, setToken] = useState<string>()
const [error, setError] = useState<string>()
useEffect(() => {
const cacheKey = `quiltt_session_${profileId}`
// Reuse the cached token until it expires
const cached = localStorage.getItem(cacheKey)
if (cached) {
const session = JSON.parse(cached) as SessionResponse
if (new Date(session.expiresAt) > new Date()) {
setToken(session.token)
return
}
}
fetch('/api/quiltt-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
.then(async (res) => {
if (!res.ok) throw new Error('Unable to create Quiltt session')
return (await res.json()) as SessionResponse
})
.then((session) => {
if (!session.token || !session.expiresAt) throw new Error('Invalid Quiltt session')
localStorage.setItem(
cacheKey,
JSON.stringify({ token: session.token, expiresAt: session.expiresAt })
)
setToken(session.token)
})
.catch((err: Error) => setError(err.message))
}, [profileId])
if (error) return <div>{error}</div>
if (!token) return <div>Loading...</div>
return <QuilttAuthProvider token={token}>{children}</QuilttAuthProvider>
}
Critical: Session tokens are rate limited to 10/hour and 20/day per Profile. The localStorage check above is what stops every page refresh from issuing a new token - without it you'll hit 429 errors during development. See Issuing Session Tokens for expiration and revocation details.
Wire the provider into your root layout:
// app/layout.tsx
import type { PropsWithChildren } from 'react'
import { Providers } from './providers'
export default async function RootLayout({ children }: PropsWithChildren) {
const profileId = await getQuilttProfileIdForCurrentUser()
return (
<html lang="en">
<body>
<Providers profileId={profileId}>{children}</Providers>
</body>
</html>
)
}
async function getQuilttProfileIdForCurrentUser(): Promise<string> {
// Replace this with your app's server-side auth lookup.
throw new Error('Missing Profile ID lookup')
}
Add the buttons that open the Connector:
// app/page.tsx
'use client'
import { useRouter } from 'next/navigation'
import { QuilttButton } from '@quiltt/react'
import type { ConnectorSDKOnExitSuccessCallback } from '@quiltt/react'
export default function Home() {
const router = useRouter()
const connectorId = process.env.NEXT_PUBLIC_QUILTT_CONNECTOR_ID
if (!connectorId) return <div>Missing NEXT_PUBLIC_QUILTT_CONNECTOR_ID</div>
const handleSuccess: ConnectorSDKOnExitSuccessCallback = (metadata) => {
console.log('Connected:', metadata.connectionId)
router.push('/connections')
}
return (
<main className="mx-auto max-w-4xl space-y-4 p-6">
<h1 className="mb-4 font-bold text-2xl">Connect Your Financial Accounts</h1>
<QuilttButton
connectorId={connectorId}
onExitSuccess={handleSuccess}
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
Connect Account
</QuilttButton>
<QuilttButton
connectorId={connectorId}
institution="Chase"
onExitSuccess={handleSuccess}
className="rounded bg-green-600 px-4 py-2 text-white hover:bg-green-700"
>
Connect Chase Account
</QuilttButton>
</main>
)
}
The first button opens the full institution search. The second prefills that search with "Chase". On success, both send the user to the connections page you'll build in Step 8.
Visit http://localhost:3000 and click "Connect Account" to launch the Connector.
If your Connector is configured with an Exit Redirect, the Connector redirects the user instead of invoking callbacks - onExitSuccess will never fire. Leave Exit Redirect unset to use the callbacks shown here.
Fetch accounts with useQuery and keep balances current with useSubscription:
// app/components/AccountsList.tsx
'use client'
import { gql, useQuery, useSubscription } from '@quiltt/react'
const GET_ACCOUNTS = gql`
query GetAccounts($connectionId: ID!) {
connection(id: $connectionId) {
accounts {
id
name
balance {
current
}
kind
}
}
}
`
const CONNECTION_SYNCED = gql`
subscription OnConnectionSynced($connectionId: ID!) {
connectionSynced(connectionId: $connectionId) {
connection {
id
accounts {
id
name
balance {
current
}
kind
}
}
}
}
`
export default function AccountsList({ connectionId }: { connectionId: string }) {
const { data, loading, error } = useQuery(GET_ACCOUNTS, { variables: { connectionId } })
// Keeps displayed balances current as the connection syncs
useSubscription(CONNECTION_SYNCED, { variables: { connectionId } })
if (loading) return <div>Loading accounts...</div>
if (error) return <div className="text-red-600">Error loading accounts: {error.message}</div>
return (
<div className="space-y-4">
{data?.connection?.accounts.map((account) => (
<div key={account.id} className="rounded-lg bg-white p-4 shadow">
<h3 className="font-semibold">{account.name}</h3>
<p className="text-gray-600">${account.balance?.current?.toFixed(2) ?? 'N/A'}</p>
<p className="text-gray-500 text-sm">{account.kind}</p>
</div>
))}
</div>
)
}
Connections can break - password changes, institution outages, and aggregator issues all interrupt data. Show the current status and offer a repair flow.
First, map statuses to user-facing messages:
// app/lib/connection-status.ts
import type { ConnectionStatus } from '../types/generated/graphql'
interface StatusHandler {
message: string
severity: 'success' | 'info' | 'warning' | 'error'
action: 'repair' | 'reconnect' | null
}
export function handleConnectionStatus(status: ConnectionStatus): StatusHandler {
switch (status) {
case 'SYNCED':
return { message: 'Connected and up to date', severity: 'success', action: null }
case 'SYNCING':
case 'INITIALIZING':
case 'UPGRADING':
return { message: 'Updating connection...', severity: 'info', action: null }
case 'ERROR_REPAIRABLE':
return { message: 'Connection needs repair', severity: 'warning', action: 'repair' }
// Transient errors - Quiltt keeps retrying these on its own
case 'ERROR_INSTITUTION':
case 'ERROR_PROVIDER':
case 'ERROR_SERVICE':
return { message: 'Temporarily unable to sync', severity: 'warning', action: null }
case 'DISCONNECTED':
return { message: 'Connection disconnected', severity: 'error', action: 'reconnect' }
default:
return { message: 'Connection unavailable', severity: 'error', action: null }
}
}
Then render it with a repair button when needed:
// app/components/ConnectionStatusIndicator.tsx
'use client'
import { QuilttButton } from '@quiltt/react'
import { handleConnectionStatus } from '../lib/connection-status'
import type { ConnectionStatus } from '../types/generated/graphql'
const severityStyles: Record<ReturnType<typeof handleConnectionStatus>['severity'], string> = {
success: 'bg-green-100 text-green-800',
info: 'bg-blue-100 text-blue-800',
warning: 'bg-yellow-100 text-yellow-800',
error: 'bg-red-100 text-red-800'
}
export default function ConnectionStatusIndicator({
status,
connectionId
}: {
status: ConnectionStatus
connectionId: string
}) {
const { message, severity, action } = handleConnectionStatus(status)
const connectorId = process.env.NEXT_PUBLIC_QUILTT_CONNECTOR_ID
if (!connectorId) return <div>Missing NEXT_PUBLIC_QUILTT_CONNECTOR_ID</div>
return (
<div className="flex items-center gap-4">
<span className={`rounded-full px-3 py-1 text-sm ${severityStyles[severity]}`}>{message}</span>
{action === 'repair' && (
<QuilttButton
connectorId={connectorId}
connectionId={connectionId}
className="rounded bg-yellow-600 px-4 py-2 text-white hover:bg-yellow-700"
>
Repair
</QuilttButton>
)}
{action === 'reconnect' && (
<QuilttButton
connectorId={connectorId}
connectionId={connectionId}
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
Reconnect
</QuilttButton>
)}
</div>
)
}
ConnectionStatus is generated from the GraphQL schema. These examples expect it in app/types/generated/ - see the GraphQL Tooling Tutorial for code generation setup.
Finally, list all connections with their accounts and status:
// app/connections/page.tsx
'use client'
import { gql, useQuery } from '@quiltt/react'
import AccountsList from '../components/AccountsList'
import ConnectionStatusIndicator from '../components/ConnectionStatusIndicator'
import type { Connection } from '../types/generated/graphql'
const GET_CONNECTIONS = gql`
query GetConnections {
connections {
id
status
institution {
name
}
}
}
`
export default function ConnectionsPage() {
const { data, loading, error } = useQuery<{ connections: Array<Connection> }>(GET_CONNECTIONS)
if (loading)
return (
<main className="mx-auto max-w-4xl p-6">
<h1 className="mb-6 font-bold text-2xl">Your Connected Accounts</h1>
<div>Loading connections...</div>
</main>
)
if (error)
return (
<main className="mx-auto max-w-4xl p-6">
<h1 className="mb-6 font-bold text-2xl">Your Connected Accounts</h1>
<div className="text-red-600">Error loading connections: {error.message}</div>
</main>
)
return (
<main className="mx-auto max-w-4xl p-6">
<h1 className="mb-6 font-bold text-2xl">Your Connected Accounts</h1>
<div className="space-y-6">
{data?.connections.map((connection) => (
<div key={connection.id} className="rounded-lg bg-white p-6 shadow">
<div className="mb-4 flex items-start justify-between">
<h2 className="font-semibold text-lg">{connection.institution.name}</h2>
<ConnectionStatusIndicator status={connection.status} connectionId={connection.id} />
</div>
<AccountsList connectionId={connection.id} />
</div>
))}
</div>
</main>
)
}
You now have a multi-aggregator connector that:
Continue Learning:
Reference Documentation: