> ## 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.

# TanStack Start

> Mount Corsair as a TanStack Start server route, read synced data in a server function or loader, and connect tenants with React hooks.

TanStack Start is full-stack React with no serialization seam you have to think about. A server function runs on the server, returns typed data, and the type flows straight into the component that called it. Corsair rides that all the way. Call `.db` inside a server function or a route loader and the entity types land in your JSX without a hand-written API boundary.

## Install

The adapter ships in core. Each integration is its own package. Add one per service you connect.

<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>

Every service is its own `@corsair-dev/*` package. The full catalog with exact ids 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).

## Mount the handler

Export the adapter from a splat server route.

```ts src/routes/api/corsair/$.ts theme={null}
import { toTanStackHandler } from 'corsair';
import { corsair } from '~/server/corsair';

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

`~/server/corsair` is your `createCorsair({ ... })` instance. See [Getting Started](/quick-start) if you haven't built it yet.

<Note>Server-route helpers have shifted across TanStack Start releases. If your version wraps routes differently, keep the shape: a splat route under `/api/corsair` whose `GET`/`POST`/`OPTIONS` come from `toTanStackHandler`.</Note>

## Resolve the tenant

Every read and write is scoped to a tenant id, whatever stable id identifies the current user or org. Resolve that id inside each data server function. Read your session from its request context (however your auth works), pass the id to `corsair.withTenant(tenantId)`, and return only plain data (rows) to the client. Never return the client from `withTenant`: a server function serializes its return value across to the client, which strips the client's methods. The `getIssues` example below shows the pattern.

## Read and write

Wrap the read in a server function so it runs on the server and its return type reaches the client. Load it from a route with `loader`, and the `.db` result is fully typed in the component.

```tsx src/routes/issues.tsx theme={null}
import { createServerFn } from '@tanstack/react-start';
import { createFileRoute } from '@tanstack/react-router';
import { corsair } from '~/server/corsair';

const getIssues = createServerFn().handler(async () => {
    const tenantId = await getTenantId();
    const tenant = corsair.withTenant(tenantId);
    return tenant.github.db.issues.search({ data: { state: 'open' }, limit: 50 });
});

export const Route = createFileRoute('/issues')({
    loader: () => getIssues(),
    component: () => {
        const issues = Route.useLoaderData();
        return issues.map((i) => <li key={i.id}>{i.data.title} — {i.data.user?.login}</li>);
    },
});
```

To write, give a server function the mutation and call `.api`, e.g. `await tenant.github.api.issues.create({ owner: 'acme', repo: 'app', title: '…' })`. Corsair upserts the response, so the next loader read reflects it. Entity fields live on `.data` in camelCase.

## Refresh after a write

The `.api` write already made `.db` current, so the loader just needs to run again. Call `router.invalidate()` after the mutating server function resolves and the route re-runs its loader with fresh `.db` data.

```tsx theme={null}
const router = useRouter();

async function close(number: number) {
    await closeIssue({ data: { number } }); // server fn calling .api
    await router.invalidate();
}
```

## Connect a tenant

The connect UI is plain React, so use the typed [React hooks](/adapters/react) client. Create it once and call the hooks from any route or 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 the TanStack Start app as usual, and the server route ships with it. In production, `/api/corsair` at your deployed origin is the delivery URL Hub calls.

## Next

<CardGroup cols={2}>
  <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="Use with an agent" icon="robot" href="/mcp-adapters/vercel-ai">
    Give an agent the Corsair tools and let it call any endpoint.
  </Card>

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