> ## Documentation Index
> Fetch the complete documentation index at: https://corsair-feat-reconnect-error.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Database

> Six tables, always-fresh integration data, tenant-scoped by default.

export const WindowsMigrationNote = () => <div className="not-prose" style={calloutStyle}>
    <WindowsIcon />
    <div>
      <div style={titleStyle}>PowerShell syntax</div>
      <div style={bodyStyle}>
        Bash redirection and <span style={codeStyle}>$VAR</span> env vars do not work in PowerShell.
        Use the Windows commands shown in this tab instead.
      </div>
    </div>
  </div>;

Every API call and webhook that flows through Corsair is stored in your database automatically. When something changes in Slack, GitHub, or Linear, Corsair updates the same row, so your UI can read from `*.db.*` instead of hitting third-party APIs on every page load. Third-party data becomes an extension of your own DB, always fresh.

## Get started

Run the migration once, then pass your connection to `createCorsair({ database, ... })`. See [Quick Start](/quick-start) for a full setup example.

<Tabs>
  <Tab title="SQLite">
    Install the driver, then run the migration:

    <CodeGroup>
      ```bash npm theme={null}
      npm install better-sqlite3
      ```

      ```bash yarn theme={null}
      yarn add better-sqlite3
      ```

      ```bash pnpm theme={null}
      pnpm install better-sqlite3
      ```

      ```bash bun theme={null}
      bun add better-sqlite3
      ```
    </CodeGroup>

    <CodeGroup>
      ```bash npm theme={null}
      npm install --save-dev @types/better-sqlite3
      ```

      ```bash yarn theme={null}
      yarn add --dev @types/better-sqlite3
      ```

      ```bash pnpm theme={null}
      pnpm install --save-dev @types/better-sqlite3
      ```

      ```bash bun theme={null}
      bun add --dev @types/better-sqlite3
      ```
    </CodeGroup>
  </Tab>

  <Tab title="PostgreSQL">
    Install the driver, then run the migration:

    <CodeGroup>
      ```bash npm theme={null}
      npm install pg
      ```

      ```bash yarn theme={null}
      yarn add pg
      ```

      ```bash pnpm theme={null}
      pnpm install pg
      ```

      ```bash bun theme={null}
      bun add pg
      ```
    </CodeGroup>

    <CodeGroup>
      ```bash npm theme={null}
      npm install --save-dev @types/pg
      ```

      ```bash yarn theme={null}
      yarn add --dev @types/pg
      ```

      ```bash pnpm theme={null}
      pnpm install --save-dev @types/pg
      ```

      ```bash bun theme={null}
      bun add --dev @types/pg
      ```
    </CodeGroup>
  </Tab>
</Tabs>

