Getting Started

Quick Start

Open in Claude

Build your first Vim app in 5 minutes

This guide walks you through creating a minimal Vim app that displays patient information when a chart is opened.

1. Create the Project

npx create-next-app@latest my-vim-app --typescript --app
cd my-vim-app
npm install @vimconnect/app-sdk zod

2. Set Up Environment Variables

Create .env.local in your project root:

# Your OAuth credentials (from Vim Connect platform)
NEXT_PUBLIC_CLIENT_ID=your_client_id
CLIENT_SECRET=your_client_secret

# Environment. Leave this as production unless Vim has given you staging access —
# the credentials you create in the Console are production credentials.
NEXT_PUBLIC_ENV=production

3. Create the Launch Page

When a provider clicks your app in the Vim Hub, the extension navigates to your app's launch URL with a launch_id parameter. This page initiates the OAuth flow.

Create src/app/launch/page.tsx:

'use client';

import { useEffect, useRef } from 'react';
import { useSearchParams } from 'next/navigation';
import { Suspense } from 'react';

function LaunchContent() {
  const searchParams = useSearchParams();
  const redirectingRef = useRef(false);

  useEffect(() => {
    if (redirectingRef.current) return;

    const launchId = searchParams.get('launch_id');
    if (!launchId) return;

    redirectingRef.current = true;

    // Generate CSRF token for security
    const csrfToken = crypto.randomUUID();
    sessionStorage.setItem(`oauth_state_${launchId}`, csrfToken);

    // Determine backend URL based on environment
    const backendUrl = process.env.NEXT_PUBLIC_ENV === 'staging'
      ? 'https://api.stage.getvim.ai'
      : 'https://api.getvim.ai';

    // Redirect to Vim OAuth authorization
    const authorizeUrl = new URL('/app-auth/authorize', backendUrl);
    authorizeUrl.searchParams.set('response_type', 'code');
    authorizeUrl.searchParams.set('client_id', process.env.NEXT_PUBLIC_CLIENT_ID!);
    authorizeUrl.searchParams.set('launch', launchId);
    authorizeUrl.searchParams.set('scope', 'launch openid');
    authorizeUrl.searchParams.set('redirect_uri', `${window.location.origin}/app`);
    authorizeUrl.searchParams.set('state', `${launchId}:${csrfToken}`);

    window.location.href = authorizeUrl.toString();
  }, [searchParams]);

  return <div>Redirecting to authorization...</div>;
}

export default function LaunchPage() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LaunchContent />
    </Suspense>
  );
}

4. Create the Token Exchange Endpoint

The OAuth callback returns an authorization code. Exchange it for an access token server-side (keeping your client secret safe).

Create src/app/api/auth/token/route.ts:

