Skip to main content
When a user clicks “Connect GitHub” in your dashboard, your backend calls client.connect.createLink(), redirects them to the returned connectUrl, and Corsair handles the rest. The same API works in both modes — hub or manual config on createCorsair picks the backend:
ConfigWhere connectUrl pointsWhat you build
hub: { ... }Corsair Hub hosted UIDelivery endpoint only
manual: { baseUrl, redirectUri }Your app’s connect pageConnect page + OAuth callback

Response shape

Every createLink call returns:
type ConnectLink = {
  connectUrl: string;   // redirect the user's browser here
  expiresAt?: string;   // ISO timestamp — always set in practice
};
Redirect to connectUrl. That is the entire client-side contract.

Hub mode (hosted connect UI)

Use this when you want Corsair Hub to handle the connect pages, OAuth redirects, and token delivery.
server.ts
export const corsair = createCorsair({
  plugins: [github(), slack()],
  database,
  kek,
  hub: {
    projectApiKey: process.env.CORSAIR_API_KEY!,
    signingSecret: process.env.CORSAIR_SIGNING_SECRET!,
    deliveryUrl: `${appUrl}/api/corsair`,
  },
});
Mount two routes:
app/api/corsair/route.ts
// Hub delivery — OAuth results land here
import { createHubRouteHandlers } from "corsair/hub";
import { corsair } from "@/server";

const hub = createHubRouteHandlers(corsair);
export const GET = hub.delivery;
export const POST = hub.delivery;
export const OPTIONS = hub.deliveryOptions;
app/api/corsair/[...path]/route.ts
// Management API — createLink and the rest
import { toNextJsHandler } from "corsair";
import { corsair } from "@/server";

export const { GET, POST } = toNextJsHandler(corsair, {
  basePath: "/api/corsair",
});
Create a connect link and redirect:
backend.ts
const { connectUrl } = await client.connect.createLink({
  plugin: "github",
  tenantId: "acme",
  oauthMode: "managed", // or "byo"
});
window.location.href = connectUrl;
Hub-specific input fields (ignored in manual mode):
FieldPurpose
pluginOptional — omit to show all configured plugins
oauthMode"byo" (your OAuth app) or "managed" (Corsair’s)
source"client" or "server" — inferred from deliveryUrl when omitted
Hub delivers tokens to your deliveryUrl. You do not call resolve or oauthCallback.

Manual mode (self-hosted)

Use this when you want full control over connect pages and OAuth callbacks.
server.ts
export const corsair = createCorsair({
  plugins: [github(), slack()],
  database,
  kek,
  manual: {
    baseUrl: "https://app.example.com/connect",
    redirectUri: "https://app.example.com/api/oauth/callback",
  },
});
Mount the management handler and build two pages:
  1. Connect page at manual.baseUrl — receives ?state=…, resolves to the provider OAuth URL
  2. OAuth callback at manual.redirectUri — receives ?code=…&state=…, exchanges for tokens
backend.ts
const { connectUrl } = await client.connect.createLink({
  plugin: "github",
  tenantId: "acme",
});
window.location.href = connectUrl;
In React:
connect-button.tsx
function ConnectGithub({ tenantId }: { tenantId: string }) {
  const { mutate, loading } = useCreateConnectLink();

  return (
    <button
      disabled={loading}
      onClick={async () => {
        const link = await mutate({ plugin: "github", tenantId });
        if (link) window.location.href = link.connectUrl;
      }}
    >
      Connect GitHub
    </button>
  );
}
The signed state is embedded in connectUrl as a query parameter — you do not need to handle it separately.

Step 2 — Resolve

The browser hits your connect page with ?state=…. Call resolve to get the provider OAuth URL:
const resolved = await client.connect.resolve(state);
// redirect to resolved.oauthUrl
Or use corsair.manage.connect.resolve(state) in-process.

Step 3 — OAuth callback

The provider redirects back with ?code=…&state=…:
app/api/oauth/callback/route.ts
import { corsair } from "@/server";

export async function GET(req: Request) {
  const url = new URL(req.url);
  const code = url.searchParams.get("code")!;
  const state = url.searchParams.get("state")!;

  await corsair.manage.connect.oauthCallback({ code, state });

  return Response.redirect("/dashboard?connected=1");
}
Corsair re-verifies the state, exchanges the code, encrypts tokens, and stores them.

Checking connection status

After a successful connect, useConnectionStatus({ tenantId }) reflects the new state:
status.tsx
const { data, refetch } = useConnectionStatus({ tenantId: "acme" });
// data: { github: 'connected', slack: 'not_connected', ... }
Call refetch() after connect completes to update the dashboard.

Errors

StatuserrorWhen
500connect_not_configuredNeither hub nor manual config was passed
500connect_misconfiguredInvalid manual.baseUrl, or missing connect URLs for manual connect
500database_not_configureddatabase and kek required to issue connect links
400missing_credentialsPlugin OAuth client id / secret not configured (manual mode BYO)
400hub_moderesolve or oauthCallback called when only hub is configured
500resolve_failedState invalid or expired (manual mode)
502oauth_callback_failedProvider rejected the code (manual mode)
All client errors surface as CorsairClientError with these code values.