> ## Documentation Index
> Fetch the complete documentation index at: https://docs.corsair.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Next.js

> Mount Corsair as one catch-all route, read synced data in Server Components, and mutate with Server Actions.

In the App Router, Corsair fits the model you already use. One file under `app/api` is the whole server surface, Server Components read data at render time, and Server Actions handle the writes. Nothing runs on the client that doesn't have to.

## Install

<CodeGroup>
  ```bash npm theme={null}
  npm i corsair @corsair-dev/github
  ```

  ```bash pnpm theme={null}
  pnpm add corsair @corsair-dev/github
  ```

  ```bash yarn theme={null}
  yarn add corsair @corsair-dev/github
  ```

  ```bash bun theme={null}
  bun add corsair @corsair-dev/github
  ```
</CodeGroup>

The adapters ship in core `corsair`; add one `@corsair-dev/*` package per service you connect. The full list is in [Plugins](/guides/plugins).

## Create the instance

Build the instance once. The handler and your read/write code both import it.

```ts corsair.ts theme={null}
import Database from 'better-sqlite3';
import { createCorsair } from 'corsair';
import { github } from '@corsair-dev/github';

export const corsair = createCorsair({
    database: new Database('corsair.db'),
    kek: process.env.CORSAIR_KEK!,
    multiTenancy: true,
    plugins: [github({ authType: 'managed' })], // one entry per @corsair-dev/* package you install
});
```

The handler and every `.db` / `.api` call on this page import this `corsair`. `multiTenancy: true` is what makes `corsair.withTenant(id)` available, and a `tenant.github.*` call resolves to the `github()` entry here. Add a service by installing its `@corsair-dev/*` package and dropping it into `plugins`. Database choices and the Hub keys are in [Quick Start](/quick-start).

## Project layout

```
app/
  api/corsair/[[...path]]/route.ts   # the handler
  issues/page.tsx                    # Server Component reads .db
  issues/actions.ts                  # Server Action writes .api
  corsair-client.ts                  # "use client" hook client
server/corsair.ts                    # createCorsair instance
```

## Mount the handler

The management API, OAuth callbacks, and Hub's connect delivery all arrive on the same path, so a single catch-all route handler serves them. Mount it once and you never edit it again.

```ts app/api/corsair/[[...path]]/route.ts theme={null}
import { toNextJsHandler } from 'corsair';
import { corsair } from '@/server/corsair';

export const { GET, POST, OPTIONS } = toNextJsHandler(corsair, {
    basePath: '/api/corsair',
});
```

`@/server/corsair` is your `createCorsair({ ... })` instance. The optional-catch-all segment `[[...path]]` matches `/api/corsair` and everything under it.

## Resolve the tenant

`withTenant` takes any stable string that identifies the current user or org. You produce it from the auth you already run; Corsair never supplies one. Read it at the top of the component and scope from there.

```tsx theme={null}
import { corsair } from '@/server/corsair';
import { getTenantId } from '@/server/tenant'; // YOUR code — reads the signed-in user from your session

const tenant = corsair.withTenant(await getTenantId());
```

## Read and write

A Server Component reads from `.db` with no network, so the page renders as fast as any database query. Because it's already on the server, you call the tenant directly instead of round-tripping through the route.

```tsx app/issues/page.tsx theme={null}
import { corsair } from '@/server/corsair';
import { getTenantId } from '@/server/tenant';
import { closeIssue } from './actions';

export default async function IssuesPage() {
    const tenant = corsair.withTenant(await getTenantId());
    const issues = await tenant.github.db.issues.search({
        data: { state: 'open' },
        limit: 50,
    });

    return (
        <ul>
            {issues.map((issue) => (
                <li key={issue.id}>
                    {issue.data.title}
                    <form action={closeIssue.bind(null, issue.data.number)}>
                        <button>Close</button>
                    </form>
                </li>
            ))}
        </ul>
    );
}
```

The Server Action writes through `.api`:

