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

# SvelteKit

> Read synced data in a load function, render it in +page.svelte, and mutate through form actions.

SvelteKit keeps server work in `+page.server.ts`. A `load` function fetches, `+page.svelte` renders, and form actions mutate. Corsair drops straight into that flow, with `load` reading from `.db` and form actions writing through `.api`, and the handler itself living in a Web-standard endpoint.

## 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 inside core `corsair`; add one plugin package per service you connect.

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

```
src/
  hooks.server.ts                    # populates event.locals.user
  routes/api/corsair/[...rest]/+server.ts   # the handler
  routes/issues/+page.server.ts      # load reads .db, action writes .api
  routes/issues/+page.svelte         # renders data.issues
  lib/server/corsair.ts              # createCorsair instance
```

## Mount the handler

An endpoint receives a `Request` and returns a `Response`, which is exactly the adapter's shape. Add a rest-parameter route and export the pair.

```ts src/routes/api/corsair/[...rest]/+server.ts theme={null}
import { toSvelteKitHandler } from 'corsair';
import { corsair } from '$lib/server/corsair';

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

`$lib/server/corsair` is your `createCorsair({ ... })` instance. The `[...rest]` segment catches `/api/corsair` and every path beneath it.

## Resolve the tenant

Set the signed-in user on `event.locals` in `hooks.server.ts`, which is your auth's job, not Corsair's. Then `load` and form actions read it off `locals` and hand the id to `withTenant`.

```ts theme={null}
// in load / actions — event.locals.user comes from hooks.server.ts
const tenant = corsair.withTenant(await getTenantId(locals));
```

## Read and write

`load` reads `.db` and hands the data to the page, with no network on navigation. A form action writes through `.api`; because Corsair upserts the response into your database as part of the call, the next `load` sees the row already current.

```ts src/routes/issues/+page.server.ts theme={null}
import { corsair } from '$lib/server/corsair';

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

export const actions = {
    close: async ({ locals, request }) => {
        const tenant = corsair.withTenant(await getTenantId(locals));
        const number = Number((await request.formData()).get('number'));
        await tenant.github.api.issues.update({
            owner: 'acme', repo: 'app', issueNumber: number, state: 'closed',
        });
    },
};
```

The page renders `data.issues` and posts to the action. Entity fields live on `.data` in camelCase:

```svelte src/routes/issues/+page.svelte theme={null}
<script lang="ts">
    export let data;
</script>

<ul>
    {#each data.issues as issue (issue.id)}
        <li>
            {issue.data.title}
            <form method="POST" action="?/close">
                <input type="hidden" name="number" value={issue.data.number} />
                <button>Close</button>
            </form>
        </li>
    {/each}
</ul>
```

## Refresh after a write

A form action that returns nothing makes SvelteKit re-run the page's `load`, so the closed issue drops off on its own. When a write happens outside a form, such as a button handler or a websocket message, call `invalidate` to re-run `load` yourself; the `.api` upsert means the re-read is already current.

```ts theme={null}
import { invalidateAll } from '$app/navigation';
await invalidateAll();   // re-runs load; .db already reflects the write
```

## Connect a tenant

A tenant connects GitHub before `load` has anything to read. Svelte isn't React, so use the vanilla client from a component, a `load` function, or a script.

<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

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

Pick the SvelteKit adapter for your target (`adapter-node`, `adapter-vercel`, and so on) and deploy as usual. The endpoint runs server-side under every adapter, so in production `/api/corsair` is the delivery URL Hub calls.

## 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="Vanilla client" icon="code" href="/adapters/client">
    Every management-API method the client exposes, fully typed.
  </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>
