Supakeys

Quick Start

Get passkey authentication working in your Supabase project in 5 minutes. This quickstart uses Next.js and Tailwind as an example, but the Supabase setup is the same for all frameworks.

Prerequisites

  • Node.js 18+
  • Supabase CLI installed (npm install -g supabase)
  • A Supabase project (create one here)

1. Install Packages

npm install supakeys @supabase/supabase-js

2. Run the CLI

npx supakeys init

This creates:

  • Database migration in supabase/migrations/
  • Edge function in supabase/functions/passkey-auth/

3. Configure TypeScript (Important!)

If using TypeScript, add this to your tsconfig.json to prevent build errors:
{
  "exclude": ["node_modules", "supabase/functions"]
}
# This will show your available projects to select from.
supabase link

# Alternatively, specify the project ref directly
supabase link --project-ref YOUR_PROJECT_REF

5. Deploy to Supabase

# Apply database migration
supabase db push

# Deploy edge function
supabase functions deploy passkey-auth

6. Verify Deployment (optional)

Find your project URL and anon key in the Supabase dashboard under Settings > API.

curl -X POST 'https://YOUR_PROJECT.supabase.co/functions/v1/passkey-auth' \
  -H 'Content-Type: application/json' \
  -H 'apikey: YOUR_ANON_KEY' \
  -d '{"endpoint":"/register/start","data":{"email":"test@test.com","rpId":"localhost","rpName":"Test"}}'

You should see {"success":true,"data":{...}}

7. Configure Environment Variables

Create a .env.local file in your project root with your Supabase credentials:

NEXT_PUBLIC_SUPABASE_URL=https://YOUR_PROJECT.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

Find these values in your Supabase dashboard under Settings > API.

8. Add to Your App

Initialize the Client

import { createClient } from '@supabase/supabase-js'
import { createPasskeyAuth } from 'supakeys'

const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!)

const passkeys = createPasskeyAuth(supabase, {
  rpId: typeof window !== 'undefined' ? window.location.hostname : 'localhost',
  rpName: 'My App',
})

Check Browser Support (optional)

const support = await passkeys.isSupported()

if (!support.webauthn) {
  console.log('Passkeys not supported')
}

Register a New User

async function register(email: string) {
  const result = await passkeys.register({
    email,
    displayName: 'John Doe',
    authenticatorName: 'My MacBook',
  })

  if (result.success) {
    console.log('Registered!', result.passkey)
  } else {
    console.error('Registration failed:', result.error.message)
  }
}

Sign In

async function signIn() {
  const result = await passkeys.signIn()

  if (result.success) {
    console.log('Signed in!', result.session)
  } else {
    console.error('Sign in failed:', result.error.message)
  }
}

Sign In with Email Hint

async function signInWithEmail(email: string) {
  const result = await passkeys.signIn({ email })

  if (result.success) {
    console.log('Signed in!', result.session)
  }
}

9. Add UI Component

Create lib/supabase.ts:

import { createClient } from '@supabase/supabase-js'
import { createPasskeyAuth } from 'supakeys'

export const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!)

export const passkeys = createPasskeyAuth(supabase, {
  rpId: typeof window !== 'undefined' ? window.location.hostname : 'localhost',
  rpName: 'My App',
})

Create app/page.tsx:

'use client'

import { useState } from 'react'
import { Session } from '@supabase/supabase-js'
import { Passkey } from 'supakeys'
import { supabase, passkeys } from '@/lib/supabase'