import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const { code } = await request.json();

  const backendUrl = process.env.NEXT_PUBLIC_ENV === 'staging'
    ? 'https://api.stage.getvim.ai'
    : 'https://api.getvim.ai';

  const response = await fetch(`${backendUrl}/app-auth/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'authorization_code',
      code,
      client_id: process.env.NEXT_PUBLIC_CLIENT_ID,
      client_secret: process.env.CLIENT_SECRET,
    }),
  });

  if (!response.ok) {
    const error = await response.json().catch(() => ({}));
    return NextResponse.json(error, { status: response.status });
  }

  const tokenData = await response.json();
  return NextResponse.json({
    access_token: tokenData.access_token,
    token_type: tokenData.token_type,
    expires_in: tokenData.expires_in,
  });
}

5. Create the Main App Page

This is where your app lives. It validates the OAuth callback, exchanges the code for a token, initializes the SDK, and subscribes to EHR events.

Create src/app/app/page.tsx:

'use client';

import { Suspense, useEffect, useRef, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { initVimSDK, type VimSDK } from '@vimconnect/app-sdk';

function AppContent() {
  const searchParams = useSearchParams();
  const [sdk, setSdk] = useState<VimSDK | null>(null);
  const [patient, setPatient] = useState<any>(null);
  const [error, setError] = useState<string | null>(null);
  const initRef = useRef(false);

  useEffect(() => {
    if (initRef.current) return;
    initRef.current = true;

    async function initialize() {
      try {
        // 1. Validate OAuth callback parameters
        const code = searchParams.get('code');
        const state = searchParams.get('state');
        if (!code || !state) throw new Error('Missing OAuth parameters');

        // 2. Verify CSRF token
        const [launchId, csrfToken] = state.split(':');
        const stored = sessionStorage.getItem(`oauth_state_${launchId}`);
        if (csrfToken !== stored) throw new Error('CSRF validation failed');
        sessionStorage.removeItem(`oauth_state_${launchId}`);

        // 3. Exchange code for access token
        const tokenRes = await fetch('/api/auth/token', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ code }),
        });
        if (!tokenRes.ok) throw new Error('Token exchange failed');
        const { access_token } = await tokenRes.json();

        // 4. Initialize the SDK
        const vimSdk = await initVimSDK({
          accessToken: access_token,
          debug: true,
        });
        setSdk(vimSdk);

        // 5. Tell the hub we're ready
        vimSdk.hub.setActivationStatus('ENABLED');

        // 6. Subscribe to patient context
        vimSdk.ehr.context.onChange('chart_open:patient', (prev, curr) => {
          if (curr) {
            setPatient(curr);
          } else {
            setPatient(null);
          }
        });
      } catch (err: any) {
        setError(err.message);
      }
    }

    initialize();
  }, [searchParams]);

  if (error) return <div style={{ color: 'red', padding: 16 }}>Error: {error}</div>;
  if (!sdk) return <div style={{ padding: 16 }}>Connecting to Vim...</div>;
  if (!patient) return <div style={{ padding: 16 }}>Waiting for patient chart...</div>;

  return (
    <div style={{ padding: 16 }}>
      <h2>Patient Chart</h2>
      <pre>{JSON.stringify(patient.fields, null, 2)}</pre>
    </div>
  );
}

export default function AppPage() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <AppContent />
    </Suspense>
  );
}

6. Run Your App

npm run dev -- -p 8080

Local development in Chrome

Recent versions of Chrome (v142+) block loading localhost inside iframes by default. Since Vim apps run in an iframe, this can prevent your local app from loading during development — showing up as CORS errors, a blank iframe, or an authentication flow that never starts. This is not a Vim issue.

Workaround: disable the Chrome flag chrome://flags/#local-network-access-check and relaunch the browser. See chromestatus.com/feature/5152728072060928 for details.

7. Test in the EHR

You do not need a real EHR for this. Your developer account comes with a Sandbox EHR — a working EHR with synthetic patients, opened from Build → Sandbox EHR in the Console — and it is the surface Vim reviews your app on. Use it here, and only move to a real EHR when you need something the sandbox cannot show you.

  1. Sign into Chrome with the Vim Connect extension installed, and make sure your app is installed for your account.
  2. Open the Sandbox EHR (or your EHR) in a tab.
  3. Your app launches on its own — see below. Watch your dev server: you should see /launch hit within a second or two of the page loading.
  4. Open a patient chart. The patient data appears in your app.

Clicking your icon does not start the launch flow

This is the part that surprises people. Once the provider has the Vim side panel open in that window and is on a recognised EHR tab, the extension launches every app installed for them, in a hidden frame, before your icon is clicked:

  1. The extension mints a launch_id for your app and loads <your app URL>/launch?launch_id=....
  2. Your launch page runs the OAuth handshake and your app arrives at /app.
  3. initVimSDK() connects, and you set your activation status.
  4. Clicking the icon only reveals an app that is already running.

Two qualifications. The frames live in the side-panel document, and Chrome only opens a side panel on a user gesture — so "before anything is clicked" means before your icon is clicked, not before the provider has opened Vim. And if your app ships both surfaces, the UI frame is loaded only once the worker reports ready, so a worker that fails to start blocks the UI launch too.

Either way your launch and auth path runs unattended, before anyone has expressed interest in your app. A failure there is not an error the provider sees — it is an icon that opens a blank or broken panel. A worker-only app has no icon at all.

What's Next

Now that you have a working app, explore the SDK capabilities:

  • Authentication — understand the full OAuth flow and security considerations
  • Workflow Events — subscribe to chart opens, encounters, referrals, and orders
  • Context — track real-time data changes
  • Writeback — update EHR fields with permission handling
  • React Integration — patterns for managing SDK state in React

On this page