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

# Node.js

> Mount Corsair on a bare node:http server with toNodeHandler. You own the plumbing, the adapter handles the body.

No framework, no router, just `node:http` and the request/response objects it hands you. `toNodeHandler` takes the corsair instance and returns a `(req, res)` function you can drop straight into `createServer`, or mount behind a path check on a server you already run. It reads the raw request stream itself, so there's nothing to parse first. This is the right level for scripts, workers, and tiny services where pulling in Express would be the only dependency.

## Install

The adapter ships in core `corsair`, with nothing extra for `node:http`. Add a plugin package per service you integrate (`@corsair-dev/github`, and so on).

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

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

```ts server.ts theme={null}
import { createServer } from 'node:http';
import { toNodeHandler } from 'corsair';
import { corsair } from './corsair';

const handler = toNodeHandler(corsair, { basePath: '/api/corsair' });

createServer(handler).listen(3000);
```

`./corsair` is your `createCorsair({ ... })` instance. See [Getting Started](/quick-start) if you haven't built it yet. Already have a server? Mount it behind a path check instead: `if (req.url?.startsWith('/api/corsair')) return handler(req, res)`.

## Read and write

You call Corsair the same way everywhere; the only Node-specific part is serializing the result yourself. Here's a bare JSON endpoint that reads open issues from `.db` (no network, no rate limit) and opens a new one through `.api`:

```ts issues.ts theme={null}
import { corsair } from './corsair';

const tenant = corsair.withTenant('acme');

export async function listOpenIssues(res: ServerResponse) {
    const issues = await tenant.github.db.issues.search({
        data: { state: 'open' },
        limit: 50,
    });

    res.writeHead(200, { 'content-type': 'application/json' });
    // entity fields live on .data, camelCase and typed
    res.end(JSON.stringify(issues.map((i) => i.data.title)));
}

export async function openIssue(title: string) {
    // the API response is upserted, so the next .db read already has it
    await tenant.github.api.issues.create({ owner: 'acme', repo: 'app', title });
}
```

## Connect a tenant

There's no React here, so use the vanilla client from a script, a worker, or a non-React frontend that talks to your server.

<Steps>
  <Step title="Create the client">
    ```ts corsair-client.ts theme={null}
    import { createCorsairClient } from "corsair";

    export const client = createCorsairClient({ baseURL: "/api/corsair" });
    ```

    `createCorsairClient` is a typed fetch wrapper over the management API, no React required. Use it from your frontend, a worker, or a script. Every route is typed against its response. Full surface on the [Vanilla Client](/adapters/client) reference.
  </Step>

  <Step title="Read what's connected">
    ```ts connections.ts theme={null}
    const status = await client.connectionStatus.get({ tenantId: "acme" });
    // { github: 'connected', slack: 'not_connected', notion: 'missing_credentials' }
    ```

    `connectionStatus.get` returns a map of plugin id to `connected | missing_credentials | not_connected`, so you can show what's live and what still needs connecting.
  </Step>

  <Step title="Mint a connect link">
    ```ts connect.ts theme={null}
    const { connectUrl } = await client.connect.createLink({
        plugin: "github",
        tenantId: "acme",
    });
    window.location.href = connectUrl; // send the user to Hub's hosted connect page
    ```

    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

Start the server:

```bash theme={null}
npx tsx server.ts
```

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

## Next

<CardGroup cols={2}>
  <Card title="Vanilla client" icon="code" href="/adapters/client">
    Every management-API method, fully typed.
  </Card>

  <Card title="Prefer a router?" icon="server" href="/frameworks/express">
    Express and Hono both run on Node with a one-line adapter.
  </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>
