Build an App Connections Dashboard
Build a Chat App handles authentication in-chat. That works for getting started, but production apps need a dedicated page where users manage their connections. This cookbook builds a dashboard where users can see all available apps, connect via OAuth, and disconnect at any time.
What you'll build
- A connections dashboard showing all available apps and their auth status
- Connect and disconnect buttons that handle OAuth flows
Prerequisites
- Bun (or Node.js 18+)
- Composio API key
Stack: Next.js, @composio/core
Create the project
bunx create-next-app composio-dashboard --yes
cd composio-dashboard
bun add @composio/coreAdd your API key to a .env.local file:
COMPOSIO_API_KEY=your_composio_api_keySetting up the client
Create app/api/connections/route.ts. Initialize the Composio client and set dynamic = "force-dynamic" so Next.js always fetches fresh connection data.
import { Composio } from "@composio/core";
const composio = new Composio();
export const dynamic = "force-dynamic";"user_123" is a placeholder. In production, replace it with the authenticated user's ID from your auth system. See Users & Sessions for details.
List connections
The GET handler creates a session and returns every toolkit's name, logo, and connection status. Toolkits where isNoAuth is true don't require authentication, so we filter them out.
export async function GET() {
const session = await composio.create("user_123");
const { items } = await session.toolkits({ limit: 50 });
return Response.json({
// Filter out toolkits that don't require authentication
toolkits: items
.filter((t) => !t.isNoAuth)
.map((t) => ({
slug: t.slug,
name: t.name,
logo: t.logo,
isConnected: t.connection?.isActive ?? false,
connectedAccountId: t.connection?.connectedAccount?.id,
})),
});
}session.toolkits() returns each toolkit with a connection object. When connection.isActive is true, the user has already authorized that app. We also return the connectedAccountId so the frontend can disconnect later.
session.toolkits() paginates results. This example fetches up to 50. Use the nextCursor value from the response to fetch the next page.
Connect an app
The POST handler starts an OAuth flow for a given toolkit and returns the redirect URL.
export async function POST(req: Request) {
const { toolkit }: { toolkit: string } = await req.json();
const origin = new URL(req.url).origin;
const session = await composio.create("user_123");
const connectionRequest = await session.authorize(toolkit, {
callbackUrl: origin,
});
return Response.json({ redirectUrl: connectionRequest.redirectUrl });
}session.authorize(toolkit) creates a connection request with a callbackUrl. After the user completes OAuth, they get redirected back to your app.
Disconnect an app
Create app/api/connections/disconnect/route.ts. This endpoint takes a connectedAccountId and deletes it:
import { Composio } from "@composio/core";
const composio = new Composio();
export async function POST(req: Request) {
const { connectedAccountId }: { connectedAccountId: string } =
await req.json();
await composio.connectedAccounts.delete(connectedAccountId);
return Response.json({ success: true });
}composio.connectedAccounts.delete() is a standalone SDK method. No session or provider needed.
Build the dashboard
Replace app/page.tsx with a dashboard that shows connection cards:
"use client";
import { useEffect, useState } from "react";
type Toolkit = {
slug: string;
name: string;
logo?: string;
isConnected: boolean;
connectedAccountId?: string;
};
export default function Dashboard() {
const [toolkits, setToolkits] = useState<Toolkit[]>([]);
async function fetchConnections() {
const res = await fetch("/api/connections", { cache: "no-store" });
const data = await res.json();
setToolkits(data.toolkits);
}
useEffect(() => {
fetchConnections();
}, []);
async function connect(slug: string) {
const res = await fetch("/api/connections", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ toolkit: slug }),
});
const { redirectUrl } = await res.json();
window.location.href = redirectUrl;
}
async function disconnect(connectedAccountId: string) {
await fetch("/api/connections/disconnect", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ connectedAccountId }),
});
fetchConnections();
}
return (
<main className="max-w-4xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-2">App Connections</h1>
<p className="text-gray-500 mb-6">
Connect your apps to give your agent access.
</p>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{toolkits.map((t) => (
<div
key={t.slug}
className="flex items-center justify-between p-4 border rounded-lg"
>
<div className="flex items-center gap-3">
{t.logo && (
<img src={t.logo} alt={t.name} className="w-8 h-8 rounded" />
)}
<div>
<p className="font-medium">{t.name}</p>
<p
className={`text-xs ${
t.isConnected ? "text-green-600" : "text-gray-400"
}`}
>
{t.isConnected ? "Connected" : "Not connected"}
</p>
</div>
</div>
{t.isConnected ? (
<button
onClick={() => disconnect(t.connectedAccountId!)}
className="px-3 py-1.5 text-sm border rounded hover:bg-gray-50"
>
Disconnect
</button>
) : (
<button
onClick={() => connect(t.slug)}
className="px-3 py-1.5 text-sm bg-black text-white rounded hover:bg-gray-800"
>
Connect
</button>
)}
</div>
))}
</div>
</main>
);
}The page fetches toolkit connection status on load. Clicking Connect redirects the user to OAuth. After they authorize, the callbackUrl brings them back to the dashboard with the connection now active. Clicking Disconnect deletes the connection and refreshes the list.
Complete route
The full app/api/connections/route.ts with both handlers:
// region setup
import { Composio } from "@composio/core";
const composio = new Composio();
export const dynamic = "force-dynamic";
// endregion setup
// region list
export async function GET() {
const session = await composio.create("user_123");
const { items } = await session.toolkits({ limit: 50 });
return Response.json({
// Filter out toolkits that don't require authentication
toolkits: items
.filter((t) => !t.isNoAuth)
.map((t) => ({
slug: t.slug,
name: t.name,
logo: t.logo,
isConnected: t.connection?.isActive ?? false,
connectedAccountId: t.connection?.connectedAccount?.id,
})),
});
}
// endregion list
// region connect
export async function POST(req: Request) {
const { toolkit }: { toolkit: string } = await req.json();
const origin = new URL(req.url).origin;
const session = await composio.create("user_123");
const connectionRequest = await session.authorize(toolkit, {
callbackUrl: origin,
});
return Response.json({ redirectUrl: connectionRequest.redirectUrl });
}
// endregion connectRunning the app
bun devOpen localhost:3000. You'll see all available apps with their connection status.
- Click Connect on GitHub. You'll be redirected to GitHub's OAuth page.
- Authorize the app. You'll land back on the dashboard with GitHub showing "Connected."
- Click Disconnect on GitHub. The connection is removed immediately.