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-js2. Run the CLI
npx supakeys initThis 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"]
}4. Link Your Supabase Project
# This will show your available projects to select from.
supabase link
# Alternatively, specify the project ref directly
supabase link --project-ref YOUR_PROJECT_REF5. Deploy to Supabase
# Apply database migration
supabase db push
# Deploy edge function
supabase functions deploy passkey-auth6. 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-keyFind 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>
)
}