export default function Home() {
  const [email, setEmail] = useState('')
  const [session, setSession] = useState<Session | null>(null)
  const [passkeysList, setPasskeysList] = useState<Passkey[]>([])

  async function handleRegister() {
    const result = await passkeys.register({
      email,
      displayName: email,
      authenticatorName: 'My Device',
    })

    if (result.success) {
      alert('Registration successful!')
    } else {
      alert(`Registration failed: ${result.error?.message}`)
    }
  }

  async function handleSignIn(emailHint?: string) {
    const result = await passkeys.signIn({ email: emailHint })

    if (result.success) {
      setSession(result.session ?? null)
      alert('Signed in!')
    } else {
      alert(`Sign in failed: ${result.error?.message}`)
    }
  }

  async function handleCheckSession() {
    const { data } = await supabase.auth.getSession()
    setSession(data.session)
    if (data.session) {
      alert(`Logged in as: ${data.session.user.email}`)
    } else {
      alert('Not logged in')
    }
  }

  async function handleSignOut() {
    await supabase.auth.signOut()
    setSession(null)
    setPasskeysList([])
    alert('Signed out')
  }

  async function handleListPasskeys() {
    const result = await passkeys.listPasskeys()

    if (result.success) {
      setPasskeysList(result.passkeys || [])
    } else {
      alert(`Failed to list passkeys: ${result.error?.message}`)
    }
  }

  async function handleUpdatePasskey(credentialId: string, newName: string) {
    const result = await passkeys.updatePasskey({
      credentialId,
      authenticatorName: newName,
    })

    if (result.success) {
      alert('Passkey updated!')
      handleListPasskeys()
    } else {
      alert(`Update failed: ${result.error?.message}`)
    }
  }

  async function handleRemovePasskey(credentialId: string) {
    const result = await passkeys.removePasskey({ credentialId })

    if (result.success) {
      alert('Passkey removed!')
      handleListPasskeys()
    } else {
      alert(`Remove failed: ${result.error?.message}`)
    }
  }

  return (
    <div className='flex min-h-screen items-center justify-center bg-zinc-50 dark:bg-black'>
      <main className='flex min-h-screen w-full max-w-3xl flex-col gap-4 py-16 px-8'>
        <h1 className='text-2xl font-bold mb-4'>Passkey Authentication</h1>

        <section className='border p-4 rounded-lg'>
          <h2 className='text-lg font-semibold mb-2'>Authentication</h2>
          <input
            type='email'
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder='your@email.com'
            className='border px-3 py-2 rounded-md mr-2 mb-2'
          />
          <div className='flex flex-wrap gap-2'>
            <button onClick={handleRegister} className='bg-blue-500 text-white px-4 py-2 rounded-md'>
              Register
            </button>
            <button onClick={() => handleSignIn(email)} className='bg-green-500 text-white px-4 py-2 rounded-md'>
              Sign In (with email)
            </button>
            <button onClick={() => handleSignIn()} className='bg-green-600 text-white px-4 py-2 rounded-md'>
              Sign In (discoverable)
            </button>
          </div>
        </section>

        <section className='border p-4 rounded-lg'>
          <h2 className='text-lg font-semibold mb-2'>Session</h2>
          <div className='flex flex-wrap gap-2 mb-2'>
            <button onClick={handleCheckSession} className='bg-gray-500 text-white px-4 py-2 rounded-md'>
              Check Session
            </button>
            <button onClick={handleSignOut} className='bg-red-500 text-white px-4 py-2 rounded-md'>
              Sign Out
            </button>
          </div>
          {session && (
            <pre className='bg-gray-100 dark:bg-gray-800 p-2 rounded text-xs overflow-auto'>
              {JSON.stringify(session, null, 2)}
            </pre>
          )}
        </section>

        <section className='border p-4 rounded-lg'>
          <h2 className='text-lg font-semibold mb-2'>Passkey Management (requires login)</h2>
          <button onClick={handleListPasskeys} className='bg-purple-500 text-white px-4 py-2 rounded-md mb-2'>
            List Passkeys
          </button>
          {passkeysList.length > 0 && (
            <div className='space-y-2'>
              {passkeysList.map((pk) => (
                <div key={pk.id} className='bg-gray-100 dark:bg-gray-800 p-2 rounded flex justify-between items-center'>
                  <div>
                    <div className='font-medium'>{pk.authenticatorName || 'Unnamed'}</div>
                    <div className='text-xs text-gray-500'>{pk.id}</div>
                  </div>
                  <div className='flex gap-2'>
                    <button
                      onClick={() => handleUpdatePasskey(pk.id, `Updated ${Date.now()}`)}
                      className='bg-yellow-500 text-white px-2 py-1 rounded text-sm'>
                      Rename
                    </button>
                    <button
                      onClick={() => handleRemovePasskey(pk.id)}
                      className='bg-red-500 text-white px-2 py-1 rounded text-sm'>
                      Remove
                    </button>
                  </div>
                </div>
              ))}
            </div>
          )}
        </section>
      </main>
    </div>
  )
}

On this page