<Tabs>
  <Tab title="SQLite">
    <AccordionGroup>
      <Accordion title="View migration SQL">
        ```sql migration.sql theme={null}
        CREATE TABLE IF NOT EXISTS corsair_integrations (
            id TEXT PRIMARY KEY,
            created_at INTEGER NOT NULL,
            updated_at INTEGER NOT NULL,
            name TEXT NOT NULL,
            config TEXT NOT NULL DEFAULT '{}',
            dek TEXT NULL
        );

        CREATE TABLE IF NOT EXISTS corsair_accounts (
            id TEXT PRIMARY KEY,
            created_at INTEGER NOT NULL,
            updated_at INTEGER NOT NULL,
            tenant_id TEXT NOT NULL,
            integration_id TEXT NOT NULL,
            config TEXT NOT NULL DEFAULT '{}',
            dek TEXT NULL,
            FOREIGN KEY (integration_id) REFERENCES corsair_integrations(id)
        );

        CREATE TABLE IF NOT EXISTS corsair_entities (
            id TEXT PRIMARY KEY,
            created_at INTEGER NOT NULL,
            updated_at INTEGER NOT NULL,
            account_id TEXT NOT NULL,
            entity_id TEXT NOT NULL,
            entity_type TEXT NOT NULL,
            version TEXT NOT NULL,
            data TEXT NOT NULL DEFAULT '{}',
            FOREIGN KEY (account_id) REFERENCES corsair_accounts(id)
        );

        CREATE TABLE IF NOT EXISTS corsair_events (
            id TEXT PRIMARY KEY,
            created_at INTEGER NOT NULL,
            updated_at INTEGER NOT NULL,
            account_id TEXT NOT NULL,
            event_type TEXT NOT NULL,
            payload TEXT NOT NULL DEFAULT '{}',
            status TEXT,
            FOREIGN KEY (account_id) REFERENCES corsair_accounts(id)
        );

        CREATE TABLE IF NOT EXISTS corsair_permissions (
            id TEXT PRIMARY KEY,
            created_at INTEGER NOT NULL,
            updated_at INTEGER NOT NULL,
            token TEXT NOT NULL,
            plugin TEXT NOT NULL,
            endpoint TEXT NOT NULL,
            args TEXT NOT NULL,
            tenant_id TEXT NOT NULL,
            status TEXT NOT NULL DEFAULT 'pending',
            expires_at TEXT NOT NULL,
            error TEXT
        );

        CREATE TABLE IF NOT EXISTS corsair_connect_requests (
            tenant_id TEXT PRIMARY KEY,
            plugin TEXT NOT NULL,
            connect_url TEXT NOT NULL,
            requested_at TEXT NOT NULL
        );
        ```
      </Accordion>
    </AccordionGroup>

    <Tabs>
      <Tab title="macOS / Linux">
        ```bash theme={null}
        sqlite3 corsair.db < migration.sql
        ```
      </Tab>

      <Tab title="Windows (PowerShell)">
        <WindowsMigrationNote />

        ```powershell theme={null}
        Get-Content migration.sql | sqlite3 corsair.db
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab title="PostgreSQL">
    <Tabs>
      <Tab title="SQL">
        <AccordionGroup>
          <Accordion title="View migration SQL">
            ```sql migration.sql theme={null}
            CREATE TABLE IF NOT EXISTS corsair_integrations (
                id TEXT PRIMARY KEY,
                created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
                updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
                name TEXT NOT NULL,
                config JSONB NOT NULL DEFAULT '{}',
                dek TEXT NULL
            );

            CREATE TABLE IF NOT EXISTS corsair_accounts (
                id TEXT PRIMARY KEY,
                created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
                updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
                tenant_id TEXT NOT NULL,
                integration_id TEXT NOT NULL REFERENCES corsair_integrations(id),
                config JSONB NOT NULL DEFAULT '{}',
                dek TEXT NULL
            );

            CREATE TABLE IF NOT EXISTS corsair_entities (
                id TEXT PRIMARY KEY,
                created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
                updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
                account_id TEXT NOT NULL REFERENCES corsair_accounts(id),
                entity_id TEXT NOT NULL,
                entity_type TEXT NOT NULL,
                version TEXT NOT NULL,
                data JSONB NOT NULL DEFAULT '{}'
            );

            CREATE TABLE IF NOT EXISTS corsair_events (
                id TEXT PRIMARY KEY,
                created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
                updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
                account_id TEXT NOT NULL REFERENCES corsair_accounts(id),
                event_type TEXT NOT NULL,
                payload JSONB NOT NULL DEFAULT '{}',
                status TEXT
            );

            CREATE TABLE IF NOT EXISTS corsair_permissions (
                id TEXT PRIMARY KEY,
                created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
                updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
                token TEXT NOT NULL,
                plugin TEXT NOT NULL,
                endpoint TEXT NOT NULL,
                args TEXT NOT NULL,
                tenant_id TEXT NOT NULL,
                status TEXT NOT NULL DEFAULT 'pending',
                expires_at TEXT NOT NULL,
                error TEXT
            );

            CREATE TABLE IF NOT EXISTS corsair_connect_requests (
                tenant_id TEXT PRIMARY KEY,
                plugin TEXT NOT NULL,
                connect_url TEXT NOT NULL,
                requested_at TEXT NOT NULL
            );
            ```
          </Accordion>
        </AccordionGroup>

        <Tabs>
          <Tab title="macOS / Linux">
            ```bash theme={null}
            psql $DATABASE_URL -f migration.sql
            ```
          </Tab>

          <Tab title="Windows (PowerShell)">
            <WindowsMigrationNote />

            ```powershell theme={null}
            psql $env:DATABASE_URL -f migration.sql
            ```
          </Tab>
        </Tabs>
      </Tab>

      <Tab title="Drizzle">
        <AccordionGroup>
          <Accordion title="View Drizzle schema">
            ```ts src/server/db/schema.ts theme={null}
            import { pgTable, text, jsonb, timestamp } from 'drizzle-orm/pg-core';

            export const corsairIntegrations = pgTable('corsair_integrations', {
                id: text('id').primaryKey(),
                createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
                updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
                name: text('name').notNull(),
                config: jsonb('config').notNull().default({}),
                dek: text('dek'),
            });

            export const corsairAccounts = pgTable('corsair_accounts', {
                id: text('id').primaryKey(),
                createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
                updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
                tenantId: text('tenant_id').notNull(),
                integrationId: text('integration_id').notNull().references(() => corsairIntegrations.id),
                config: jsonb('config').notNull().default({}),
                dek: text('dek'),
            });

            export const corsairEntities = pgTable('corsair_entities', {
                id: text('id').primaryKey(),
                createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
                updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
                accountId: text('account_id').notNull().references(() => corsairAccounts.id),
                entityId: text('entity_id').notNull(),
                entityType: text('entity_type').notNull(),
                version: text('version').notNull(),
                data: jsonb('data').notNull().default({}),
            });

            export const corsairEvents = pgTable('corsair_events', {
                id: text('id').primaryKey(),
                createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
                updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
                accountId: text('account_id').notNull().references(() => corsairAccounts.id),
                eventType: text('event_type').notNull(),
                payload: jsonb('payload').notNull().default({}),
                status: text('status'),
            });

            export const corsairPermissions = pgTable('corsair_permissions', {
                id: text('id').primaryKey(),
                createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
                updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
                token: text('token').notNull(),
                plugin: text('plugin').notNull(),
                endpoint: text('endpoint').notNull(),
                args: text('args').notNull(),
                tenantId: text('tenant_id').notNull(),
                status: text('status').notNull().default('pending'),
                expiresAt: text('expires_at').notNull(),
                error: text('error'),
            });

            export const corsairConnectRequests = pgTable('corsair_connect_requests', {
                tenantId: text('tenant_id').primaryKey(),
                plugin: text('plugin').notNull(),
                connectUrl: text('connect_url').notNull(),
                requestedAt: text('requested_at').notNull(),
            });
            ```
          </Accordion>
        </AccordionGroup>

        Then push the schema to your database:

        ```bash theme={null}
        npx drizzle-kit push
        ```
      </Tab>

      <Tab title="Prisma">
        <AccordionGroup>
          <Accordion title="View Prisma schema">
            ```prisma schema.prisma theme={null}
            model CorsairIntegration {
              id        String           @id
              createdAt DateTime         @default(now()) @map("created_at") @db.Timestamptz
              updatedAt DateTime         @updatedAt @map("updated_at") @db.Timestamptz
              name      String
              config    Json             @default("{}")
              dek       String?
              accounts  CorsairAccount[]

              @@map("corsair_integrations")
            }

            model CorsairAccount {
              id            String             @id
              createdAt     DateTime           @default(now()) @map("created_at") @db.Timestamptz
              updatedAt     DateTime           @updatedAt @map("updated_at") @db.Timestamptz
              tenantId      String             @map("tenant_id")
              integrationId String             @map("integration_id")
              config        Json               @default("{}")
              dek           String?
              integration   CorsairIntegration @relation(fields: [integrationId], references: [id])
              entities      CorsairEntity[]
              events        CorsairEvent[]

              @@map("corsair_accounts")
            }

            model CorsairEntity {
              id         String         @id
              createdAt  DateTime       @default(now()) @map("created_at") @db.Timestamptz
              updatedAt  DateTime       @updatedAt @map("updated_at") @db.Timestamptz
              accountId  String         @map("account_id")
              entityId   String         @map("entity_id")
              entityType String         @map("entity_type")
              version    String
              data       Json           @default("{}")
              account    CorsairAccount @relation(fields: [accountId], references: [id])

              @@map("corsair_entities")
            }

            model CorsairEvent {
              id        String         @id
              createdAt DateTime       @default(now()) @map("created_at") @db.Timestamptz
              updatedAt DateTime       @updatedAt @map("updated_at") @db.Timestamptz
              accountId String         @map("account_id")
              eventType String         @map("event_type")
              payload   Json           @default("{}")
              status    String?
              account   CorsairAccount @relation(fields: [accountId], references: [id])

              @@map("corsair_events")
            }

            model CorsairPermission {
              id        String   @id
              createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
              updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
              token     String
              plugin    String
              endpoint  String
              args      String
              tenantId  String   @map("tenant_id")
              status    String   @default("pending")
              expiresAt String   @map("expires_at")
              error     String?

              @@map("corsair_permissions")
            }

            model CorsairConnectRequest {
              tenantId    String @id @map("tenant_id")
              plugin      String
              connectUrl  String @map("connect_url")
              requestedAt String @map("requested_at")

              @@map("corsair_connect_requests")
            }
            ```
          </Accordion>
        </AccordionGroup>

        Then run the migration:

        ```bash theme={null}
        npx prisma migrate dev
        ```
      </Tab>
    </Tabs>
  </Tab>
</Tabs>

## Example: Slack

All data is scoped to tenants. `withTenant()` ensures you only read and write that tenant's rows, with no cross-contamination.

```ts theme={null}
const tenant = corsair.withTenant('user_abc123');