```ts app/issues/actions.ts theme={null}
'use server';
import { revalidatePath } from 'next/cache';
import { corsair } from '@/server/corsair';
import { getTenantId } from '@/server/tenant';

export async function closeIssue(issueNumber: number) {
    const tenant = corsair.withTenant(await getTenantId());
    await tenant.github.api.issues.update({
        owner: 'acme',
        repo: 'app',
        issueNumber,
        state: 'closed',
    });
    revalidatePath('/issues');
}
```

## Refresh after a write

The `.api` call above upserts the response into your database, so `.db` is already current. `revalidatePath('/issues')` just re-runs the Server Component, and the closed issue is gone from the list, with no cache to invalidate by hand.

## Connect a tenant

The read and write paths above assume a tenant has already connected GitHub. That flow is client-side, so create the React hook client once in a `"use client"` module and call the hooks from any component.

<Steps>
  <Step title="Create the client">
    ```tsx app/corsair-client.ts theme={null}
    "use client";
    import { createCorsairReactClient } from "corsair/client/react";

    export const {
        useConnectionStatus, useCreateConnectLink, useTenants,
        client, // escape hatch — the vanilla client
    } = createCorsairReactClient({ baseURL: "/api/corsair" });
    ```

    One factory call per app. Every hook is typed against your handler, so `useTenants()` knows it returns tenants. `client` is the escape hatch to the [vanilla client](/adapters/client) for imperative calls. The full hook list is on the [React Hooks](/adapters/react) reference.
  </Step>

  <Step title="Read what's connected">
    ```tsx connections.tsx theme={null}
    "use client";
    import { useConnectionStatus } from "./corsair-client";

    export function Connections({ tenantId }: { tenantId: string }) {
        const { data } = useConnectionStatus({ tenantId });
        if (!data) return null;
        return (
            <ul>
                {Object.entries(data).map(([plugin, status]) => (
                    <li key={plugin}>{plugin}: {status === "connected" ? "✓" : "Connect →"}</li>
                ))}
            </ul>
        );
    }
    ```

    `useConnectionStatus` returns a map of plugin id to `connected | missing_credentials | not_connected`, so your UI knows what's live and what still needs a connect flow.
  </Step>

  <Step title="Mint a connect link">
    ```tsx connect-github.tsx theme={null}
    "use client";
    import { useConnectionStatus, useCreateConnectLink } from "./corsair-client";

    export function ConnectGitHub({ tenantId }: { tenantId: string }) {
        const { mutate, loading } = useCreateConnectLink();
        const { data: status } = useConnectionStatus({ tenantId });

        if (status?.github === "connected") return <span>GitHub connected ✓</span>;

        return (
            <button
                disabled={loading}
                onClick={async () => {
                    const { connectUrl } = await mutate({ plugin: "github", tenantId });
                    window.location.href = connectUrl;
                }}
            >
                Connect GitHub
            </button>
        );
    }
    ```

    Hub hosts the consent screen and runs the OAuth handshake. When the user returns, the tokens are already encrypted in your own database. You never saw them, and neither did Hub. Swap `"github"` for any [plugin](/guides/plugins) you configured.
  </Step>
</Steps>

## Go green

```bash theme={null}
npm run dev
```

The first request to `/api/corsair` registers your delivery URL with Hub and turns the **App sync** dot in the dashboard header green.

## Deploy

Deploy to Vercel as you would any App Router app. The mounted `/api/corsair` route becomes the delivery URL Hub calls in production, with no separate service to stand up.

## Next

<CardGroup cols={2}>
  <Card title="Build a dashboard" icon="table-columns" href="/use-cases/dashboards">
    The full `.db` read / `.api` write pattern, with refresh and webhooks.
  </Card>

  <Card title="React hooks" icon="react" href="/adapters/react">
    Every hook the client factory returns, with types and examples.
  </Card>

  <Card title="Connect / OAuth" icon="link" href="/management/connect">
    The full connect flow, error codes, and retry.
  </Card>

  <Card title="Multi-tenancy" icon="users" href="/concepts/multi-tenancy">
    One flag and every user gets their own data and credentials.
  </Card>
</CardGroup>
