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

# Vibe Code Your Dashboard

> Scaffold a T3 app, wire in Google Calendar with Corsair, then let an AI agent build the whole dashboard from a single prompt.

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

export const GenerateKEK = () => {
  const [key, setKey] = useState('');
  const [copiedCmd, setCopiedCmd] = useState(false);
  const [copiedEnv, setCopiedEnv] = useState(false);
  const generate = () => {
    const bytes = new Uint8Array(32);
    crypto.getRandomValues(bytes);
    setKey(btoa(String.fromCharCode(...bytes)));
  };
  useEffect(() => {
    generate();
  }, []);
  const copy = (text, setter) => {
    navigator.clipboard.writeText(text);
    setter(true);
    setTimeout(() => setter(false), 2000);
  };
  const envValue = `CORSAIR_KEK="${key || 'loading...'}"`;
  const blockStyle = {
    borderRadius: '0.5rem',
    overflow: 'hidden',
    border: '1px solid var(--border, rgba(255,255,255,0.1))',
    backgroundColor: 'var(--code-background, #0d1117)',
    fontFamily: 'var(--font-mono, ui-monospace, monospace)',
    fontSize: '0.8125rem',
    lineHeight: '1.6'
  };
  const headerStyle = {
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'space-between',
    padding: '0.375rem 0.75rem',
    borderBottom: '1px solid var(--border, rgba(255,255,255,0.1))'
  };
  const labelStyle = {
    fontSize: '0.75rem',
    color: 'var(--muted-foreground, rgba(255,255,255,0.4))'
  };
  const btnGroupStyle = {
    display: 'flex',
    alignItems: 'center',
    gap: '0.375rem'
  };
  const btnStyle = {
    display: 'inline-flex',
    alignItems: 'center',
    gap: '0.3rem',
    padding: '0.2rem 0.6rem',
    borderRadius: '0.375rem',
    fontSize: '0.7rem',
    fontWeight: 500,
    cursor: 'pointer',
    transition: 'opacity 0.15s',
    border: '1px solid var(--border, rgba(255,255,255,0.15))',
    backgroundColor: 'var(--muted, rgba(255,255,255,0.06))',
    color: 'var(--foreground, rgba(255,255,255,0.75))',
    fontFamily: 'var(--font-sans, sans-serif)'
  };
  const generateBtnStyle = {
    ...btnStyle,
    backgroundColor: 'var(--primary, #2d7387)',
    border: '1px solid transparent',
    color: '#fff'
  };
  const codeStyle = {
    display: 'block',
    padding: '0.75rem 1rem',
    color: 'var(--code-foreground, rgba(255,255,255,0.85))',
    whiteSpace: 'pre',
    overflowX: 'auto',
    margin: 0,
    background: 'none'
  };
  return <div className="not-prose" style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '0.625rem'
  }}>
      <div style={blockStyle}>
        <div style={headerStyle}>
          <span style={labelStyle}>bash</span>
          <button style={btnStyle} onClick={() => copy('openssl rand -base64 32', setCopiedCmd)}>
            {copiedCmd ? '✓ Copied' : 'Copy'}
          </button>
        </div>
        <pre style={{
    margin: 0
  }}><code style={codeStyle}>openssl rand -base64 32</code></pre>
      </div>
      <div style={blockStyle}>
        <div style={headerStyle}>
          <span style={labelStyle}>.env</span>
          <div style={btnGroupStyle}>
            <button style={generateBtnStyle} onClick={generate}>
              Regenerate
            </button>
            <button style={btnStyle} onClick={() => copy(envValue, setCopiedEnv)}>
              {copiedEnv ? '✓ Copied' : 'Copy'}
            </button>
          </div>
        </div>
        <pre style={{
    margin: 0
  }}>
          <code style={codeStyle}>{envValue}</code>
        </pre>
      </div>
    </div>;
};

By the end of this guide you'll have a working Next.js app with a live Google Calendar dashboard — and you'll have written almost none of the UI yourself.

## Corsair context prompt for your agent

Paste this before your feature prompt so the agent knows how Corsair works:

```
This app uses Corsair. Every Corsair plugin has two namespaces:

  corsair.<plugin>.db   — reads from the local database (no network, instant)
  corsair.<plugin>.api  — calls the live external API

Both follow the same shape: corsair.<plugin>.[db|api].<group>.<method>(args)

When to use which:
- Rendering UI / reading data → .db (always the default)
- Creating, updating, or deleting → .api
- User-triggered refresh / sync → .api, then re-read from .db
- Every .api response is auto-saved to .db

Entity data is on .data in camelCase. Use .search({}) or .list() on .db to query.

To see what's available, run:
  pnpm corsair list --<plugin>
```