// 1. Send a message — Corsair stores it in corsair_entities
const posted = await tenant.slack.api.messages.post({
    channel: 'C01234567',
    text: 'Hello from Corsair!',
});
const messageId = posted.ts!;

// 2. Someone edits the message in Slack — a webhook updates the same row

// 3. Your UI reads from the local DB — always up to date
const [message] = await tenant.slack.db.messages.search({
    entity_id: messageId,
});
console.log(message?.data.text);
```

Use `*.api.*` to write or force a fresh read. Use `*.db.*` for lists, search, and detail pages. Plugin-specific filters are in each plugin's database reference (e.g. [Slack](/plugins/slack/database)).

## Core tables

Six tables. The first five are shared by every integration — Slack, GitHub, Linear, and the rest use the same schema. `corsair_connect_requests` records a tenant's pending connect prompt.

| Table                      | Purpose                                                                                                                                                    |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `corsair_integrations`     | One row per enabled plugin (`slack`, `github`, …)                                                                                                          |
| `corsair_accounts`         | Per-tenant connection + encrypted credentials (`tenant_id`, `integration_id`)                                                                              |
| `corsair_entities`         | Synced resources like messages, channels, issues, repos (`entity_id`, `entity_type`, `data`)                                                               |
| `corsair_events`           | Audit log of API calls and webhooks                                                                                                                        |
| `corsair_permissions`      | Approval queue for gated [permissions](/concepts/permissions) (`token`, `tenant_id`, `endpoint`, `args`, `status`)                                         |
| `corsair_connect_requests` | A tenant's pending connect prompt, written when a call needs auth and read by the client to open the connect dialog (`tenant_id`, `plugin`, `connect_url`) |

```sql theme={null}
-- corsair_entities example (a Slack message)
account_id: "acc_1"
entity_id: "1234567890.123456"
entity_type: "messages"
data: { text: "Hello!", channel: "C01234567", ts: "1234567890.123456" }
```

Schema types: [`packages/corsair/db/index.ts`](https://github.com/corsairdev/corsair/blob/main/packages/corsair/db/index.ts).

## Keeping data fresh

Corsair doesn't snapshot data once and forget it. Every write path keeps the same row current:

1. **API calls**: when you call `tenant.slack.api.messages.post`, Corsair sends the request and upserts the response into `corsair_entities`.
2. **Webhooks**: when Slack notifies you that a message changed, Corsair finds the row by `entity_id` and updates it in place.
3. **Out-of-order events**: if an update arrives before you've seen the create, Corsair fetches from the API and backfills the row (see [Webhooks](/concepts/webhooks)).

You don't build a sync layer or poll for changes. Query `corsair_entities` (or `*.db.*`) and trust that Corsair is maintaining it.

## How to use it

### Query through Corsair

Use the typed ORM on each plugin, no raw SQL required for most reads:

```ts theme={null}
const tenant = corsair.withTenant('user_abc123');

