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

# Remix

> Loaders read from .db, actions write through .api. Corsair's read/write split lands directly on Remix's data model.

Remix splits every route into a `loader` that reads and an `action` that writes. Corsair splits every plugin into `.db` for reads and `.api` for writes. The two models line up one-to-one, so a Corsair-backed route is just Remix with the data source swapped. React Router v7 works identically.

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

## Mount the handler

Route modules receive a standard `Request`, which is exactly what the adapter needs. Add a splat route and export the pair.

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

export const { loader, action } = toRemixHandler(corsair, {
    basePath: '/api/corsair',
});
```

`~/server/corsair` is your `createCorsair({ ... })` instance. On React Router v7 the file is `api.corsair.$.tsx` with the same exports.

## Resolve the tenant

Both `loader` and `action` receive the `request`, so read the user off your session the way you already do and pass that id to `withTenant`. Corsair brings no auth of its own; it only needs the resolved id.

```ts theme={null}
import { getTenantId } from '~/server/tenant';   // your auth, not Corsair's

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

## Read and write

The `loader` reads `.db` and returns it; `useLoaderData` renders it. No network call happens on navigation, so the route is as fast as a local query.

```tsx app/routes/issues.tsx theme={null}
import { corsair } from '~/server/corsair';
import { getTenantId } from '~/server/tenant';
import { Form, useLoaderData } from '@remix-run/react';

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

export async function action({ request }: { request: Request }) {
    const tenant = corsair.withTenant(await getTenantId(request));
    const number = Number((await request.formData()).get('number'));
    await tenant.github.api.issues.update({
        owner: 'acme', repo: 'app', issueNumber: number, state: 'closed',
    });
    return null;
}

export default function Issues() {
    const { issues } = useLoaderData<typeof loader>();
    return (
        <ul>
            {issues.map((issue) => (
                <li key={issue.id}>
                    {issue.data.title}
                    <Form method="post">
                        <input type="hidden" name="number" value={issue.data.number} />
                        <button>Close</button>
                    </Form>
                </li>
            ))}
        </ul>
    );
}
```

## Refresh after a write

Remix re-runs the page's `loader` automatically after an `action` returns. The `.api` write already upserted into `.db`, so that revalidation renders the closed issue out of the list with no manual invalidation and no client state to reconcile.

## Connect a tenant

A tenant has to connect GitHub before the loader has anything to read. That flow is client-side, so create the React hook client 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 flips the **App sync** dot in the dashboard header to green.

## Deploy

Deploy however you already ship this app. In production the mounted `/api/corsair` route is the delivery URL Hub calls. The same splat route serves it whether you're on a Node server or a Web-standard host.

## Next

<CardGroup cols={2}>
  <Card title="Build a dashboard" icon="table-columns" href="/use-cases/dashboards">
    The `.db` read / `.api` write pattern in full, 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>
