# Frontend OAuth with the Connect SDK

{% hint style="info" %}
This guide uses the sandbox environment.&#x20;
{% endhint %}

Build a React app that authenticates users with Humanity's OAuth and gates access based on the `isHuman` biometric credential.&#x20;

The Connect SDK gives you three core methods that you can use to control the flow with no pre-built components or abstraction layer.

After this guide you'll have:

* A working Vite + React app that runs locally
* The complete OAuth + PKCE flow wired up yourself
* Biometric credential verification with live status display
* A full sandbox test using mock credentials

Use this approach if you want granular control over authentication, need to understand the flow before abstracting it, or are building a custom integration.

Full source

{% embed url="<https://github.com/humanity-developers/humanity-connect-sdk-vite-biometric-quick-start>" %}

## Before you start

You need:

* [Node 18](https://nodejs.org/en) or higher
* [Bun](https://bun.sh) (used throughout this guide)
* A Humanity Developer Account to access the [Developer Portal](https://developers.humanity.org) and [Sandbox Dashboard](https://app.sandbox.humanity.org)

If you haven't registered an application yet, follow the [Developer Portal setup guide](https://docs.humanity.org/developer-portal) first. You'll need your `clientId` and `redirectUri`.

## 01. Create the project

```bash
mkdir connect-sdk-vite-biometric-quick-start && cd connect-sdk-vite-biometric-quick-start
echo "node_modules\n.env.local" >> .gitignore
git init
```

Create `package.json`:

```json
{
  "name": "humanity-biometric-gating",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@humanity-org/connect-sdk": "latest",
    "react": "^19.2.0",
    "react-dom": "^19.2.0",
    "react-router-dom": "^7.0.0"
  },
  "devDependencies": {
    "@types/node": "^24.0.0",
    "@types/react": "^19.0.0",
    "@types/react-dom": "^19.0.0",
    "@vitejs/plugin-react": "^5.0.0",
    "typescript": "~5.9.0",
    "vite": "^7.0.0",
    "vite-plugin-node-polyfills": "^0.22.0"
  }
}
```

Install dependencies:

```bash
bun install
```

{% hint style="info" %}
The Connect SDK uses Node.js APIs (crypto, buffer, stream) internally.&#x20;

In browser environments, these must be polyfilled. The `vite-plugin-node-polyfills` package handles this automatically.
{% endhint %}

Create `vite.config.ts`:

```typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { nodePolyfills } from 'vite-plugin-node-polyfills'

export default defineConfig({
  plugins: [
    react(),
    nodePolyfills({
      include: ['crypto', 'buffer', 'stream', 'util'],
      globals: {
        Buffer: true,
        process: true,
      },
    }),
  ],
})
```

Create `tsconfig.json`:

```json
{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ]
}
```

Create `tsconfig.app.json`:

```json
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "target": "ES2022",
    "useDefineForClassFields": true,
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "types": ["vite/client"],
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"]
}
```

Create `tsconfig.node.json`:

```json
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
    "target": "ES2023",
    "lib": ["ES2023"],
    "module": "ESNext",
    "types": ["node"],
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "moduleDetection": "force",
    "noEmit": true,
    "strict": true
  },
  "include": ["vite.config.ts"]
}
```

Create `index.html` at the project root:

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Biometric Access with Humanity</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
```

## 02. Configure the Developer Portal

Go to [developers.humanity.org](https://developers.humanity.org), open your application, and switch to the **Sandbox** tab in Settings.

* Add `http://localhost:5173/oauth/callback` as a redirect URI
* Under scopes, enable:
  * **OpenID Connect** (`openid`) — required for the OAuth flow
  * **Identity Information** (`identity:read`) — includes ***Palm Verified*** credential

## 03. Generate a sandbox credential

Before testing the flow, generate ***a mock Palm Vein Biometric Scan credential*** in the Sandbox Dashboard.

1. Go to [app.sandbox.humanity.org](https://app.sandbox.humanity.org) and sign in
2. Open the **Create** tab
3. Search for **Palm Vein** and select it

## 04. Set up environment variables

Create `.env.local` at the project root:

```bash
VITE_HUMANITY_CLIENT_ID=your_client_id_here
VITE_HUMANITY_REDIRECT_URI=http://localhost:5173/oauth/callback
VITE_HUMANITY_ENVIRONMENT=sandbox
```

Get these values from the Developer Portal under your application's sandbox settings.

{% hint style="warning" %}
Don't commit `.env.local`. The `.gitignore` step above prevents this, but double-check before pushing.
{% endhint %}

## 05. Build the app

Create the folder structure:

<pre><code>src/
<strong>├── components/
</strong>│   ├── Button.tsx
│   └── LoginButton.tsx
├── hooks/
│   └── useAuth.tsx
├── lib/
│   └── humanity.ts
├── pages/
│   ├── Dashboard.tsx
│   ├── Home.tsx
│   └── OAuthCallback.tsx
├── App.tsx
├── main.tsx
├── index.css
└── ...
</code></pre>

### SDK initialization — `src/lib/humanity.ts`

Initialize the Connect SDK with your credentials. Store and retrieve the PKCE code verifier in session storage.

```typescript
import { HumanitySDK } from '@humanity-org/connect-sdk'

export const sdk = new HumanitySDK({
  clientId: import.meta.env.VITE_HUMANITY_CLIENT_ID,
  redirectUri: import.meta.env.VITE_HUMANITY_REDIRECT_URI,
  environment: import.meta.env.VITE_HUMANITY_ENVIRONMENT,
})

const CODE_VERIFIER_KEY = 'humanity_code_verifier'

export function storeCodeVerifier(codeVerifier: string) {
  sessionStorage.setItem(CODE_VERIFIER_KEY, codeVerifier)
}

export function getCodeVerifier() {
  return sessionStorage.getItem(CODE_VERIFIER_KEY)
}

export function clearCodeVerifier() {
  sessionStorage.removeItem(CODE_VERIFIER_KEY)
}
```

### Auth context — `src/hooks/useAuth.tsx`

This context holds your access token, credentials, and metadata. Session storage keeps everything across page reloads during testing.

```typescript
import { createContext, useContext, useState } from 'react'
import type { ReactNode } from 'react'

interface AuthState {
  accessToken: string | null
  isAuthenticated: boolean
  presets: any[] | null
  tokenData: any | null
}

interface AuthContextType extends AuthState {
  setAuth: (token: string, presets: any[], tokenData: any) => void
  logout: () => void
}

const AuthContext = createContext<AuthContextType | null>(null)

export function AuthProvider({ children }: { children: ReactNode }) {
  const [auth, setAuthState] = useState<AuthState>({
    accessToken: sessionStorage.getItem('humanity_access_token'),
    isAuthenticated: !!sessionStorage.getItem('humanity_access_token'),
    presets: JSON.parse(
      sessionStorage.getItem('humanity_presets') || 'null',
    ) as any[] | null,
    tokenData: JSON.parse(
      sessionStorage.getItem('humanity_token_data') || 'null',
    ),
  })

  const setAuth = (token: string, presets: any[], tokenData: any) => {
    sessionStorage.setItem('humanity_access_token', token)
    sessionStorage.setItem('humanity_presets', JSON.stringify(presets))
    sessionStorage.setItem('humanity_token_data', JSON.stringify(tokenData))
    setAuthState({
      accessToken: token,
      isAuthenticated: true,
      presets,
      tokenData,
    })
  }

  const logout = () => {
    sessionStorage.removeItem('humanity_access_token')
    sessionStorage.removeItem('humanity_presets')
    sessionStorage.removeItem('humanity_token_data')
    setAuthState({
      accessToken: null,
      isAuthenticated: false,
      presets: null,
      tokenData: null,
    })
  }

  return (
    <AuthContext.Provider value={{ ...auth, setAuth, logout }}>
      {children}
    </AuthContext.Provider>
  )
}

export function useAuth() {
  const context = useContext(AuthContext)
  if (!context) throw new Error('useAuth must be used within AuthProvider')
  return context
}
```

### Button component — `src/components/Button.tsx`

A reusable button with multiple style variants.

```typescript
import type { ReactNode } from 'react'

interface ButtonProps {
  onClick?: () => void
  children: ReactNode
  variant?: 'primary' | 'outline' | 'ghost'
  className?: string
}

export function Button({
  onClick,
  children,
  variant = 'primary',
  className = '',
}: ButtonProps) {
  const baseStyles = 'px-6 py-2 font-semibold transition-colors'

  const variantStyles = {
    primary:
      'bg-primary text-primary-foreground rounded-md hover:bg-primary/90',
    outline: 'border border-border rounded-md hover:bg-muted',
    ghost: 'rounded-md hover:bg-muted hover:text-foreground',
  }

  return (
    <button
      onClick={onClick}
      className={`${baseStyles} ${variantStyles[variant]} ${className}`}
    >
      {children}
    </button>
  )
}
```

### Login button — `src/components/LoginButton.tsx`&#x20;

This generates the SDK auth URL with PKCE and stores the code verifier before redirecting to Humanity.

```typescript
import { sdk, storeCodeVerifier } from '../lib/humanity'
import { Button } from './Button'

export function LoginButton() {
  const handleLogin = () => {
    const { url, codeVerifier } = sdk.buildAuthUrl({
      scopes: ['openid', 'identity:read'],
    })

    storeCodeVerifier(codeVerifier)
    window.location.href = url
  }

  return (
    <Button className="verify-button" onClick={handleLogin}>
      Verify
    </Button>
  )
}
```

### Home page — `src/pages/Home.tsx`&#x20;

Display the verification card and redirect authenticated users to the dashboard.

```typescript
import { LoginButton } from '../components/LoginButton'
import { useAuth } from '../hooks/useAuth'
import { Navigate } from 'react-router-dom'

const HUMANITY_LOGO = (
  <svg
    className="humanity-logo"
    width="680"
    height="169"
    viewBox="0 0 680 169"
    fill="none"
    xmlns="http://www.w3.org/2000/svg"
    role="img"
    aria-label="Humanity"
  >
    <path
      d="M119.308 136.821C114.908 140.185 110.049 142.794 104.862 144.599L104.451 144.747C102.613 145.37 100.528 144.697 99.3786 143.089L96.9323 139.676C95.537 137.707 93.6822 136.673 91.3016 136.492C90.2184 136.411 89.1842 136.378 87.9034 136.378C86.7707 136.378 85.7037 136.46 84.6366 136.591C82.4864 136.854 80.7461 137.904 79.4982 139.725L76.642 143.877C75.526 145.501 73.4079 146.223 71.5203 145.6C66.8086 144.057 62.0808 141.497 56.647 137.559C55.6456 136.837 54.2338 136.968 53.5115 137.871C52.9041 138.626 52.8877 139.709 53.4458 140.496C56.6306 145.009 59.8482 149.522 62.9837 153.886L68.5978 161.73C71.6516 166.03 75.6901 168.31 80.943 168.721C83.258 168.901 85.4739 168.983 87.6409 168.983C90.5962 168.983 93.4357 168.819 96.2269 168.475C100.922 167.9 104.698 165.603 107.456 161.632C109.426 158.793 111.412 155.888 113.332 153.083L114.219 151.803C116.369 148.685 118.553 145.485 120.736 142.351C121.344 141.481 121.934 140.612 122.542 139.758C122.821 139.364 122.953 138.905 122.953 138.446C122.953 137.789 122.624 137.182 122.099 136.772C121.327 136.164 120.162 136.197 119.325 136.837L119.308 136.821Z"
      fill="white"
    />
    <path
      d="M42.7303 126.877C38.1829 123.742 34.1938 119.918 30.8777 115.554L30.615 115.193C29.4494 113.634 29.4494 111.435 30.615 109.86L29.9912 109.4L30.7628 109.647L33.1103 106.48C34.5385 104.543 34.9653 102.459 34.4072 100.129C34.161 99.0789 33.8654 98.0779 33.455 96.8633C33.0939 95.8134 32.6835 94.7957 32.2402 93.8275C31.3209 91.8589 29.7942 90.5459 27.6601 89.9058L22.8173 88.4783C20.913 87.9205 19.5833 86.1315 19.5997 84.1301C19.5997 79.1907 20.5847 73.9065 22.6695 67.5068C23.0471 66.3257 22.4889 65.0294 21.4054 64.6357C20.5026 64.2909 19.4683 64.6029 18.8938 65.3736C15.5284 69.8703 12.1795 74.4154 8.94547 78.797L8.81418 78.9611C7.00834 81.4224 5.18615 83.8837 3.36394 86.329C0.228425 90.562 -0.707305 95.1077 0.523919 100.211C1.82081 105.544 3.4296 110.27 5.46523 114.668C7.45157 118.967 10.817 121.839 15.4463 123.233C18.7624 124.218 22.1278 125.219 25.3947 126.187L26.593 126.548C30.3195 127.647 34.1445 128.78 37.8875 129.912L38.1173 129.174L38.068 129.961C39.0202 130.257 39.9887 130.536 40.9245 130.831C41.1379 130.897 41.3677 130.929 41.5975 130.929C41.8438 130.929 42.0736 130.897 42.3034 130.814C42.9273 130.601 43.4033 130.109 43.6496 129.485C43.9779 128.567 43.6003 127.467 42.7303 126.877Z"
      fill="white"
    />
    <path
      d="M56.7189 30.8432C58.3113 29.3664 59.0828 27.4957 59.0336 25.2804L58.9023 20.2427C58.853 18.2572 60.1335 16.4522 62.0376 15.8286C66.7326 14.3189 72.0351 13.6133 78.7168 13.6133H78.7986C80.0465 13.6133 81.0974 12.6779 81.1464 11.5129C81.1959 10.5448 80.5716 9.65867 79.6526 9.34685C74.6294 7.64026 69.5727 5.95011 64.648 4.30917L63.5977 3.96457C60.6917 2.99642 57.8024 2.02826 54.8967 1.06011C49.8897 -0.613652 45.2767 -0.0885504 40.8115 2.65182C36.1328 5.52347 32.1437 8.52637 28.5978 11.8247C25.1339 15.0409 23.4266 19.1269 23.5416 23.9676C23.6236 27.43 23.7221 30.9416 23.8042 34.3384L23.837 35.684C23.9355 39.5402 24.0505 43.4784 24.1325 47.3839C24.1489 48.4505 24.1818 49.5007 24.1818 50.5509C24.1818 51.0268 24.3459 51.4862 24.6251 51.8637C25.019 52.3887 25.6428 52.7005 26.2995 52.717H26.3652C27.3173 52.717 28.2038 52.0114 28.4993 51.0268C30.0917 45.7265 32.4721 40.7545 35.706 36.1106L35.8538 35.8973C36.9865 34.3056 39.0714 33.6328 40.9264 34.2563L41.1726 33.5343V34.3384L44.9156 35.5855C47.1975 36.3568 49.2988 36.1106 51.3508 34.8471C52.2701 34.2891 53.173 33.682 54.158 32.9436C55.0773 32.2544 55.8981 31.5816 56.6697 30.8432H56.7189Z"
      fill="white"
    />
    <path
      d="M140.16 35.0581C143.065 39.062 145.38 43.9027 147.449 50.3024C147.777 51.3034 148.68 51.9598 149.632 51.9598C149.813 51.9598 149.993 51.9434 150.174 51.8941C151.11 51.6315 151.766 50.7783 151.766 49.8101C151.848 44.4114 151.881 38.9799 151.93 33.7125L152.012 23.7192C152.061 18.4517 150.141 14.2181 146.135 10.8213C141.965 7.2605 137.878 4.38886 133.643 2.04231C129.506 -0.271416 125.09 -0.616013 120.526 0.99211C117.259 2.14077 113.943 3.30584 110.742 4.45449L109.543 4.88114C105.883 6.17748 102.14 7.50663 98.4294 8.80298C97.4279 9.1476 96.4265 9.49217 95.4418 9.83679C94.9823 9.98449 94.6045 10.2798 94.3252 10.6573C93.948 11.1988 93.8496 11.8715 94.0298 12.5279C94.3091 13.4633 95.212 14.1032 96.2624 14.1032H96.328C101.877 13.972 107.327 14.7104 112.695 16.3349L113.007 16.4334C114.862 17.0077 116.143 18.7963 116.126 20.7491L116.093 24.9498C116.077 27.362 116.947 29.2819 118.786 30.8408C119.607 31.5464 120.444 32.1864 121.461 32.9248C122.381 33.5648 123.3 34.1555 124.236 34.6643C126.14 35.7144 128.159 35.8785 130.244 35.1565L135.005 33.4663C136.86 32.81 138.994 33.4663 140.16 35.0909V35.0581Z"
      fill="white"
    />
    <path
      d="M172.351 86.0602C170.25 83.3197 168.116 80.5303 166.031 77.8226L164.324 75.6076C162.255 72.916 160.154 70.1761 158.102 67.4684L157.938 67.2548C157.347 66.4835 156.772 65.7128 156.197 64.9414C155.919 64.5477 155.525 64.2846 155.065 64.1534C154.441 63.9565 153.768 64.0717 153.193 64.4487C152.389 64.9903 152.06 66.1065 152.405 67.1075C154.244 72.3254 155.229 77.7409 155.344 83.238V83.6806C155.377 85.617 154.08 87.3893 152.208 87.9966L148.203 89.2601C145.904 89.9819 144.345 91.4094 143.425 93.625C143.015 94.6093 142.654 95.5942 142.26 96.8249C141.932 97.9076 141.669 98.9581 141.456 99.9919C141.045 102.125 141.505 104.094 142.851 105.85L145.921 109.854C147.136 111.429 147.152 113.644 145.97 115.253C143.064 119.257 139.174 122.949 133.724 126.903C132.722 127.642 132.426 129.02 133.067 129.972C133.477 130.595 134.167 130.94 134.889 130.94C135.102 130.94 135.316 130.907 135.529 130.842C140.897 129.184 146.282 127.478 151.486 125.837L151.683 125.771C154.589 124.852 157.511 123.933 160.433 123.014C165.457 121.439 168.888 118.305 170.89 113.448C172.975 108.377 174.453 103.618 175.388 98.8597C176.291 94.2156 175.273 89.9163 172.335 86.0763L172.351 86.0602Z"
      fill="white"
    />
    <path
      d="M228.455 108.788V51.0625C228.455 48.9257 230.19 47.1914 232.328 47.1914H239.268C241.405 47.1914 243.14 48.9257 243.14 51.0625V78.6554H243.915C245.464 70.3559 251.257 62.7379 263.774 62.7379C276.972 62.7379 283.416 71.347 283.416 82.6811V108.819C283.416 110.955 281.681 112.69 279.544 112.69H272.604C270.466 112.69 268.731 110.955 268.731 108.819V87.0788C268.731 78.8721 265.199 75.9304 256.029 75.9304C246.146 75.9304 243.109 79.9251 243.109 88.1318V108.788C243.109 110.925 241.374 112.659 239.237 112.659H232.328C230.19 112.659 228.455 110.925 228.455 108.788Z"
      fill="white"
    />
    <path
      d="M330.012 97.7101H329.237C327.967 105.917 322.019 113.628 309.099 113.628C295.312 113.628 288.775 105.143 288.775 94.1797V67.5779C288.775 65.441 290.51 63.707 292.648 63.707H299.588C301.726 63.707 303.46 65.441 303.46 67.5779V89.3177C303.46 97.2458 306.776 100.56 316.07 100.56C325.365 100.56 329.082 96.8434 329.082 88.5438V67.5779C329.082 65.441 330.817 63.707 332.955 63.707H339.895C342.033 63.707 343.767 65.441 343.767 67.5779V108.828C343.767 110.965 342.033 112.699 339.895 112.699H333.915C331.778 112.699 330.043 110.965 330.043 108.828V97.741"
      fill="white"
    />
    <path
      d="M349.28 108.781V67.5621C349.28 65.4252 351.015 63.6906 353.152 63.6906H359.008C361.146 63.6906 362.881 65.4252 362.881 67.5621V78.5558H363.655C364.832 70.3491 369.727 62.7305 382.027 62.7305C393.366 62.7305 399.129 69.6675 400.213 78.7415H401.081C402.258 70.4419 407.246 62.7305 419.856 62.7305C432.465 62.7305 438.63 71.03 438.63 82.1791V108.812C438.63 110.949 436.896 112.683 434.758 112.683H427.818C425.681 112.683 423.945 110.949 423.945 108.812V87.072C423.945 78.9582 421.126 75.9236 412.699 75.9236C403.807 75.9236 401.267 79.454 401.267 87.9393V108.812C401.267 110.949 399.532 112.683 397.394 112.683H390.454C388.317 112.683 386.582 110.949 386.582 108.812V87.072C386.582 78.9582 383.762 75.9236 375.335 75.9236C366.444 75.9236 363.903 79.454 363.903 87.9393V108.812C363.903 110.949 362.168 112.683 360.03 112.683H353.09C350.952 112.683 349.218 110.949 349.218 108.812L349.28 108.781Z"
      fill="white"
    />
    <path
      d="M444.211 100.547C444.211 93.3001 449.602 88.8102 460.043 87.7572L481.048 85.6202V83.7619C481.048 77.4135 478.229 75.6486 470.391 75.6486C462.552 75.6486 459.919 77.5992 459.919 83.1738V83.5762H445.172V83.2976C445.172 71.189 455.334 62.7656 471.475 62.7656C487.617 62.7656 495.517 71.1581 495.517 83.9792V108.847C495.517 110.983 493.782 112.718 491.644 112.718H485.665C483.527 112.718 481.792 110.983 481.792 108.847V101.476H481.017C478.756 109.095 471.94 113.678 461.065 113.678C450.191 113.678 444.149 108.784 444.149 100.578L444.211 100.547ZM465.31 103.272C467.665 103.272 469.771 103.149 471.63 102.838C476.587 102.065 475.503 94.6323 470.515 95.1895L464.04 95.933C460.508 96.2115 458.958 97.2026 458.958 99.5562C458.958 102.188 461.003 103.272 465.31 103.272Z"
      fill="white"
    />
    <path
      d="M501.001 108.781V67.5621C501.001 65.4252 502.735 63.6906 504.873 63.6906H510.729C512.867 63.6906 514.601 65.4252 514.601 67.5621V78.7415H515.469C516.739 70.5348 522.595 62.7305 535.421 62.7305C548.248 62.7305 555.156 71.3402 555.156 82.1791V108.812C555.156 110.949 553.421 112.683 551.284 112.683H544.344C542.206 112.683 540.471 110.949 540.471 108.812V87.072C540.471 79.2677 537.249 75.9236 528.264 75.9236C519.28 75.9236 515.655 79.6398 515.655 87.9393V108.812C515.655 110.949 513.92 112.683 511.782 112.683H504.842C502.704 112.683 500.97 110.949 500.97 108.812L501.001 108.781Z"
      fill="white"
    />
    <path
      d="M560.642 55.678V52.9218C560.642 50.785 562.377 49.0508 564.515 49.0508H571.454C573.592 49.0508 575.327 50.785 575.327 52.9218V55.678C575.327 57.8149 573.592 59.5491 571.454 59.5491H564.515C562.377 59.5491 560.642 57.8149 560.642 55.678ZM560.642 108.789V67.57C560.642 65.433 562.377 63.6991 564.515 63.6991H571.454C573.592 63.6991 575.327 65.433 575.327 67.57V108.82C575.327 110.957 573.592 112.691 571.454 112.691H564.515C562.377 112.691 560.642 110.957 560.642 108.82V108.789Z"
      fill="white"
    />
    <path
      d="M605.405 112.66C593.972 112.66 587.032 107.271 587.032 94.9767V79.6782C587.032 77.5412 585.297 75.8073 583.16 75.8073C581.022 75.8073 579.287 74.0727 579.287 71.9364V67.5386C579.287 65.4017 581.022 63.6677 583.16 63.6677C585.297 63.6677 587.032 61.9332 587.032 59.7965V58.0312C587.032 55.8944 588.767 54.1602 590.905 54.1602H597.845C599.983 54.1602 601.718 55.8944 601.718 58.0312V59.7965C601.718 61.9332 603.452 63.6677 605.59 63.6677H613.708C615.845 63.6677 617.58 65.4017 617.58 67.5386V71.9364C617.58 74.0727 615.845 75.8073 613.708 75.8073H605.59C603.452 75.8073 601.718 77.5412 601.718 79.6782V93.3046C601.718 98.1976 603.576 99.4673 608.75 99.4673H613.677C615.814 99.4673 617.549 101.201 617.549 103.338V108.82C617.549 110.957 615.814 112.691 613.677 112.691H605.405V112.66Z"
      fill="white"
    />
    <path
      d="M627.47 125.415V119.841C627.47 117.704 629.205 115.97 631.341 115.97H638.345C639.213 115.97 639.987 115.908 640.637 115.784C643.114 115.35 644.384 112.563 643.3 110.302L623.284 69.207C622.045 66.6367 623.903 63.6641 626.758 63.6641H634.716C636.234 63.6641 637.596 64.5307 638.215 65.8939L647.48 85.8681C647.48 85.8681 647.573 86.0854 647.604 86.2092L651.667 97.5743H652.658L656.528 86.0854C656.528 86.0854 656.59 85.899 656.621 85.8371L664.864 66.0486C665.453 64.6242 666.877 63.6641 668.425 63.6641H676.142C678.96 63.6641 680.818 66.5748 679.641 69.1451L657.922 116.156C653.246 126.406 646.675 129.255 634.376 129.255H631.372C629.235 129.255 627.501 127.521 627.501 125.384L627.47 125.415Z"
      fill="white"
    />
  </svg>
)

export function Home() {
  const { isAuthenticated } = useAuth()

  if (isAuthenticated) {
    return <Navigate to="/dashboard" />
  }

  return (
    <div className="home-screen">
      {/* Humanity logo SVG here */}
      <div className="verification-card verification-card-surface rounded-xl border border-border p-8 space-y-8">
        <div className="verification-copy text-center">
          <p className="home-primary-copy text-muted-foreground">
            Biometric gating flow using{' '}
            <span className="font-mono text-green-400">connect-sdk</span>
          </p>
          <p className="home-tagline text-muted-foreground">
            Requested scopes: <span className="font-mono text-green-400">openid</span> and{' '}
            <span className="font-mono text-green-400">identity:read</span>
          </p>
        </div>
        <div className="verification-note text-center">
          <div className="text-xs font-semibold text-muted-foreground">
            Implementation note
          </div>
          <p className="text-sm text-muted-foreground">
            Request the preset as <span className="font-mono text-green-400">is_human</span>, but read
            the SDK response using <span className="font-mono text-green-400">isHuman</span> or{' '}
            <span className="font-mono text-green-400">presetName</span>: <span className="font-mono text-green-400">is_human</span>.
          </p>
        </div>
        <div className="flex justify-center">
          <LoginButton />
        </div>
      </div>
    </div>
  )
}
```

### OAuth callback — `src/pages/OAuthCallback.tsx`&#x20;

This page handles the redirect back from Humanity. It exchanges your authorization code for an access token and checks for the `is_human` credential.

```typescript
import { useEffect, useState } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { sdk, getCodeVerifier, clearCodeVerifier } from '../lib/humanity'
import { useAuth } from '../hooks/useAuth'

export function OAuthCallback() {
  const [searchParams] = useSearchParams()
  const navigate = useNavigate()
  const { setAuth } = useAuth()
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    async function handleCallback() {
      const oauthError = searchParams.get('error')
      const errorDescription = searchParams.get('error_description')

      if (oauthError) {
        setError(
          `OAuth Error: ${oauthError}${errorDescription ? ` — ${errorDescription}` : ''}`,
        )
        return
      }

      const code = searchParams.get('code')
      const codeVerifier = getCodeVerifier()

      if (!code) {
        setError('Missing authorization code from callback URL')
        return
      }

      if (!codeVerifier) {
        setError('Missing PKCE code verifier from session storage')
        return
      }

      try {
        const token = await sdk.exchangeCodeForToken({
          code,
          codeVerifier,
        })

        const verification = await sdk.verifyPresets({
          accessToken: token.accessToken,
          presets: ['is_human'],
        })

        setAuth(token.accessToken, verification.results, token)
        clearCodeVerifier()
        navigate('/dashboard')
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Authentication failed')
      }
    }

    handleCallback()
  }, [searchParams, navigate, setAuth])

  if (error) {
    return (
      <div className="min-h-screen flex items-center justify-center p-4">
        <div className="w-full max-w-md space-y-4">
          <div className="rounded-md border border-red-400/50 bg-red-400/10 p-4">
            <h2 className="text-sm font-semibold text-red-400 mb-1">
              Authentication Error
            </h2>
            <p className="text-sm text-muted-foreground">{error}</p>
          </div>
          <a
            href="/"
            className="block text-sm text-center text-muted-foreground hover:text-foreground transition-colors"
          >
            Try again
          </a>
        </div>
      </div>
    )
  }

  return (
    <div className="min-h-screen flex items-center justify-center">
      <p className="text-sm text-muted-foreground">
        Processing authentication...
      </p>
    </div>
  )
}
```

### Dashboard — `src/pages/Dashboard.tsx`&#x20;

Display the biometric status. The `is_human` credential can be `true`, `false`, or missing—the dashboard shows all three.

```typescript
import { Navigate } from 'react-router-dom'
import { useAuth } from '../hooks/useAuth'
import { Button } from '../components/Button'

function normalizeCredentialBoolean(value: unknown): boolean | null {
  if (typeof value === 'boolean') return value
  if (typeof value === 'string') {
    const normalized = value.trim().toLowerCase()
    if (normalized === 'true') return true
    if (normalized === 'false') return false
  }
  if (typeof value === 'number') {
    if (value === 1) return true
    if (value === 0) return false
  }
  return null
}

export function Dashboard() {
  const { isAuthenticated, logout, tokenData, presets } = useAuth()
  const environment = import.meta.env.VITE_HUMANITY_ENVIRONMENT || 'sandbox'

  if (!isAuthenticated) {
    return <Navigate to="/" />
  }

  const expiresIn = tokenData?.expiresIn
    ? Math.floor(tokenData.expiresIn / 60)
    : 0
  const authorizationId =
    tokenData?.authorizationId || tokenData?.authorization_id || '—'
  const isHumanPreset = presets?.find((preset: any) => {
    const presetKey = preset?.preset
    const presetName = preset?.presetName

    return (
      presetKey === 'is_human' ||
      presetKey === 'isHuman' ||
      presetName === 'is_human'
    )
  })
  const credentialValue = normalizeCredentialBoolean(isHumanPreset?.value)
  const hasCredential = isHumanPreset?.status === 'valid' && credentialValue !== null
  const isHumanValid = hasCredential && credentialValue === true
  const statusLabel = isHumanValid ? 'Human verified' : 'Not verified'
  const statusDescription = isHumanValid
    ? 'Biometric credential is valid and access is granted.'
    : credentialValue === false
      ? 'Biometric credential was found, but the is_human value is false.'
      : 'Biometric credential is missing or could not be evaluated for gated access.'
  const statusClassName = isHumanValid
    ? 'biometric-status-card biometric-status-card-valid'
    : 'biometric-status-card biometric-status-card-invalid'
  const statusDotClassName = isHumanValid
    ? 'status-dot status-dot-valid'
    : 'status-dot status-dot-invalid'
  const statusBadgeClassName = isHumanValid
    ? 'status-badge status-badge-valid'
    : 'status-badge status-badge-invalid'
  const valueLabel =
    credentialValue === null ? '—' : credentialValue ? 'true' : 'false'

  return (
    <div className="min-h-screen flex items-center justify-center p-6">
      <div className="dashboard-shell space-y-6">
        <div className="flex items-center justify-between">
          <div className="flex items-center">
            <div className="w-2 h-2 bg-green-400 rounded-full mr-2"></div>
            <h2 className="text-sm font-semibold">Biometric Access</h2>
          </div>
          <Button variant="ghost" onClick={logout}>
            Sign out
          </Button>
        </div>

        <div className={statusClassName}>
          <div className="status-header">
            <div className="flex items-center">
              <span className={statusDotClassName}></span>
              <div className="space-y-1">
                <div className="text-xs font-medium text-muted-foreground tracking-wider">
                  BIOMETRIC STATUS
                </div>
                <h3 className="text-xl font-semibold">{statusLabel}</h3>
              </div>
            </div>
            <span className={statusBadgeClassName}>{isHumanValid ? 'valid' : 'invalid'}</span>
          </div>

          <p className="text-sm text-muted-foreground">{statusDescription}</p>

          <div className="biometric-value-row">
            <div className="space-y-1">
              <div className="text-xs font-medium text-muted-foreground tracking-wider">
                PRESET
              </div>
              <div className="text-sm font-mono text-foreground">is_human</div>
            </div>
            <div className="space-y-1">
              <div className="text-xs font-medium text-muted-foreground tracking-wider">
                VALUE
              </div>
              <div className="text-sm font-semibold text-foreground">{valueLabel}</div>
            </div>
          </div>

          <div className="dashboard-meta-grid">
            <div className="dashboard-meta-card">
              <div className="text-xs font-medium text-muted-foreground tracking-wider">
                AUTHORIZATION ID
              </div>
              <div className="text-sm font-mono text-foreground credential-text">
                {authorizationId}
              </div>
            </div>
            <div className="dashboard-meta-card">
              <div className="text-xs font-medium text-muted-foreground tracking-wider">
                ACCESS TOKEN EXPIRES IN
              </div>
              <div className="text-sm text-foreground">{expiresIn} min</div>
            </div>
            <div className="dashboard-meta-card">
              <div className="text-xs font-medium text-muted-foreground tracking-wider">
                REQUESTED SCOPE
              </div>
              <div className="text-sm font-mono text-green-400">identity:read</div>
            </div>
            <div className="dashboard-meta-card">
              <div className="text-xs font-medium text-muted-foreground tracking-wider">
                SDK ENVIRONMENT
              </div>
              <div className="text-sm font-mono text-foreground">
                {environment}
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}
```

### App routing — `src/App.tsx`&#x20;

```typescript
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { AuthProvider } from './hooks/useAuth'
import { Home } from './pages/Home'
import { OAuthCallback } from './pages/OAuthCallback'
import { Dashboard } from './pages/Dashboard'

function App() {
  return (
    <AuthProvider>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/oauth/callback" element={<OAuthCallback />} />
          <Route path="/dashboard" element={<Dashboard />} />
        </Routes>
      </BrowserRouter>
    </AuthProvider>
  )
}

export default App
```

### Entry point — `src/main.tsx`

```typescript
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)
```

### Stylesheet — `src/index.css`

The app uses a plain CSS stylesheet — no Tailwind. All component classes (`biometric-status-card`, `dashboard-shell`, `home-screen`, etc.) and the utility classes used throughout the components are defined here.

```css
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

:root {
  --color-background: #080808;
  --color-foreground: #ffffffde;
  --color-muted: #151918;
  --color-muted-2: #212827;
  --color-muted-foreground: #ffffff4d;
  --color-border: #2a3230;
  --color-border-subtle: #ffffff08;
  --color-primary: #7cff4a;
  --color-primary-foreground: #080808;
  --color-primary-muted: #7cff4a1a;
  --color-primary-border: #7cff4a33;
  --color-green: #7cff4a;
  --color-red: #fb2c36;
  --color-red-muted: #fb2c361a;
  --color-red-border: #fb2c3633;
}

body {
  background: linear-gradient(to bottom, #0a0a0a, #050505);
  color: var(--color-foreground);
  font-family: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.min-h-screen {
  min-height: 100vh;
}

.home-screen {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3rem;
  padding: 1rem;
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

.home-screen::before,
.home-screen::after {
  content: '';
  position: fixed;
  border-radius: 9999px;
  pointer-events: none;
  z-index: -1;
  animation: background-pulse 4s ease-in-out infinite;
}

.home-screen::before {
  top: 15%;
  left: 10%;
  width: 18rem;
  height: 18rem;
  background: rgba(124, 255, 74, 0.1);
  filter: blur(100px);
}

.home-screen::after {
  right: 15%;
  bottom: 15%;
  width: 25rem;
  height: 25rem;
  background: rgba(37, 99, 235, 0.05);
  filter: blur(120px);
  animation-delay: 2s;
}

.home-screen > * {
  position: relative;
  z-index: 1;
}

.humanity-logo {
  display: block;
  width: min(300px, 80vw);
  height: auto;
}

.verification-card {
  width: min(100%, 36rem);
}

.verification-card-surface {
  background: rgba(21, 25, 24, 0.82);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

.dashboard-shell {
  width: min(100%, 42rem);
}

.biometric-status-card {
  border-width: 1px;
  border-style: solid;
  border-radius: 16px;
  padding: 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  background: rgba(255, 255, 255, 0.03);
}

.biometric-status-card-valid {
  border-color: var(--color-primary-border);
  background: linear-gradient(180deg, rgba(124, 255, 74, 0.08), rgba(255, 255, 255, 0.03));
  box-shadow: inset 0 1px 0 rgba(124, 255, 74, 0.08);
}

.biometric-status-card-invalid {
  border-color: var(--color-red-border);
  background: linear-gradient(180deg, rgba(251, 44, 54, 0.08), rgba(255, 255, 255, 0.03));
  box-shadow: inset 0 1px 0 rgba(251, 44, 54, 0.08);
}

.status-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
}

.status-dot {
  width: 0.75rem;
  height: 0.75rem;
  border-radius: 9999px;
  margin-right: 0.75rem;
  margin-top: 0.2rem;
  flex-shrink: 0;
}

.status-dot-valid {
  background: var(--color-green);
  box-shadow: 0 0 0 6px rgba(124, 255, 74, 0.12);
}

.status-dot-invalid {
  background: var(--color-red);
  box-shadow: 0 0 0 6px rgba(251, 44, 54, 0.12);
}

.status-badge {
  border-radius: 9999px;
  padding: 0.3rem 0.7rem;
  font-size: 0.75rem;
  line-height: 1rem;
  font-weight: 600;
  text-transform: uppercase;
}

.status-badge-valid {
  background: var(--color-primary-muted);
  color: var(--color-green);
}

.status-badge-invalid {
  background: var(--color-red-muted);
  color: var(--color-red);
}

.biometric-value-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1rem;
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: 8px;
  background: rgba(0, 0, 0, 0.12);
}

.dashboard-meta-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.75rem;
}

.dashboard-meta-card {
  padding: 0.9rem 1rem;
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.02);
  min-width: 0;
}

.credential-text {
  overflow-wrap: anywhere;
  word-break: break-word;
}

.verification-copy {
  padding: 0.25rem 0;
}

.home-primary-copy {
  font-size: 1.375rem;
  line-height: 1.9rem;
  font-weight: 600;
}

.home-tagline {
  margin-top: 0.65rem;
  font-size: 0.875rem;
  line-height: 1.4rem;
}

.verification-note {
  padding: 1rem;
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.025);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
  text-align: left;
}

.verification-note > * + * {
  margin-top: 0.4rem;
}

.verify-button {
  min-width: 14rem;
  padding: 0.875rem 2rem;
  font-size: 1rem;
  line-height: 1.5rem;
  border-radius: 18px;
}

.flex {
  display: flex;
}
.items-center {
  align-items: center;
}
.justify-center {
  justify-content: center;
}
.justify-between {
  justify-content: space-between;
}
.text-center {
  text-align: center;
}
.w-full {
  width: 100%;
}
.w-3\/4 {
  width: 75%;
}
.max-w-md {
  max-width: 28rem;
}
.max-w-lg {
  max-width: 32rem;
}
.max-w-5xl {
  max-width: 64rem;
}
.p-4 {
  padding: 1rem;
}
.p-3 {
  padding: 0.75rem;
}
.p-5 {
  padding: 1.25rem;
}
.p-6 {
  padding: 1.5rem;
}
.px-4 {
  padding-left: 1rem;
  padding-right: 1rem;
}
.px-6 {
  padding-left: 1.5rem;
  padding-right: 1.5rem;
}
.p-8 {
  padding: 2rem;
}
.py-2 {
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
}
.py-1 {
  padding-top: 0.25rem;
  padding-bottom: 0.25rem;
}
.px-2 {
  padding-left: 0.5rem;
  padding-right: 0.5rem;
}
.mx-auto {
  margin-left: auto;
  margin-right: auto;
}
.mb-4 {
  margin-bottom: 1rem;
}
.mr-2 {
  margin-right: 0.5rem;
}

.space-y-1 > * + * {
  margin-top: 0.25rem;
}
.space-y-2 > * + * {
  margin-top: 0.5rem;
}
.space-y-3 > * + * {
  margin-top: 0.75rem;
}
.space-y-4 > * + * {
  margin-top: 1rem;
}
.space-y-5 > * + * {
  margin-top: 1.25rem;
}
.space-y-6 > * + * {
  margin-top: 1.5rem;
}
.space-y-8 > * + * {
  margin-top: 2rem;
}
.gap-2 {
  gap: 0.5rem;
}

@media (max-width: 640px) {
  .status-header,
  .biometric-value-row {
    flex-direction: column;
    align-items: flex-start;
  }

  .dashboard-meta-grid {
    grid-template-columns: 1fr;
  }
}

.text-3xl {
  font-size: 1.875rem;
  line-height: 1.2;
}
.text-2xl {
  font-size: 1.5rem;
  line-height: 2rem;
}
.text-xl {
  font-size: 1.25rem;
  line-height: 1.75rem;
}
.text-sm {
  font-size: 0.875rem;
  line-height: 1.25rem;
}
.text-xs {
  font-size: 0.75rem;
  line-height: 1rem;
}

.font-semibold {
  font-weight: 600;
}
.font-medium {
  font-weight: 500;
}
.font-mono {
  font-family: ui-monospace, 'Courier New', monospace;
}
.tracking-tight {
  letter-spacing: -0.025em;
}
.tracking-wider {
  letter-spacing: 0.05em;
}

.text-muted-foreground {
  color: var(--color-muted-foreground);
}
.text-red-400 {
  color: var(--color-red);
}
.text-green-400 {
  color: var(--color-green);
}
.text-foreground {
  color: var(--color-foreground);
}
.text-primary-foreground {
  color: var(--color-primary-foreground);
}

.rounded-md {
  border-radius: 0.375rem;
}
.rounded-xl {
  border-radius: 0.75rem;
}
.rounded-2xl {
  border-radius: 1rem;
}
.rounded-full {
  border-radius: 9999px;
}
.border {
  border-width: 1px;
  border-style: solid;
}
.border-b-2 {
  border-bottom-width: 2px;
}
.border-border {
  border-color: var(--color-border);
}
.border-red-400\/50 {
  border-color: var(--color-red-border);
}
.border-primary {
  border-color: var(--color-primary-border);
}

.bg-primary {
  background-color: var(--color-primary);
}
.bg-muted {
  background-color: var(--color-muted);
}
.bg-muted-2 {
  background-color: var(--color-muted-2);
}
.bg-red-400\/10 {
  background-color: var(--color-red-muted);
}
.bg-green-400 {
  background-color: var(--color-green);
}
.bg-green-500\/20 {
  background-color: var(--color-primary-muted);
}

.transition-colors {
  transition-property: color, background-color, border-color;
  transition-duration: 150ms;
}
.hover\:bg-primary\/90:hover {
  background-color: #6ce63e;
}
.hover\:bg-muted:hover {
  background-color: var(--color-muted);
}
.hover\:text-foreground:hover {
  color: var(--color-foreground);
}

.block {
  display: block;
}
.h-8 {
  height: 2rem;
}
.w-8 {
  width: 2rem;
}
.w-2 {
  width: 0.5rem;
}
.h-2 {
  height: 0.5rem;
}

.animate-spin {
  animation: spin 1s linear infinite;
}

@keyframes spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@keyframes background-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}

button {
  cursor: pointer;
  border: none;
  outline: none;
}
a {
  color: inherit;
  text-decoration: none;
}

```

## 06. Run and test

Start the dev server:

```bash
bun dev
```

Open `http://localhost:5173`. You should see the home page with a "Verify" button.

1. Click **Verify**
2. Sign in with your sandbox account (the one with the mock `is_human` credential)
3. You'll land on the dashboard showing **Human verified**

If you test without a credential, the dashboard shows **Not verified** with value `false` or missing.

<figure><img src="https://383350980-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FL1EBectYBMnvu9vU5N1M%2Fuploads%2Fd5jZgfwlDPDOlCkwqZG3%2Fhumanity-connect-sdk-vite-biometric-quick-start-success.png?alt=media&#x26;token=0ef05f34-566d-40c5-8910-bf04b3ac453b" alt=""><figcaption></figcaption></figure>

## How it works

The whole flow comes down to three SDK methods:

1. **`buildAuthUrl()`** — creates an authorization URL with PKCE. Store the code verifier in session storage.
2. **`exchangeCodeForToken()`** — swaps the authorization code for an access token using the stored verifier.
3. **`verifyPresets()`** — checks which credentials the user has. In this case, just `is_human`.

The app keeps tokens and credential data in session storage. On the dashboard, it pulls out the `is_human` through `isHuman` mapping value and displays the result.

## Next Steps

Ready to build a more complex application?&#x20;

The [Personalized Newsletter App](https://docs.humanity.org/developer-guides-and-tutorials/sdk-api-guides/personalized-newsletter-app-reference-implementation) takes everything you built here and moves it to a full-stack Next.js setup — tokens handled server-side, `clientSecret` kept out of the browser, and session storage replaced with HTTP-only cookies.&#x20;

You'll work with batch preset verification, the Query Engine, and MongoDB to build something that actually acts on verified identity: a personalized content feed driven by what Humanity knows about the user.