await tenant.slack.db.messages.list({ limit: 50 });
await tenant.slack.db.messages.findByEntityId('1234567890.123456');
await tenant.github.db.repositories.search({ data: { name: { contains: 'api' } } });
```

### Join to your own tables

Reference `corsair_entities.id` from your schema and treat third-party data like first-class rows in your app:

```sql theme={null}
CREATE TABLE support_tickets (
    id UUID PRIMARY KEY,
    title TEXT NOT NULL,
    slack_message_id TEXT NOT NULL REFERENCES corsair_entities(id)
);
```

```ts theme={null}
// Create a ticket linked to a Corsair entity
const posted = await tenant.slack.api.messages.post({ channel, text });
const [entity] = await tenant.slack.db.messages.search({ entity_id: posted.ts! });

await db.insert(supportTickets).values({
    title: 'Customer issue',
    slackMessageId: entity!.id, // corsair_entities.id
});

// Join your table to live Slack data
const rows = await db
    .select()
    .from(supportTickets)
    .innerJoin(corsairEntities, eq(supportTickets.slackMessageId, corsairEntities.id));
// rows[].corsair_entities.data.text stays fresh via webhooks
```

Because Corsair updates `corsair_entities` on every API call and webhook, foreign keys to that table always point at current data, not a stale cache you wrote once.

### When to use API vs DB

| Use       | When                                                                    |
| --------- | ----------------------------------------------------------------------- |
| `*.api.*` | Writes, deletes, or you need the latest value right now from the source |
| `*.db.*`  | UI lists, search, detail pages; fast reads, no rate limits              |

## SQLite, Postgres, and ORMs

Corsair runs on **your** database. Pass a connection to `createCorsair`, and it works alongside whatever ORM you already use for your own tables.

<Tabs>
  <Tab title="SQLite">
    Best for local dev and small apps. Pass a `better-sqlite3` instance:

    ```ts theme={null}
    import Database from 'better-sqlite3';

    export const corsair = createCorsair({
        database: new Database('corsair.db'),
        plugins: [slack()],
        kek: process.env.CORSAIR_KEK!,
    });
    ```
  </Tab>

  <Tab title="PostgreSQL">
    Production default. Pass a `pg` Pool:

    ```ts theme={null}
    import { Pool } from 'pg';

    export const corsair = createCorsair({
        database: new Pool({ connectionString: process.env.DATABASE_URL }),
        plugins: [slack()],
        kek: process.env.CORSAIR_KEK!,
    });
    ```
  </Tab>

  <Tab title="Drizzle / Prisma">
    Define the six Corsair tables in your ORM schema (see migration tabs above), run your normal migrations, and pass the **underlying connection** to Corsair, not the ORM client:

    ```ts theme={null}
    const pool = new Pool({ connectionString: process.env.DATABASE_URL });

    export const corsair = createCorsair({ database: pool, ... });

    // Drizzle — your app tables
    export const db = drizzle(pool);

    // Prisma — your app tables (same pool, not PrismaClient)
    export const prisma = new PrismaClient();
    ```

    Corsair also accepts **postgres.js** and a typed **Kysely** instance if you want to share a single query builder.
  </Tab>
</Tabs>

Supported connection types: `pg` Pool, `better-sqlite3`, postgres.js `Sql`, and Kysely. Use the same database for Corsair tables and your app tables: one migration, one connection string.

## What's next

<CardGroup cols={2}>
  <Card title="Multi-Tenancy" href="/concepts/multi-tenancy">
    Scope credentials and data per user with `withTenant()`.
  </Card>

  <Card title="Webhooks" href="/concepts/webhooks">
    How incoming events update your rows automatically.
  </Card>

  <Card title="API" href="/concepts/api">
    Call third-party APIs with full type safety.
  </Card>
</CardGroup>