<Steps>
  <Step>
    ## Scaffold the T3 app

    <CodeGroup>
      ```bash npm theme={null}
      npm create t3-app@latest my-cal-dashboard -- --CI --trpc --tailwind --appRouter
      ```

      ```bash yarn theme={null}
      yarn create t3-app@latest my-cal-dashboard -- --CI --trpc --tailwind --appRouter
      ```

      ```bash pnpm theme={null}
      pnpm create t3-app@latest my-cal-dashboard -- --CI --trpc --tailwind --appRouter
      ```

      ```bash bun theme={null}
      bun create t3-app@latest my-cal-dashboard -- --CI --trpc --tailwind --appRouter
      ```
    </CodeGroup>

    ```bash theme={null}
    cd my-cal-dashboard
    ```
  </Step>

  <Step>
    ## Install

    <CodeGroup>
      ```bash npm theme={null}
      npm install corsair @corsair-dev/googlecalendar
      ```

      ```bash yarn theme={null}
      yarn add corsair @corsair-dev/googlecalendar
      ```

      ```bash pnpm theme={null}
      pnpm install corsair @corsair-dev/googlecalendar
      ```

      ```bash bun theme={null}
      bun add corsair @corsair-dev/googlecalendar
      ```
    </CodeGroup>
  </Step>

  <Step>
    ## Generate your encryption key

    Corsair encrypts stored credentials with a Key Encryption Key. Click **Regenerate** for a new one, or run the command yourself:

    <GenerateKEK />

    <Warning>
      **Keep this key safe**
      If you lose it, you lose access to all stored credentials. Treat it like a root password.
    </Warning>
  </Step>

  <Step>
    ## Migrate the database

    Corsair needs five tables. 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>

    <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)
        );
        ```
      </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>
  </Step>

  <Step>
    ## Create src/server/corsair.ts

    ```ts src/server/corsair.ts theme={null}
    import 'dotenv/config';
    import Database from 'better-sqlite3';
    import { createCorsair } from 'corsair';
    import { googlecalendar } from "@corsair-dev/googlecalendar";

    const db = new Database('corsair.db');

    export const corsair = createCorsair({
        plugins: [googlecalendar()],
        database: db,
        kek: process.env.CORSAIR_KEK!,
    });
    ```
  </Step>

  <Step>
    ## Connect Google Calendar

    Google Calendar uses OAuth2. You'll need a GCP OAuth app first:

    1. Go to the [Google Cloud Console](https://console.cloud.google.com/), create a project, and enable the **Google Calendar API**.
    2. Under **APIs & Services → Credentials**, create an **OAuth 2.0 Client ID** (Application type: Web application). No redirect URI needed — Corsair handles it locally.
    3. Copy your Client ID and Client Secret, then store them:

    <CodeGroup>
      ```bash npm theme={null}
      npm install @corsair-dev/cli
      ```

      ```bash yarn theme={null}
      yarn add @corsair-dev/cli
      ```

      ```bash pnpm theme={null}
      pnpm install @corsair-dev/cli
      ```

      ```bash bun theme={null}
      bun add @corsair-dev/cli
      ```
    </CodeGroup>

    <CodeGroup>
      ```bash npm theme={null}
      npx corsair setup --plugin=googlecalendar client_id=YOUR_CLIENT_ID client_secret=YOUR_CLIENT_SECRET
      ```

      ```bash yarn theme={null}
      yarn corsair setup --plugin=googlecalendar client_id=YOUR_CLIENT_ID client_secret=YOUR_CLIENT_SECRET
      ```

      ```bash pnpm theme={null}
      pnpm corsair setup --plugin=googlecalendar client_id=YOUR_CLIENT_ID client_secret=YOUR_CLIENT_SECRET
      ```

      ```bash bun theme={null}
      bunx corsair setup --plugin=googlecalendar client_id=YOUR_CLIENT_ID client_secret=YOUR_CLIENT_SECRET
      ```
    </CodeGroup>

    <AccordionGroup>
      <Accordion title="pnpm: command not found or setup fails?">
        If `pnpm corsair` isn't found, add this to your `package.json` so pnpm builds the native dependency correctly:

        ```json package.json theme={null}
        {
          "pnpm": {
            "onlyBuiltDependencies": [
              "better-sqlite3"
            ]
          }
        }
        ```

        Then re-run `pnpm install` and retry the setup command.
      </Accordion>
    </AccordionGroup>

    4. Start the OAuth flow:

    <CodeGroup>
      ```bash npm theme={null}
      npx corsair auth --plugin=googlecalendar
      ```

      ```bash yarn theme={null}
      yarn corsair auth --plugin=googlecalendar
      ```

      ```bash pnpm theme={null}
      pnpm corsair auth --plugin=googlecalendar
      ```

      ```bash bun theme={null}
      bunx corsair auth --plugin=googlecalendar
      ```
    </CodeGroup>

    Open the `authUrl` printed in the terminal. Once you authorize in the browser, tokens are saved automatically and the command exits.
  </Step>

  <Step>
    ## Vibe code the dashboard

    Paste this prompt into Claude, Cursor, or any AI coding agent:

    ```
    I have a Next.js T3 app. The Google Calendar plugin is connected via Corsair.

    Build me a Google Calendar dashboard on the home page. I want to see:

    - A stats row showing: how many events I have today, how many this week, what my next meeting is, and how many hours are blocked
    today
    - Today's full agenda — all my events for today in order with times and titles
    - An upcoming events panel — my next couple weeks of events grouped by day
    - A Create Meeting button that opens a form to schedule a new event
    - A notes field on each meeting card where I can save and edit private notes per event (store these in a local table, not on Google
    Calendar)
    - A Refresh button in the header that fetches the latest events from Google Calendar and updates what's shown on screen, including
    removing any events the user has deleted from Google Calendar

    The app should use await corsair.googlecalendar.db as a cache — load from there first, and only call Google Calendar when the data isn't
    cached yet, when creating an event, or when the user clicks Refresh. The page should not make any live API calls on its own after
    the first load.
    ```

    Run `pnpm dev` when it's done.
  </Step>
</Steps>

***

## What's next

<CardGroup cols={2}>
  <Card title="Google Calendar Plugin" href="/plugins/googlecalendar/api">
    Full API reference — create events, check availability, and handle webhook notifications.
  </Card>

  <Card title="Webhooks" href="/plugins/googlecalendar/webhooks">
    React to calendar changes in real time — new events, updates, and deletions pushed to your server.
  </Card>

  <Card title="Workflows" href="/guides/workflows">
    Chain calendar events to other plugins — meeting created → Slack notification → Linear task.
  </Card>

  <Card title="Multi-Tenancy" href="/concepts/multi-tenancy">
    Building a product? Each user gets their own calendar credentials and isolated data.
  </Card>
</CardGroup>
