diff --git a/apps/ui-library/__registry__/index.tsx b/apps/ui-library/__registry__/index.tsx
index f20ed0206cd..377a96a39d3 100644
--- a/apps/ui-library/__registry__/index.tsx
+++ b/apps/ui-library/__registry__/index.tsx
@@ -19,6 +19,18 @@ export const Index = {
component: React.lazy(() => import("@/registry/default/examples/password-based-auth.tsx")),
}
,
+ "passwordless-auth-demo": {
+ component: React.lazy(() => import("@/registry/default/examples/passwordless-auth.tsx")),
+ }
+ ,
+ "magic-link-auth-demo": {
+ component: React.lazy(() => import("@/registry/default/examples/magic-link-auth.tsx")),
+ }
+ ,
+ "otp-auth-demo": {
+ component: React.lazy(() => import("@/registry/default/examples/otp-auth.tsx")),
+ }
+ ,
"current-user-avatar-demo": {
component: React.lazy(() => import("@/registry/default/examples/current-user-avatar-demo.tsx")),
}
diff --git a/apps/ui-library/app/example/passwordless-auth-demo/layout.tsx b/apps/ui-library/app/example/passwordless-auth-demo/layout.tsx
new file mode 100644
index 00000000000..3a6efd5bfd4
--- /dev/null
+++ b/apps/ui-library/app/example/passwordless-auth-demo/layout.tsx
@@ -0,0 +1,45 @@
+import { Metadata } from 'next'
+import { BaseInjector } from './../base-injector'
+import { ThemeProvider } from '@/app/Providers'
+
+export const metadata: Metadata = {
+ title: 'Passwordless Auth Example',
+}
+
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/ui-library/app/example/passwordless-auth-demo/page.tsx b/apps/ui-library/app/example/passwordless-auth-demo/page.tsx
new file mode 100644
index 00000000000..247964286d5
--- /dev/null
+++ b/apps/ui-library/app/example/passwordless-auth-demo/page.tsx
@@ -0,0 +1,7 @@
+import { PasswordlessLoginForm } from '@/registry/default/blocks/passwordless-auth-nextjs/components/passwordless-login-form'
+
+const PasswordlessAuthDemo = () => {
+ return
+}
+
+export default PasswordlessAuthDemo
diff --git a/apps/ui-library/config/docs.ts b/apps/ui-library/config/docs.ts
index 98a90cb62f1..330861d4d99 100644
--- a/apps/ui-library/config/docs.ts
+++ b/apps/ui-library/config/docs.ts
@@ -66,6 +66,14 @@ export const componentPages: SidebarNavGroup = {
items: [],
commandItemLabel: 'Password-Based Auth',
},
+ {
+ title: 'Passwordless Auth',
+ supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
+ href: '/docs/nextjs/passwordless-auth',
+ items: [],
+ new: true,
+ commandItemLabel: 'Passwordless Auth',
+ },
{
title: 'Social Auth',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react', 'vue', 'nuxtjs'],
diff --git a/apps/ui-library/content/docs/nextjs/passwordless-auth.mdx b/apps/ui-library/content/docs/nextjs/passwordless-auth.mdx
new file mode 100644
index 00000000000..fb6bf32c0d0
--- /dev/null
+++ b/apps/ui-library/content/docs/nextjs/passwordless-auth.mdx
@@ -0,0 +1,229 @@
+---
+title: Passwordless Authentication
+description: Passwordless authentication block for Next.js with Magic Link and OTP support
+---
+
+
+
+## Installation
+
+
+
+## Folder structure
+
+This block includes the [Supabase client](/ui/docs/nextjs/client). If you already have one installed, you can skip overwriting it.
+
+
+
+## Usage
+
+Once you install the block in your Next.js project, you'll get all the necessary pages and components to set up passwordless email authentication with both Magic Link and OTP methods.
+
+### Getting started
+
+After installing the block, you'll have the following environment variables in your `.env.local` file:
+
+```env
+NEXT_PUBLIC_SUPABASE_URL=
+NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY=
+```
+
+- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=nextjs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api).
+
+- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
+
+## Authentication Methods
+
+This block supports two passwordless authentication methods:
+
+### Magic Link
+
+Magic Links are one-time use URLs sent to the user's email. When clicked, they automatically authenticate the user.
+
+**When to use**: Simple, one-click authentication with minimal user interaction.
+
+### OTP (One-Time Password)
+
+OTPs are 6-digit codes sent to the user's email that they manually enter to authenticate.
+
+**When to use**: When you need more control over the authentication flow or for mobile apps.
+
+## Components
+
+### PasswordlessLoginForm (Recommended)
+
+A unified passwordless authentication form. Use the `method` prop to choose between Magic Link or OTP authentication.
+
+```tsx
+import { PasswordlessLoginForm } from '@/components/auth/passwordless-login-form'
+
+export default function LoginPage() {
+ // Defaults to 'magic-link' if method prop is not provided
+ return
+}
+```
+
+**Props:**
+
+- `method?: 'magic-link' | 'otp'` - Authentication method (default: `'magic-link'`)
+- `className?: string` - Additional CSS classes
+
+**Examples:**
+
+```tsx
+// Magic Link authentication
+
+
+// OTP authentication
+
+```
+
+### MagicLinkForm
+
+Standalone Magic Link authentication form.
+
+```tsx
+import { MagicLinkForm } from '@/components/auth/magic-link-form'
+
+export default function LoginPage() {
+ return
+}
+```
+
+### OTPRequestForm & OTPVerifyForm
+
+Two-step OTP authentication flow.
+
+```tsx
+// Request OTP
+import { OTPRequestForm } from '@/components/auth/otp-request-form'
+
+export default function RequestPage() {
+ return
+}
+
+// Verify OTP
+import { OTPVerifyForm } from '@/components/auth/otp-verify-form'
+
+export default function VerifyPage() {
+ return
+}
+```
+
+## Configuration
+
+### Email Templates
+
+You need to configure your email template based on which method you want to use:
+
+#### For Magic Link (Default)
+
+1. Go to [Email Templates](https://supabase.com/dashboard/project/_/auth/templates) in the Supabase Dashboard
+2. Select "Magic Link" template
+3. Use the following HTML:
+
+ ```html
+ Magic Link
+
+ Follow this link to login:
+
+ Log In
+
+ ```
+
+#### For OTP
+
+1. Go to [Email Templates](https://supabase.com/dashboard/project/_/auth/templates) in the Supabase Dashboard
+2. Select "Magic Link" template (yes, the same one)
+3. Replace with the following HTML:
+
+ ```html
+ One Time Login Code
+
+ Please enter this code to sign in:
+ {{ .Token }}
+ This code will expire in 1 hour.
+ ```
+
+
+
+If you want to support both methods, you can include both the link and the token in your email template.
+
+
+
+### Setting up Redirect URLs
+
+1. Set the site URL in the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings in the Supabase Dashboard.
+
+2. Add the following redirect URLs to your [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration):
+
+ - `http://localhost:3000/auth/confirm` (for development)
+ - `https://yourdomain.com/auth/confirm` (for production)
+ - `http://localhost:3000/protected` (or your authenticated route)
+
+3. Update the redirect paths in the components to point to the logged-in routes in your app. Our examples use `/protected`, but you can set this to whatever fits your app.
+
+### Configuration Options
+
+#### Disable Auto Sign-Up
+
+By default, users are automatically created if they don't exist. To prevent this:
+
+```tsx
+const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ shouldCreateUser: false, // Only allow existing users
+ },
+})
+```
+
+#### OTP Expiration
+
+Configure OTP expiration in the Supabase Dashboard:
+
+1. Go to **Authentication** > **Providers** > **Email**
+2. Find **Email OTP Expiration**
+3. Set custom duration (default: 3600 seconds = 1 hour, max: 86400 seconds = 24 hours)
+
+## Security
+
+### Rate Limiting
+
+By default, users can request a magic link or OTP once every 60 seconds to prevent abuse.
+
+### Token Expiration
+
+- Magic Links expire after 1 hour by default
+- OTPs expire after 1 hour by default
+
+## Troubleshooting
+
+### Magic Link not working
+
+- Verify redirect URLs are configured in Supabase Dashboard
+- Check email template configuration
+- Check browser console for errors, output them yourself if none are present
+
+### OTP not received
+
+- Verify email template shows `{{ .Token }}`
+- Check spam folder
+- Verify rate limiting hasn't been triggered (wait 60 seconds)
+
+### "Invalid token" error
+
+- Check if token has expired (default: 1 hour)
+- Ensure user hasn't already used the token
+- Verify system clock is synchronized
+
+## Further reading
+
+- [Passwordless email logins](https://supabase.com/docs/guides/auth/auth-email-passwordless)
+- [Magic Link authentication](https://supabase.com/docs/guides/auth/auth-email-passwordless#with-magic-link)
+- [OTP authentication](https://supabase.com/docs/guides/auth/auth-email-passwordless#with-otp)
+- [Email templates](https://supabase.com/docs/guides/auth/auth-email-templates)
+- [Authentication error codes](https://supabase.com/docs/guides/auth/debugging/error-codes)
diff --git a/apps/ui-library/content/docs/react-router/passwordless-auth.mdx b/apps/ui-library/content/docs/react-router/passwordless-auth.mdx
new file mode 100644
index 00000000000..bb0fba0da7d
--- /dev/null
+++ b/apps/ui-library/content/docs/react-router/passwordless-auth.mdx
@@ -0,0 +1,229 @@
+---
+title: Passwordless Authentication
+description: Passwordless authentication block for React Router with Magic Link and OTP support
+---
+
+
+
+## Installation
+
+
+
+## Folder structure
+
+This block includes the [Supabase client](/ui/docs/react-router/client). If you already have one installed, you can skip overwriting it.
+
+
+
+## Usage
+
+Once you install the block in your React Router project, you'll get all the necessary routes and components to set up passwordless email authentication with both Magic Link and OTP methods.
+
+### Getting started
+
+After installing the block, you'll have the following environment variables in your `.env.local` file:
+
+```env
+VITE_SUPABASE_URL=
+VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY=
+```
+
+- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=nextjs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api).
+
+- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
+
+## Authentication Methods
+
+This block supports two passwordless authentication methods:
+
+### Magic Link
+
+Magic Links are one-time use URLs sent to the user's email. When clicked, they automatically authenticate the user.
+
+**When to use**: Simple, one-click authentication with minimal user interaction.
+
+### OTP (One-Time Password)
+
+OTPs are 6-digit codes sent to the user's email that they manually enter to authenticate.
+
+**When to use**: When you need more control over the authentication flow or for mobile apps.
+
+## Components
+
+### PasswordlessLoginForm (Recommended)
+
+A unified passwordless authentication form. Use the `method` prop to choose between Magic Link or OTP authentication.
+
+```tsx
+import { PasswordlessLoginForm } from '@/components/auth/passwordless-login-form'
+
+export default function LoginPage() {
+ // Defaults to 'magic-link' if method prop is not provided
+ return
+}
+```
+
+**Props:**
+
+- `method?: 'magic-link' | 'otp'` - Authentication method (default: `'magic-link'`)
+- `className?: string` - Additional CSS classes
+
+**Examples:**
+
+```tsx
+// Magic Link authentication
+
+
+// OTP authentication
+
+```
+
+### MagicLinkForm
+
+Standalone Magic Link authentication form.
+
+```tsx
+import { MagicLinkForm } from '@/components/auth/magic-link-form'
+
+export default function LoginPage() {
+ return
+}
+```
+
+### OTPRequestForm & OTPVerifyForm
+
+Two-step OTP authentication flow.
+
+```tsx
+// Request OTP
+import { OTPRequestForm } from '@/components/auth/otp-request-form'
+
+export default function RequestPage() {
+ return
+}
+
+// Verify OTP
+import { OTPVerifyForm } from '@/components/auth/otp-verify-form'
+
+export default function VerifyPage() {
+ return
+}
+```
+
+## Configuration
+
+### Email Templates
+
+You need to configure your email template based on which method you want to use:
+
+#### For Magic Link (Default)
+
+1. Go to [Email Templates](https://supabase.com/dashboard/project/_/auth/templates) in the Supabase Dashboard
+2. Select "Magic Link" template
+3. Use the following HTML:
+
+ ```html
+ Magic Link
+
+ Follow this link to login:
+
+ Log In
+
+ ```
+
+#### For OTP
+
+1. Go to [Email Templates](https://supabase.com/dashboard/project/_/auth/templates) in the Supabase Dashboard
+2. Select "Magic Link" template (yes, the same one)
+3. Replace with the following HTML:
+
+ ```html
+ One Time Login Code
+
+ Please enter this code to sign in:
+ {{ .Token }}
+ This code will expire in 1 hour.
+ ```
+
+
+
+If you want to support both methods, you can include both the link and the token in your email template.
+
+
+
+### Setting up Redirect URLs
+
+1. Set the site URL in the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings in the Supabase Dashboard.
+
+2. Add the following redirect URLs to your [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration):
+
+ - `http://localhost:3000/auth/confirm` (for development)
+ - `https://yourdomain.com/auth/confirm` (for production)
+ - `http://localhost:3000/protected` (or your authenticated route)
+
+3. Update the redirect paths in the components to point to the logged-in routes in your app. Our examples use `/protected`, but you can set this to whatever fits your app.
+
+### Configuration Options
+
+#### Disable Auto Sign-Up
+
+By default, users are automatically created if they don't exist. To prevent this:
+
+```tsx
+const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ shouldCreateUser: false, // Only allow existing users
+ },
+})
+```
+
+#### OTP Expiration
+
+Configure OTP expiration in the Supabase Dashboard:
+
+1. Go to **Authentication** > **Providers** > **Email**
+2. Find **Email OTP Expiration**
+3. Set custom duration (default: 3600 seconds = 1 hour, max: 86400 seconds = 24 hours)
+
+## Security
+
+### Rate Limiting
+
+By default, users can request a magic link or OTP once every 60 seconds to prevent abuse.
+
+### Token Expiration
+
+- Magic Links expire after 1 hour by default
+- OTPs expire after 1 hour by default
+
+## Troubleshooting
+
+### Magic Link not working
+
+- Verify redirect URLs are configured in Supabase Dashboard
+- Check email template configuration
+- Check browser console for errors, output them yourself if none are present
+
+### OTP not received
+
+- Verify email template shows `{{ .Token }}`
+- Check spam folder
+- Verify rate limiting hasn't been triggered (wait 60 seconds)
+
+### "Invalid token" error
+
+- Check if token has expired (default: 1 hour)
+- Ensure user hasn't already used the token
+- Verify system clock is synchronized
+
+## Further reading
+
+- [Passwordless email logins](https://supabase.com/docs/guides/auth/auth-email-passwordless)
+- [Magic Link authentication](https://supabase.com/docs/guides/auth/auth-email-passwordless#with-magic-link)
+- [OTP authentication](https://supabase.com/docs/guides/auth/auth-email-passwordless#with-otp)
+- [Email templates](https://supabase.com/docs/guides/auth/auth-email-templates)
+- [Authentication error codes](https://supabase.com/docs/guides/auth/debugging/error-codes)
diff --git a/apps/ui-library/content/docs/react/passwordless-auth.mdx b/apps/ui-library/content/docs/react/passwordless-auth.mdx
new file mode 100644
index 00000000000..0b16af8756e
--- /dev/null
+++ b/apps/ui-library/content/docs/react/passwordless-auth.mdx
@@ -0,0 +1,229 @@
+---
+title: Passwordless Authentication
+description: Passwordless authentication block for React SPA with Magic Link and OTP support
+---
+
+
+
+## Installation
+
+
+
+## Folder structure
+
+This block provides reusable components for passwordless authentication. Unlike framework-specific blocks, this doesn't include routing - you'll integrate these components into your own routing solution.
+
+
+
+## Usage
+
+Once you install the block in your React project, you'll get all the necessary components to set up passwordless email authentication with both Magic Link and OTP methods. You'll need to handle routing and navigation yourself.
+
+### Getting started
+
+After installing the block, you'll need to set up your Supabase client. Create a `.env` file with:
+
+```env
+VITE_SUPABASE_URL=
+VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY=
+```
+
+- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=nextjs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api).
+
+- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
+
+## Authentication Methods
+
+This block supports two passwordless authentication methods:
+
+### Magic Link
+
+Magic Links are one-time use URLs sent to the user's email. When clicked, they automatically authenticate the user.
+
+**When to use**: Simple, one-click authentication with minimal user interaction.
+
+### OTP (One-Time Password)
+
+OTPs are 6-digit codes sent to the user's email that they manually enter to authenticate.
+
+**When to use**: When you need more control over the authentication flow or for mobile apps.
+
+## Components
+
+### PasswordlessLoginForm (Recommended)
+
+A unified passwordless authentication form. Use the `method` prop to choose between Magic Link or OTP authentication.
+
+```tsx
+import { PasswordlessLoginForm } from '@/components/auth/passwordless-login-form'
+
+export default function LoginPage() {
+ // Defaults to 'magic-link' if method prop is not provided
+ return
+}
+```
+
+**Props:**
+
+- `method?: 'magic-link' | 'otp'` - Authentication method (default: `'magic-link'`)
+- `className?: string` - Additional CSS classes
+
+**Examples:**
+
+```tsx
+// Magic Link authentication
+
+
+// OTP authentication
+
+```
+
+### MagicLinkForm
+
+Standalone Magic Link authentication form.
+
+```tsx
+import { MagicLinkForm } from '@/components/auth/magic-link-form'
+
+export default function LoginPage() {
+ return
+}
+```
+
+### OTPRequestForm & OTPVerifyForm
+
+Two-step OTP authentication flow.
+
+```tsx
+// Request OTP
+import { OTPRequestForm } from '@/components/auth/otp-request-form'
+
+export default function RequestPage() {
+ return
+}
+
+// Verify OTP
+import { OTPVerifyForm } from '@/components/auth/otp-verify-form'
+
+export default function VerifyPage() {
+ return
+}
+```
+
+## Configuration
+
+### Email Templates
+
+You need to configure your email template based on which method you want to use:
+
+#### For Magic Link (Default)
+
+1. Go to [Email Templates](https://supabase.com/dashboard/project/_/auth/templates) in the Supabase Dashboard
+2. Select "Magic Link" template
+3. Use the following HTML:
+
+ ```html
+ Magic Link
+
+ Follow this link to login:
+
+ Log In
+
+ ```
+
+#### For OTP
+
+1. Go to [Email Templates](https://supabase.com/dashboard/project/_/auth/templates) in the Supabase Dashboard
+2. Select "Magic Link" template (yes, the same one)
+3. Replace with the following HTML:
+
+ ```html
+ One Time Login Code
+
+ Please enter this code to sign in:
+ {{ .Token }}
+ This code will expire in 1 hour.
+ ```
+
+
+
+If you want to support both methods, you can include both the link and the token in your email template.
+
+
+
+### Setting up Redirect URLs
+
+1. Set the site URL in the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings in the Supabase Dashboard.
+
+2. Add the following redirect URLs to your [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration):
+
+ - `http://localhost:3000/auth/confirm` (for development)
+ - `https://yourdomain.com/auth/confirm` (for production)
+ - `http://localhost:3000/protected` (or your authenticated route)
+
+3. Update the redirect paths in the components to point to the logged-in routes in your app. Our examples use `/protected`, but you can set this to whatever fits your app.
+
+### Configuration Options
+
+#### Disable Auto Sign-Up
+
+By default, users are automatically created if they don't exist. To prevent this:
+
+```tsx
+const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ shouldCreateUser: false, // Only allow existing users
+ },
+})
+```
+
+#### OTP Expiration
+
+Configure OTP expiration in the Supabase Dashboard:
+
+1. Go to **Authentication** > **Providers** > **Email**
+2. Find **Email OTP Expiration**
+3. Set custom duration (default: 3600 seconds = 1 hour, max: 86400 seconds = 24 hours)
+
+## Security
+
+### Rate Limiting
+
+By default, users can request a magic link or OTP once every 60 seconds to prevent abuse.
+
+### Token Expiration
+
+- Magic Links expire after 1 hour by default
+- OTPs expire after 1 hour by default
+
+## Troubleshooting
+
+### Magic Link not working
+
+- Verify redirect URLs are configured in Supabase Dashboard
+- Check email template configuration
+- Check browser console for errors, output them yourself if none are present
+
+### OTP not received
+
+- Verify email template shows `{{ .Token }}`
+- Check spam folder
+- Verify rate limiting hasn't been triggered (wait 60 seconds)
+
+### "Invalid token" error
+
+- Check if token has expired (default: 1 hour)
+- Ensure user hasn't already used the token
+- Verify system clock is synchronized
+
+## Further reading
+
+- [Passwordless email logins](https://supabase.com/docs/guides/auth/auth-email-passwordless)
+- [Magic Link authentication](https://supabase.com/docs/guides/auth/auth-email-passwordless#with-magic-link)
+- [OTP authentication](https://supabase.com/docs/guides/auth/auth-email-passwordless#with-otp)
+- [Email templates](https://supabase.com/docs/guides/auth/auth-email-templates)
+- [Authentication error codes](https://supabase.com/docs/guides/auth/debugging/error-codes)
diff --git a/apps/ui-library/content/docs/tanstack/passwordless-auth.mdx b/apps/ui-library/content/docs/tanstack/passwordless-auth.mdx
new file mode 100644
index 00000000000..64a919200bf
--- /dev/null
+++ b/apps/ui-library/content/docs/tanstack/passwordless-auth.mdx
@@ -0,0 +1,229 @@
+---
+title: Passwordless Authentication
+description: Passwordless authentication block for TanStack Start with Magic Link and OTP support
+---
+
+
+
+## Installation
+
+
+
+## Folder structure
+
+This block includes the [Supabase client](/ui/docs/tanstack/client). If you already have one installed, you can skip overwriting it.
+
+
+
+## Usage
+
+Once you install the block in your TanStack Start project, you'll get all the necessary routes and components to set up passwordless email authentication with both Magic Link and OTP methods.
+
+### Getting started
+
+After installing the block, you'll have the following environment variables in your `.env` file:
+
+```env
+VITE_SUPABASE_URL=
+VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY=
+```
+
+- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=nextjs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api).
+
+- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
+
+## Authentication Methods
+
+This block supports two passwordless authentication methods:
+
+### Magic Link
+
+Magic Links are one-time use URLs sent to the user's email. When clicked, they automatically authenticate the user.
+
+**When to use**: Simple, one-click authentication with minimal user interaction.
+
+### OTP (One-Time Password)
+
+OTPs are 6-digit codes sent to the user's email that they manually enter to authenticate.
+
+**When to use**: When you need more control over the authentication flow or for mobile apps.
+
+## Components
+
+### PasswordlessLoginForm (Recommended)
+
+A unified passwordless authentication form. Use the `method` prop to choose between Magic Link or OTP authentication.
+
+```tsx
+import { PasswordlessLoginForm } from '@/components/auth/passwordless-login-form'
+
+export default function LoginPage() {
+ // Defaults to 'magic-link' if method prop is not provided
+ return
+}
+```
+
+**Props:**
+
+- `method?: 'magic-link' | 'otp'` - Authentication method (default: `'magic-link'`)
+- `className?: string` - Additional CSS classes
+
+**Examples:**
+
+```tsx
+// Magic Link authentication
+
+
+// OTP authentication
+
+```
+
+### MagicLinkForm
+
+Standalone Magic Link authentication form.
+
+```tsx
+import { MagicLinkForm } from '@/components/auth/magic-link-form'
+
+export default function LoginPage() {
+ return
+}
+```
+
+### OTPRequestForm & OTPVerifyForm
+
+Two-step OTP authentication flow.
+
+```tsx
+// Request OTP
+import { OTPRequestForm } from '@/components/auth/otp-request-form'
+
+export default function RequestPage() {
+ return
+}
+
+// Verify OTP
+import { OTPVerifyForm } from '@/components/auth/otp-verify-form'
+
+export default function VerifyPage() {
+ return
+}
+```
+
+## Configuration
+
+### Email Templates
+
+You need to configure your email template based on which method you want to use:
+
+#### For Magic Link (Default)
+
+1. Go to [Email Templates](https://supabase.com/dashboard/project/_/auth/templates) in the Supabase Dashboard
+2. Select "Magic Link" template
+3. Use the following HTML:
+
+ ```html
+ Magic Link
+
+ Follow this link to login:
+
+ Log In
+
+ ```
+
+#### For OTP
+
+1. Go to [Email Templates](https://supabase.com/dashboard/project/_/auth/templates) in the Supabase Dashboard
+2. Select "Magic Link" template (yes, the same one)
+3. Replace with the following HTML:
+
+ ```html
+ One Time Login Code
+
+ Please enter this code to sign in:
+ {{ .Token }}
+ This code will expire in 1 hour.
+ ```
+
+
+
+If you want to support both methods, you can include both the link and the token in your email template.
+
+
+
+### Setting up Redirect URLs
+
+1. Set the site URL in the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings in the Supabase Dashboard.
+
+2. Add the following redirect URLs to your [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration):
+
+ - `http://localhost:3000/auth/confirm` (for development)
+ - `https://yourdomain.com/auth/confirm` (for production)
+ - `http://localhost:3000/protected` (or your authenticated route)
+
+3. Update the redirect paths in the components to point to the logged-in routes in your app. Our examples use `/protected`, but you can set this to whatever fits your app.
+
+### Configuration Options
+
+#### Disable Auto Sign-Up
+
+By default, users are automatically created if they don't exist. To prevent this:
+
+```tsx
+const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ shouldCreateUser: false, // Only allow existing users
+ },
+})
+```
+
+#### OTP Expiration
+
+Configure OTP expiration in the Supabase Dashboard:
+
+1. Go to **Authentication** > **Providers** > **Email**
+2. Find **Email OTP Expiration**
+3. Set custom duration (default: 3600 seconds = 1 hour, max: 86400 seconds = 24 hours)
+
+## Security
+
+### Rate Limiting
+
+By default, users can request a magic link or OTP once every 60 seconds to prevent abuse.
+
+### Token Expiration
+
+- Magic Links expire after 1 hour by default
+- OTPs expire after 1 hour by default
+
+## Troubleshooting
+
+### Magic Link not working
+
+- Verify redirect URLs are configured in Supabase Dashboard
+- Check email template configuration
+- Check browser console for errors, output them yourself if none are present
+
+### OTP not received
+
+- Verify email template shows `{{ .Token }}`
+- Check spam folder
+- Verify rate limiting hasn't been triggered (wait 60 seconds)
+
+### "Invalid token" error
+
+- Check if token has expired (default: 1 hour)
+- Ensure user hasn't already used the token
+- Verify system clock is synchronized
+
+## Further reading
+
+- [Passwordless email logins](https://supabase.com/docs/guides/auth/auth-email-passwordless)
+- [Magic Link authentication](https://supabase.com/docs/guides/auth/auth-email-passwordless#with-magic-link)
+- [OTP authentication](https://supabase.com/docs/guides/auth/auth-email-passwordless#with-otp)
+- [Email templates](https://supabase.com/docs/guides/auth/auth-email-templates)
+- [Authentication error codes](https://supabase.com/docs/guides/auth/debugging/error-codes)
diff --git a/apps/ui-library/public/r/ai-editor-rules.json b/apps/ui-library/public/r/ai-editor-rules.json
index 7d59f8be252..375824e29c8 100644
--- a/apps/ui-library/public/r/ai-editor-rules.json
+++ b/apps/ui-library/public/r/ai-editor-rules.json
@@ -33,7 +33,7 @@
},
{
"path": "registry/default/ai-editor-rules/writing-supabase-edge-functions.mdc",
- "content": "---\ndescription: Coding rules for Supabase Edge Functions\nalwaysApply: false\n---\n\n# Writing Supabase Edge Functions\n\nYou're an expert in writing TypeScript and Deno JavaScript runtime. Generate **high-quality Supabase Edge Functions** that adhere to the following best practices:\n\n## Guidelines\n\n1. Try to use Web APIs and Deno’s core APIs instead of external dependencies (eg: use fetch instead of Axios, use WebSockets API instead of node-ws)\n2. If you are reusing utility methods between Edge Functions, add them to `supabase/functions/_shared` and import using a relative path. Do NOT have cross dependencies between Edge Functions.\n3. Do NOT use bare specifiers when importing dependecnies. If you need to use an external dependency, make sure it's prefixed with either `npm:` or `jsr:`. For example, `@supabase/supabase-js` should be written as `npm:@supabase/supabase-js`.\n4. For external imports, always define a version. For example, `npm:@express` should be written as `npm:express@4.18.2`.\n5. For external dependencies, importing via `npm:` and `jsr:` is preferred. Minimize the use of imports from @`deno.land/x` , `esm.sh` and @`unpkg.com` . If you have a package from one of those CDNs, you can replace the CDN hostname with `npm:` specifier.\n6. You can also use Node built-in APIs. You will need to import them using `node:` specifier. For example, to import Node process: `import process from \"node:process\". Use Node APIs when you find gaps in Deno APIs.\n7. Do NOT use `import { serve } from \"https://deno.land/std@0.168.0/http/server.ts\"`. Instead use the built-in `Deno.serve`.\n8. Following environment variables (ie. secrets) are pre-populated in both local and hosted Supabase environments. Users don't need to manually set them:\n - SUPABASE_URL\n - SUPABASE_PUBLISHABLE_OR_ANON_KEY\n - SUPABASE_SERVICE_ROLE_KEY\n - SUPABASE_DB_URL\n9. To set other environment variables (ie. secrets) users can put them in a env file and run the `supabase secrets set --env-file path/to/env-file`\n10. A single Edge Function can handle multiple routes. It is recommended to use a library like Express or Hono to handle the routes as it's easier for developer to understand and maintain. Each route must be prefixed with `/function-name` so they are routed correctly.\n11. File write operations are ONLY permitted on `/tmp` directory. You can use either Deno or Node File APIs.\n12. Use `EdgeRuntime.waitUntil(promise)` static method to run long-running tasks in the background without blocking response to a request. Do NOT assume it is available in the request / execution context.\n\n## Example Templates\n\n### Simple Hello World Function\n\n```tsx\ninterface reqPayload {\n name: string\n}\n\nconsole.info('server started')\n\nDeno.serve(async (req: Request) => {\n const { name }: reqPayload = await req.json()\n const data = {\n message: `Hello ${name} from foo!`,\n }\n\n return new Response(JSON.stringify(data), {\n headers: { 'Content-Type': 'application/json', Connection: 'keep-alive' },\n })\n})\n```\n\n### Example Function using Node built-in API\n\n```tsx\nimport { randomBytes } from 'node:crypto'\nimport { createServer } from 'node:http'\nimport process from 'node:process'\n\nconst generateRandomString = (length) => {\n const buffer = randomBytes(length)\n return buffer.toString('hex')\n}\n\nconst randomString = generateRandomString(10)\nconsole.log(randomString)\n\nconst server = createServer((req, res) => {\n const message = `Hello`\n res.end(message)\n})\n\nserver.listen(9999)\n```\n\n### Using npm packages in Functions\n\n```tsx\nimport express from 'npm:express@4.18.2'\n\nconst app = express()\n\napp.get(/(.*)/, (req, res) => {\n res.send('Welcome to Supabase')\n})\n\napp.listen(8000)\n```\n\n### Generate embeddings using built-in @Supabase.ai API\n\n```tsx\nconst model = new Supabase.ai.Session('gte-small')\n\nDeno.serve(async (req: Request) => {\n const params = new URL(req.url).searchParams\n const input = params.get('text')\n const output = await model.run(input, { mean_pool: true, normalize: true })\n return new Response(JSON.stringify(output), {\n headers: {\n 'Content-Type': 'application/json',\n Connection: 'keep-alive',\n },\n })\n})\n```\n",
+ "content": "---\ndescription: Coding rules for Supabase Edge Functions\nalwaysApply: false\n---\n\n# Writing Supabase Edge Functions\n\nYou're an expert in writing TypeScript and Deno JavaScript runtime. Generate **high-quality Supabase Edge Functions** that adhere to the following best practices:\n\n## Guidelines\n\n1. Try to use Web APIs and Deno’s core APIs instead of external dependencies (eg: use fetch instead of Axios, use WebSockets API instead of node-ws)\n2. If you are reusing utility methods between Edge Functions, add them to `supabase/functions/_shared` and import using a relative path. Do NOT have cross dependencies between Edge Functions.\n3. Do NOT use bare specifiers when importing dependencies. If you need to use an external dependency, make sure it's prefixed with either `npm:` or `jsr:`. For example, `@supabase/supabase-js` should be written as `npm:@supabase/supabase-js`.\n4. For external imports, always define a version. For example, `npm:@express` should be written as `npm:express@4.18.2`.\n5. For external dependencies, importing via `npm:` and `jsr:` is preferred. Minimize the use of imports from @`deno.land/x` , `esm.sh` and @`unpkg.com` . If you have a package from one of those CDNs, you can replace the CDN hostname with `npm:` specifier.\n6. You can also use Node built-in APIs. You will need to import them using `node:` specifier. For example, to import Node process: `import process from \"node:process\". Use Node APIs when you find gaps in Deno APIs.\n7. Do NOT use `import { serve } from \"https://deno.land/std@0.168.0/http/server.ts\"`. Instead use the built-in `Deno.serve`.\n8. Following environment variables (ie. secrets) are pre-populated in both local and hosted Supabase environments. Users don't need to manually set them:\n - SUPABASE_URL\n - SUPABASE_PUBLISHABLE_OR_ANON_KEY\n - SUPABASE_SERVICE_ROLE_KEY\n - SUPABASE_DB_URL\n9. To set other environment variables (ie. secrets) users can put them in a env file and run the `supabase secrets set --env-file path/to/env-file`\n10. A single Edge Function can handle multiple routes. It is recommended to use a library like Express or Hono to handle the routes as it's easier for developer to understand and maintain. Each route must be prefixed with `/function-name` so they are routed correctly.\n11. File write operations are ONLY permitted on `/tmp` directory. You can use either Deno or Node File APIs.\n12. Use `EdgeRuntime.waitUntil(promise)` static method to run long-running tasks in the background without blocking response to a request. Do NOT assume it is available in the request / execution context.\n\n## Example Templates\n\n### Simple Hello World Function\n\n```tsx\ninterface reqPayload {\n name: string\n}\n\nconsole.info('server started')\n\nDeno.serve(async (req: Request) => {\n const { name }: reqPayload = await req.json()\n const data = {\n message: `Hello ${name} from foo!`,\n }\n\n return new Response(JSON.stringify(data), {\n headers: { 'Content-Type': 'application/json', Connection: 'keep-alive' },\n })\n})\n```\n\n### Example Function using Node built-in API\n\n```tsx\nimport { randomBytes } from 'node:crypto'\nimport { createServer } from 'node:http'\nimport process from 'node:process'\n\nconst generateRandomString = (length) => {\n const buffer = randomBytes(length)\n return buffer.toString('hex')\n}\n\nconst randomString = generateRandomString(10)\nconsole.log(randomString)\n\nconst server = createServer((req, res) => {\n const message = `Hello`\n res.end(message)\n})\n\nserver.listen(9999)\n```\n\n### Using npm packages in Functions\n\n```tsx\nimport express from 'npm:express@4.18.2'\n\nconst app = express()\n\napp.get(/(.*)/, (req, res) => {\n res.send('Welcome to Supabase')\n})\n\napp.listen(8000)\n```\n\n### Generate embeddings using built-in @Supabase.ai API\n\n```tsx\nconst model = new Supabase.ai.Session('gte-small')\n\nDeno.serve(async (req: Request) => {\n const params = new URL(req.url).searchParams\n const input = params.get('text')\n const output = await model.run(input, { mean_pool: true, normalize: true })\n return new Response(JSON.stringify(output), {\n headers: {\n 'Content-Type': 'application/json',\n Connection: 'keep-alive',\n },\n })\n})\n```\n",
"type": "registry:file",
"target": "~/.cursor/rules/writing-supabase-edge-functions.mdc"
},
diff --git a/apps/ui-library/public/r/password-based-auth-react-router.json b/apps/ui-library/public/r/password-based-auth-react-router.json
index 5f24bcffc3e..a6d3e31ef17 100644
--- a/apps/ui-library/public/r/password-based-auth-react-router.json
+++ b/apps/ui-library/public/r/password-based-auth-react-router.json
@@ -55,7 +55,7 @@
},
{
"path": "registry/default/blocks/password-based-auth-react-router/app/routes/sign-up.tsx",
- "content": "import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { type ActionFunctionArgs, Link, redirect, useFetcher, useSearchParams } from 'react-router'\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n const { supabase } = createClient(request)\n\n const url = new URL(request.url)\n const origin = url.origin\n\n const formData = await request.formData()\n\n const email = formData.get('email') as string\n const password = formData.get('password') as string\n const repeatPassword = formData.get('repeat-password') as string\n\n if (!password) {\n return {\n error: 'Password is required',\n }\n }\n\n if (password !== repeatPassword) {\n return { error: 'Passwords do not match' }\n }\n\n const { error } = await supabase.auth.signUp({\n email,\n password,\n options: {\n emailRedirectTo: `${origin}/protected`,\n },\n })\n\n if (error) {\n return { error: error.message }\n }\n\n return redirect('/sign-up?success')\n}\n\nexport default function SignUp() {\n const fetcher = useFetcher()\n let [searchParams] = useSearchParams()\n\n const success = !!searchParams.has('success')\n const error = fetcher.data?.error\n const loading = fetcher.state === 'submitting'\n\n return (\n \n
\n
\n {success ? (\n
\n \n Thank you for signing up! \n Check your email to confirm \n \n \n \n You've successfully signed up. Please check your email to confirm your account\n before signing in.\n
\n \n \n ) : (\n
\n \n Sign up \n Create a new account \n \n \n \n \n
\n Email \n \n
\n
\n
\n
\n Repeat Password \n
\n
\n
\n {error &&
{error}
}\n
\n {loading ? 'Creating an account...' : 'Sign up'}\n \n
\n \n Already have an account?{' '}\n \n Login\n \n
\n \n \n \n )}\n
\n
\n
\n )\n}\n",
+ "content": "import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { type ActionFunctionArgs, Link, redirect, useFetcher, useSearchParams } from 'react-router'\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n const { supabase } = createClient(request)\n\n const url = new URL(request.url)\n const origin = url.origin\n\n const formData = await request.formData()\n\n const email = formData.get('email') as string\n const password = formData.get('password') as string\n const repeatPassword = formData.get('repeat-password') as string\n\n if (!password) {\n return {\n error: 'Password is required',\n }\n }\n\n if (password !== repeatPassword) {\n return { error: 'Passwords do not match' }\n }\n\n const { error } = await supabase.auth.signUp({\n email,\n password,\n options: {\n emailRedirectTo: `${origin}/protected`,\n },\n })\n\n if (error) {\n return { error: error.message }\n }\n\n return redirect('/sign-up?success')\n}\n\nexport default function SignUp() {\n const fetcher = useFetcher()\n let [searchParams] = useSearchParams()\n\n const success = !!searchParams.has('success')\n const error = fetcher.data?.error\n const loading = fetcher.state === 'submitting'\n\n return (\n \n
\n
\n {success ? (\n
\n \n Thank you for signing up! \n Check your email to confirm \n \n \n \n You've successfully signed up. Please check your email to confirm your\n account before signing in.\n
\n \n \n ) : (\n
\n \n Sign up \n Create a new account \n \n \n \n \n
\n Email \n \n
\n
\n
\n
\n Repeat Password \n
\n
\n
\n {error &&
{error}
}\n
\n {loading ? 'Creating an account...' : 'Sign up'}\n \n
\n \n Already have an account?{' '}\n \n Login\n \n
\n \n \n \n )}\n
\n
\n
\n )\n}\n",
"type": "registry:file",
"target": "app/routes/sign-up.tsx"
},
diff --git a/apps/ui-library/public/r/password-based-auth-react.json b/apps/ui-library/public/r/password-based-auth-react.json
index a50604835e8..75fdd187e97 100644
--- a/apps/ui-library/public/r/password-based-auth-react.json
+++ b/apps/ui-library/public/r/password-based-auth-react.json
@@ -21,7 +21,7 @@
},
{
"path": "registry/default/blocks/password-based-auth-react/components/sign-up-form.tsx",
- "content": "import { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/react/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { useState } from 'react'\n\nexport function SignUpForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [repeatPassword, setRepeatPassword] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const [success, setSuccess] = useState(false)\n\n const handleSignUp = async (e: React.FormEvent) => {\n const supabase = createClient()\n e.preventDefault()\n setError(null)\n\n if (password !== repeatPassword) {\n setError('Passwords do not match')\n return\n }\n setIsLoading(true)\n\n try {\n const { error } = await supabase.auth.signUp({\n email,\n password,\n })\n if (error) throw error\n setSuccess(true)\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n \n {success ? (\n
\n \n Thank you for signing up! \n Check your email to confirm \n \n \n \n You've successfully signed up. Please check your email to confirm your account before\n signing in.\n
\n \n \n ) : (\n
\n \n Sign up \n Create a new account \n \n \n \n \n \n )}\n
\n )\n}\n",
+ "content": "import { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/react/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { useState } from 'react'\n\nexport function SignUpForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [repeatPassword, setRepeatPassword] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const [success, setSuccess] = useState(false)\n\n const handleSignUp = async (e: React.FormEvent) => {\n const supabase = createClient()\n e.preventDefault()\n setError(null)\n\n if (password !== repeatPassword) {\n setError('Passwords do not match')\n return\n }\n setIsLoading(true)\n\n try {\n const { error } = await supabase.auth.signUp({\n email,\n password,\n })\n if (error) throw error\n setSuccess(true)\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n \n {success ? (\n
\n \n Thank you for signing up! \n Check your email to confirm \n \n \n \n You've successfully signed up. Please check your email to confirm your account\n before signing in.\n
\n \n \n ) : (\n
\n \n Sign up \n Create a new account \n \n \n \n \n \n )}\n
\n )\n}\n",
"type": "registry:component"
},
{
diff --git a/apps/ui-library/public/r/password-based-auth-tanstack.json b/apps/ui-library/public/r/password-based-auth-tanstack.json
index 0d84095b204..9c5efdba4de 100644
--- a/apps/ui-library/public/r/password-based-auth-tanstack.json
+++ b/apps/ui-library/public/r/password-based-auth-tanstack.json
@@ -58,7 +58,7 @@
},
{
"path": "registry/default/blocks/password-based-auth-tanstack/routes/sign-up-success.tsx",
- "content": "import {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { createFileRoute } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/sign-up-success')({\n component: SignUpSuccess,\n})\n\nfunction SignUpSuccess() {\n return (\n \n
\n
\n
\n \n Thank you for signing up! \n Check your email to confirm \n \n \n \n You've successfully signed up. Please check your email to confirm your account\n before signing in.\n
\n \n \n
\n
\n
\n )\n}\n",
+ "content": "import {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { createFileRoute } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/sign-up-success')({\n component: SignUpSuccess,\n})\n\nfunction SignUpSuccess() {\n return (\n \n
\n
\n
\n \n Thank you for signing up! \n Check your email to confirm \n \n \n \n You've successfully signed up. Please check your email to confirm your account\n before signing in.\n
\n \n \n
\n
\n
\n )\n}\n",
"type": "registry:file",
"target": "routes/sign-up-success.tsx"
},
diff --git a/apps/ui-library/public/r/passwordless-auth-nextjs.json b/apps/ui-library/public/r/passwordless-auth-nextjs.json
new file mode 100644
index 00000000000..f845e0b4e19
--- /dev/null
+++ b/apps/ui-library/public/r/passwordless-auth-nextjs.json
@@ -0,0 +1,108 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "passwordless-auth-nextjs",
+ "type": "registry:block",
+ "title": "Passwordless Auth for Next.js and Supabase",
+ "description": "Passwordless authentication for Next.js using Supabase Auth. Supports both Magic Link and OTP (One-Time Password) login methods.",
+ "dependencies": [
+ "@supabase/ssr@latest",
+ "@supabase/supabase-js@latest"
+ ],
+ "registryDependencies": [
+ "button",
+ "card",
+ "input",
+ "label",
+ "link"
+ ],
+ "files": [
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/components/magic-link-form.tsx",
+ "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { useState } from 'react'\n\nexport function MagicLinkForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const [isSuccess, setIsSuccess] = useState(false)\n\n const handleMagicLinkRequest = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n setIsSuccess(false)\n\n try {\n const { error } = await supabase.auth.signInWithOtp({\n email,\n options: {\n // Set this to false if you do not want the user to be automatically signed up\n shouldCreateUser: true,\n emailRedirectTo: `${window.location.origin}/auth/confirm`,\n },\n })\n if (error) throw error\n setIsSuccess(true)\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n \n
\n \n Magic Link Login \n \n Enter your email to receive a magic link for passwordless login\n \n \n \n {isSuccess ? (\n \n
Check your email!
\n
\n We've sent a magic link to {email} . Click the\n link in the email to sign in.\n
\n
\n ) : (\n \n )}\n \n \n
\n )\n}\n",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/components/otp-request-form.tsx",
+ "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { useRouter } from 'next/navigation'\nimport { useState } from 'react'\n\nexport function OTPRequestForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const router = useRouter()\n\n const handleOTPRequest = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithOtp({\n email,\n options: {\n // Set this to false if you do not want the user to be automatically signed up\n shouldCreateUser: true,\n },\n })\n if (error) throw error\n // Navigate to OTP verification page\n router.push(`/auth/verify-otp?email=${encodeURIComponent(email)}`)\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n \n
\n \n OTP Login \n \n Enter your email to receive a one-time password for passwordless login\n \n \n \n \n \n \n
\n )\n}\n",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/components/otp-verify-form.tsx",
+ "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { useState } from 'react'\n\nexport function OTPVerifyForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const searchParams = useSearchParams()\n const email = searchParams.get('email') || ''\n const [otp, setOTP] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const [isResending, setIsResending] = useState(false)\n const router = useRouter()\n\n const handleVerifyOTP = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.verifyOtp({\n email,\n token: otp,\n type: 'email',\n })\n if (error) throw error\n // Redirect to protected page after successful verification\n router.push('/protected')\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n const handleResendOTP = async () => {\n const supabase = createClient()\n setIsResending(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithOtp({\n email,\n options: {\n shouldCreateUser: false,\n },\n })\n if (error) throw error\n setError(null)\n // Show success message\n alert('A new verification code has been sent to your email.')\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'Failed to resend code')\n } finally {\n setIsResending(false)\n }\n }\n\n return (\n \n
\n \n Verify Your Email \n \n Enter the 6-digit code sent to{' '}\n {email} \n \n \n \n \n \n \n
\n )\n}\n",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/components/passwordless-login-form.tsx",
+ "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { useRouter } from 'next/navigation'\nimport { useState } from 'react'\n\ntype LoginMethod = 'magic-link' | 'otp'\n\ninterface PasswordlessLoginFormProps extends React.ComponentPropsWithoutRef<'div'> {\n method?: LoginMethod\n}\n\nexport function PasswordlessLoginForm({\n className,\n method = 'magic-link',\n ...props\n}: PasswordlessLoginFormProps) {\n const [email, setEmail] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const [isMagicLinkSent, setIsMagicLinkSent] = useState(false)\n const router = useRouter()\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n setIsMagicLinkSent(false)\n\n try {\n if (method === 'magic-link') {\n const { error } = await supabase.auth.signInWithOtp({\n email,\n options: {\n shouldCreateUser: true,\n emailRedirectTo: `${window.location.origin}/auth/confirm`,\n },\n })\n if (error) throw error\n setIsMagicLinkSent(true)\n } else {\n // OTP method\n const { error } = await supabase.auth.signInWithOtp({\n email,\n options: {\n shouldCreateUser: true,\n },\n })\n if (error) throw error\n // Navigate to OTP verification page\n router.push(`/auth/verify-otp?email=${encodeURIComponent(email)}`)\n }\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n \n
\n \n Sign up \n Sign in or create a new account \n \n \n {isMagicLinkSent ? (\n \n
Check your email!
\n
\n We've sent a magic link to {email} . Click\n the link in the email to sign in.\n
\n
{\n setIsMagicLinkSent(false)\n setEmail('')\n }}\n >\n Send another link\n \n
\n ) : (\n \n )}\n \n \n
\n )\n}\n",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/auth/magic-link/page.tsx",
+ "content": "import { MagicLinkForm } from '@/registry/default/blocks/passwordless-auth-nextjs/components/magic-link-form'\n\nexport default function MagicLinkPage() {\n return (\n \n )\n}\n",
+ "type": "registry:page",
+ "target": "app/auth/magic-link/page.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/auth/otp/page.tsx",
+ "content": "import { OTPRequestForm } from '@/registry/default/blocks/passwordless-auth-nextjs/components/otp-request-form'\n\nexport default function OTPRequestPage() {\n return (\n \n )\n}\n",
+ "type": "registry:page",
+ "target": "app/auth/otp/page.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/auth/verify-otp/page.tsx",
+ "content": "import { OTPVerifyForm } from '@/registry/default/blocks/passwordless-auth-nextjs/components/otp-verify-form'\nimport { Suspense } from 'react'\n\nfunction OTPVerifyContent() {\n return (\n \n )\n}\n\nexport default function OTPVerifyPage() {\n return (\n Loading...}>\n \n \n )\n}\n",
+ "type": "registry:page",
+ "target": "app/auth/verify-otp/page.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/auth/passwordless/page.tsx",
+ "content": "import { PasswordlessLoginForm } from '@/registry/default/blocks/passwordless-auth-nextjs/components/passwordless-login-form'\n\nexport default function PasswordlessLoginPage() {\n return (\n \n )\n}\n",
+ "type": "registry:page",
+ "target": "app/auth/passwordless/page.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/auth/confirm/route.ts",
+ "content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server'\nimport { type EmailOtpType } from '@supabase/supabase-js'\nimport { type NextRequest, NextResponse } from 'next/server'\n\nexport async function GET(request: NextRequest) {\n const { searchParams } = new URL(request.url)\n const token_hash = searchParams.get('token_hash')\n const type = searchParams.get('type') as EmailOtpType | null\n const next = searchParams.get('next') ?? '/protected'\n\n if (token_hash && type) {\n const supabase = await createClient()\n\n const { error } = await supabase.auth.verifyOtp({\n type,\n token_hash,\n })\n if (!error) {\n // Redirect user to specified redirect URL or default to protected page\n return NextResponse.redirect(new URL(next, request.url))\n }\n }\n\n // Redirect the user to an error page with some instructions\n return NextResponse.redirect(new URL('/auth/error', request.url))\n}\n",
+ "type": "registry:page",
+ "target": "app/auth/confirm/route.ts"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/auth/error/page.tsx",
+ "content": "import {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Button } from '@/registry/default/components/ui/button'\nimport Link from 'next/link'\n\nexport default function AuthErrorPage() {\n return (\n \n
\n
\n \n Authentication Error \n There was an error authenticating your account \n \n \n \n
\n The authentication link may have expired or is invalid. Please try requesting a new\n one.\n
\n
\n Try again\n \n
\n \n \n
\n
\n )\n}\n",
+ "type": "registry:page",
+ "target": "app/auth/error/page.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/protected/page.tsx",
+ "content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server'\nimport { redirect } from 'next/navigation'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Button } from '@/registry/default/components/ui/button'\n\nexport default async function ProtectedPage() {\n const supabase = await createClient()\n\n const {\n data: { user },\n } = await supabase.auth.getUser()\n\n if (!user) {\n redirect('/auth/passwordless')\n }\n\n const handleSignOut = async () => {\n 'use server'\n const supabase = await createClient()\n await supabase.auth.signOut()\n redirect('/auth/passwordless')\n }\n\n return (\n \n
\n
\n \n Protected Page \n You are logged in \n \n \n \n
\n
Email
\n
{user.email}
\n
\n
\n
\n
\n \n \n
\n
\n )\n}\n",
+ "type": "registry:page",
+ "target": "app/protected/page.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/middleware.ts",
+ "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function middleware(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n const {\n data: { user },\n } = await supabase.auth.getUser()\n\n // Protect /protected route\n if (request.nextUrl.pathname.startsWith('/protected') && !user) {\n const url = request.nextUrl.clone()\n url.pathname = '/auth/passwordless'\n return NextResponse.redirect(url)\n }\n\n return supabaseResponse\n}\n\nexport const config = {\n matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],\n}\n",
+ "type": "registry:file",
+ "target": "middleware.ts"
+ },
+ {
+ "path": "registry/default/clients/nextjs/lib/supabase/client.ts",
+ "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n",
+ "type": "registry:lib"
+ },
+ {
+ "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts",
+ "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n",
+ "type": "registry:lib"
+ },
+ {
+ "path": "registry/default/clients/nextjs/lib/supabase/server.ts",
+ "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n",
+ "type": "registry:lib"
+ }
+ ],
+ "envVars": {
+ "NEXT_PUBLIC_SUPABASE_URL": "",
+ "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": ""
+ },
+ "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`."
+}
\ No newline at end of file
diff --git a/apps/ui-library/public/r/passwordless-auth-react-router.json b/apps/ui-library/public/r/passwordless-auth-react-router.json
new file mode 100644
index 00000000000..bd2abdc3592
--- /dev/null
+++ b/apps/ui-library/public/r/passwordless-auth-react-router.json
@@ -0,0 +1,86 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "passwordless-auth-react-router",
+ "type": "registry:block",
+ "title": "Passwordless Auth for React Router and Supabase",
+ "description": "Passwordless authentication for React Router using Supabase Auth. Supports both Magic Link and OTP (One-Time Password) login methods.",
+ "dependencies": [
+ "@supabase/ssr@latest",
+ "@react-router/dev@latest",
+ "@react-router/fs-routes@latest",
+ "@supabase/supabase-js@latest"
+ ],
+ "registryDependencies": [
+ "button",
+ "card",
+ "input",
+ "label"
+ ],
+ "files": [
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/components/passwordless-login-form.tsx",
+ "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/react-router/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { useNavigate } from 'react-router'\nimport { useState } from 'react'\n\ntype LoginMethod = 'magic-link' | 'otp'\n\ninterface PasswordlessLoginFormProps extends React.ComponentPropsWithoutRef<'div'> {\n method?: LoginMethod\n}\n\nexport function PasswordlessLoginForm({\n className,\n method = 'magic-link',\n ...props\n}: PasswordlessLoginFormProps) {\n const [email, setEmail] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const [isMagicLinkSent, setIsMagicLinkSent] = useState(false)\n const navigate = useNavigate()\n const supabase = createClient()\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n setIsMagicLinkSent(false)\n\n try {\n if (method === 'magic-link') {\n const { error } = await supabase.auth.signInWithOtp({\n email,\n options: {\n shouldCreateUser: true,\n emailRedirectTo: `${window.location.origin}/auth/confirm`,\n },\n })\n if (error) throw error\n setIsMagicLinkSent(true)\n } else {\n // OTP method\n const { error } = await supabase.auth.signInWithOtp({\n email,\n options: {\n shouldCreateUser: true,\n },\n })\n if (error) throw error\n // Navigate to OTP verification page\n navigate(`/verify-otp?email=${encodeURIComponent(email)}`)\n }\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n \n
\n \n Sign up \n Sign in or create a new account \n \n \n {isMagicLinkSent ? (\n \n
Check your email!
\n
\n We've sent a magic link to {email} . Click\n the link in the email to sign in.\n
\n
{\n setIsMagicLinkSent(false)\n setEmail('')\n }}\n >\n Send another link\n \n
\n ) : (\n \n )}\n \n \n
\n )\n}\n",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/components/magic-link-form.tsx",
+ "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/react-router/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { useState } from 'react'\n\nexport function MagicLinkForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const [isSuccess, setIsSuccess] = useState(false)\n const supabase = createClient()\n\n const handleMagicLinkRequest = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n setIsSuccess(false)\n\n try {\n const { error } = await supabase.auth.signInWithOtp({\n email,\n options: {\n // Set this to false if you do not want the user to be automatically signed up\n shouldCreateUser: true,\n emailRedirectTo: `${window.location.origin}/auth/confirm`,\n },\n })\n if (error) throw error\n setIsSuccess(true)\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n \n
\n \n Magic Link Login \n \n Enter your email to receive a magic link for passwordless login\n \n \n \n {isSuccess ? (\n \n
Check your email!
\n
\n We've sent a magic link to {email} . Click the\n link in the email to sign in.\n
\n
\n ) : (\n \n )}\n \n \n
\n )\n}\n",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/components/otp-request-form.tsx",
+ "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/react-router/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { useNavigate } from 'react-router'\nimport { useState } from 'react'\n\nexport function OTPRequestForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const navigate = useNavigate()\n const supabase = createClient()\n\n const handleOTPRequest = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithOtp({\n email,\n options: {\n // Set this to false if you do not want the user to be automatically signed up\n shouldCreateUser: true,\n },\n })\n if (error) throw error\n // Navigate to OTP verification page\n navigate(`/verify-otp?email=${encodeURIComponent(email)}`)\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n \n
\n \n OTP Login \n \n Enter your email to receive a one-time password for passwordless login\n \n \n \n \n \n \n
\n )\n}\n",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/components/otp-verify-form.tsx",
+ "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/react-router/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { useNavigate, useSearchParams } from 'react-router'\nimport { useState } from 'react'\n\nexport function OTPVerifyForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [searchParams] = useSearchParams()\n const email = searchParams.get('email') || ''\n const [otp, setOTP] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const [isResending, setIsResending] = useState(false)\n const navigate = useNavigate()\n const supabase = createClient()\n\n const handleVerifyOTP = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.verifyOtp({\n email,\n token: otp,\n type: 'email',\n })\n if (error) throw error\n // Redirect to protected page after successful verification\n navigate('/protected')\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n const handleResendOTP = async () => {\n setIsResending(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithOtp({\n email,\n options: {\n shouldCreateUser: false,\n },\n })\n if (error) throw error\n setError(null)\n // Show success message\n alert('A new verification code has been sent to your email.')\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'Failed to resend code')\n } finally {\n setIsResending(false)\n }\n }\n\n return (\n \n
\n \n Verify Your Email \n \n Enter the 6-digit code sent to{' '}\n {email} \n \n \n \n \n \n \n
\n )\n}\n",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/app/routes/auth.confirm.tsx",
+ "content": "import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { type EmailOtpType } from '@supabase/supabase-js'\nimport { type LoaderFunctionArgs, redirect } from 'react-router'\n\nexport async function loader({ request }: LoaderFunctionArgs) {\n const requestUrl = new URL(request.url)\n const token_hash = requestUrl.searchParams.get('token_hash')\n const type = requestUrl.searchParams.get('type') as EmailOtpType | null\n const _next = requestUrl.searchParams.get('next')\n const next = _next?.startsWith('/') ? _next : '/protected'\n\n if (token_hash && type) {\n const { supabase, headers } = createClient(request)\n const { error } = await supabase.auth.verifyOtp({\n type,\n token_hash,\n })\n if (!error) {\n return redirect(next, { headers })\n } else {\n return redirect(`/auth/error?error=${error?.message}`)\n }\n }\n\n // redirect the user to an error page with some instructions\n return redirect(`/auth/error?error=No token hash or type`)\n}\n",
+ "type": "registry:file",
+ "target": "app/routes/auth.confirm.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/app/routes/auth.error.tsx",
+ "content": "import {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Button } from '@/registry/default/components/ui/button'\nimport { Link } from 'react-router'\n\nexport default function AuthErrorPage() {\n return (\n \n
\n
\n \n Authentication Error \n There was an error authenticating your account \n \n \n \n
\n The authentication link may have expired or is invalid. Please try requesting a new\n one.\n
\n
\n Try again\n \n
\n \n \n
\n
\n )\n}\n",
+ "type": "registry:file",
+ "target": "app/routes/auth.error.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/app/routes/passwordless.tsx",
+ "content": "import { PasswordlessLoginForm } from '@/registry/default/blocks/passwordless-auth-react-router/components/passwordless-login-form'\n\nexport default function PasswordlessPage() {\n return (\n \n )\n}\n",
+ "type": "registry:file",
+ "target": "app/routes/passwordless.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/app/routes/verify-otp.tsx",
+ "content": "import { OTPVerifyForm } from '@/registry/default/blocks/passwordless-auth-react-router/components/otp-verify-form'\n\nexport default function VerifyOTPPage() {\n return (\n \n )\n}\n",
+ "type": "registry:file",
+ "target": "app/routes/verify-otp.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/app/routes/protected.tsx",
+ "content": "import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { type LoaderFunctionArgs, redirect, useLoaderData } from 'react-router'\n\nexport const loader = async ({ request }: LoaderFunctionArgs) => {\n const { supabase } = createClient(request)\n\n const { data, error } = await supabase.auth.getUser()\n if (error || !data?.user) {\n return redirect('/passwordless')\n }\n\n return data\n}\n\nexport default function ProtectedPage() {\n let data = useLoaderData()\n\n return (\n \n
\n
\n \n Protected Page \n You are logged in \n \n \n \n
\n
Email
\n
{data.user.email}
\n
\n
\n
User ID
\n
{data.user.id}
\n
\n
\n
\n \n \n
\n
\n )\n}\n",
+ "type": "registry:file",
+ "target": "app/routes/protected.tsx"
+ },
+ {
+ "path": "registry/default/clients/react-router/lib/supabase/client.ts",
+ "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n",
+ "type": "registry:lib"
+ },
+ {
+ "path": "registry/default/clients/react-router/lib/supabase/server.ts",
+ "content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n",
+ "type": "registry:lib"
+ }
+ ],
+ "envVars": {
+ "VITE_SUPABASE_URL": "",
+ "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": ""
+ },
+ "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`."
+}
\ No newline at end of file
diff --git a/apps/ui-library/public/r/passwordless-auth-react.json b/apps/ui-library/public/r/passwordless-auth-react.json
new file mode 100644
index 00000000000..f8c6b46a30d
--- /dev/null
+++ b/apps/ui-library/public/r/passwordless-auth-react.json
@@ -0,0 +1,48 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "passwordless-auth-react",
+ "type": "registry:block",
+ "title": "Passwordless Auth for React and Supabase",
+ "description": "Passwordless authentication for React using Supabase Auth. Supports both Magic Link and OTP (One-Time Password) login methods. Components only, no routing included.",
+ "dependencies": [
+ "@supabase/supabase-js@latest"
+ ],
+ "registryDependencies": [
+ "button",
+ "card",
+ "input",
+ "label"
+ ],
+ "files": [
+ {
+ "path": "registry/default/blocks/passwordless-auth-react/components/passwordless-login-form.tsx",
+ "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/react/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { useState } from 'react'\n\ntype LoginMethod = 'magic-link' | 'otp'\n\ninterface PasswordlessLoginFormProps extends React.ComponentPropsWithoutRef<'div'> {\n method?: LoginMethod\n onOtpRequested?: (email: string) => void\n}\n\nexport function PasswordlessLoginForm({\n className,\n method = 'magic-link',\n onOtpRequested,\n ...props\n}: PasswordlessLoginFormProps) {\n const [email, setEmail] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const [isMagicLinkSent, setIsMagicLinkSent] = useState(false)\n const supabase = createClient()\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n setIsMagicLinkSent(false)\n\n try {\n if (method === 'magic-link') {\n const { error } = await supabase.auth.signInWithOtp({\n email,\n options: {\n shouldCreateUser: true,\n emailRedirectTo: `${window.location.origin}/auth/confirm`,\n },\n })\n if (error) throw error\n setIsMagicLinkSent(true)\n } else {\n // OTP method\n const { error } = await supabase.auth.signInWithOtp({\n email,\n options: {\n shouldCreateUser: true,\n },\n })\n if (error) throw error\n // Call callback to notify parent component\n if (onOtpRequested) {\n onOtpRequested(email)\n }\n }\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n \n
\n \n Sign up \n Sign in or create a new account \n \n \n {isMagicLinkSent ? (\n \n
Check your email!
\n
\n We've sent a magic link to {email} . Click\n the link in the email to sign in.\n
\n
{\n setIsMagicLinkSent(false)\n setEmail('')\n }}\n >\n Send another link\n \n
\n ) : (\n \n )}\n \n \n
\n )\n}\n",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react/components/magic-link-form.tsx",
+ "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/react/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { useState } from 'react'\n\nexport function MagicLinkForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const [isSuccess, setIsSuccess] = useState(false)\n const supabase = createClient()\n\n const handleMagicLinkRequest = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n setIsSuccess(false)\n\n try {\n const { error } = await supabase.auth.signInWithOtp({\n email,\n options: {\n // Set this to false if you do not want the user to be automatically signed up\n shouldCreateUser: true,\n emailRedirectTo: `${window.location.origin}/auth/confirm`,\n },\n })\n if (error) throw error\n setIsSuccess(true)\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n \n
\n \n Magic Link Login \n \n Enter your email to receive a magic link for passwordless login\n \n \n \n {isSuccess ? (\n \n
Check your email!
\n
\n We've sent a magic link to {email} . Click the\n link in the email to sign in.\n
\n
\n ) : (\n \n )}\n \n \n
\n )\n}\n",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react/components/otp-request-form.tsx",
+ "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/react/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { useState } from 'react'\n\ninterface OTPRequestFormProps extends React.ComponentPropsWithoutRef<'div'> {\n onOtpRequested?: (email: string) => void\n}\n\nexport function OTPRequestForm({ className, onOtpRequested, ...props }: OTPRequestFormProps) {\n const [email, setEmail] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const supabase = createClient()\n\n const handleOTPRequest = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithOtp({\n email,\n options: {\n // Set this to false if you do not want the user to be automatically signed up\n shouldCreateUser: true,\n },\n })\n if (error) throw error\n // Call callback to notify parent component\n if (onOtpRequested) {\n onOtpRequested(email)\n }\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n \n
\n \n OTP Login \n \n Enter your email to receive a one-time password for passwordless login\n \n \n \n \n \n \n
\n )\n}\n",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react/components/otp-verify-form.tsx",
+ "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/react/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { useState } from 'react'\n\ninterface OTPVerifyFormProps extends React.ComponentPropsWithoutRef<'div'> {\n email: string\n onVerified?: () => void\n}\n\nexport function OTPVerifyForm({ className, email, onVerified, ...props }: OTPVerifyFormProps) {\n const [otp, setOTP] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const [isResending, setIsResending] = useState(false)\n const supabase = createClient()\n\n const handleVerifyOTP = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.verifyOtp({\n email,\n token: otp,\n type: 'email',\n })\n if (error) throw error\n // Call callback to notify parent component\n if (onVerified) {\n onVerified()\n }\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n const handleResendOTP = async () => {\n setIsResending(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithOtp({\n email,\n options: {\n shouldCreateUser: false,\n },\n })\n if (error) throw error\n setError(null)\n // Show success message\n alert('A new verification code has been sent to your email.')\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'Failed to resend code')\n } finally {\n setIsResending(false)\n }\n }\n\n return (\n \n
\n \n Verify Your Email \n \n Enter the 6-digit code sent to{' '}\n {email} \n \n \n \n \n \n \n
\n )\n}\n",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/clients/react/lib/supabase/client.ts",
+ "content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n",
+ "type": "registry:lib"
+ }
+ ],
+ "envVars": {
+ "VITE_SUPABASE_URL": "",
+ "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": ""
+ },
+ "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`."
+}
\ No newline at end of file
diff --git a/apps/ui-library/public/r/passwordless-auth-tanstack.json b/apps/ui-library/public/r/passwordless-auth-tanstack.json
new file mode 100644
index 00000000000..0d76a017882
--- /dev/null
+++ b/apps/ui-library/public/r/passwordless-auth-tanstack.json
@@ -0,0 +1,95 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "passwordless-auth-tanstack",
+ "type": "registry:block",
+ "title": "Passwordless Auth for TanStack and Supabase",
+ "description": "Passwordless authentication for TanStack using Supabase Auth. Supports both Magic Link and OTP (One-Time Password) login methods.",
+ "dependencies": [
+ "@supabase/ssr@latest",
+ "@supabase/supabase-js@latest"
+ ],
+ "registryDependencies": [
+ "button",
+ "card",
+ "input",
+ "label"
+ ],
+ "files": [
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/components/passwordless-login-form.tsx",
+ "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { useNavigate } from '@tanstack/react-router'\nimport { useState } from 'react'\n\ntype LoginMethod = 'magic-link' | 'otp'\n\ninterface PasswordlessLoginFormProps extends React.ComponentPropsWithoutRef<'div'> {\n method?: LoginMethod\n}\n\nexport function PasswordlessLoginForm({\n className,\n method = 'magic-link',\n ...props\n}: PasswordlessLoginFormProps) {\n const [email, setEmail] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const [isMagicLinkSent, setIsMagicLinkSent] = useState(false)\n const navigate = useNavigate()\n const supabase = createClient()\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n setIsMagicLinkSent(false)\n\n try {\n if (method === 'magic-link') {\n const { error } = await supabase.auth.signInWithOtp({\n email,\n options: {\n shouldCreateUser: true,\n emailRedirectTo: `${window.location.origin}/auth/confirm`,\n },\n })\n if (error) throw error\n setIsMagicLinkSent(true)\n } else {\n // OTP method\n const { error } = await supabase.auth.signInWithOtp({\n email,\n options: {\n shouldCreateUser: true,\n },\n })\n if (error) throw error\n // Navigate to OTP verification page\n navigate({ to: '/verify-otp', search: { email } })\n }\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n \n
\n \n Sign up \n Sign in or create a new account \n \n \n {isMagicLinkSent ? (\n \n
Check your email!
\n
\n We've sent a magic link to {email} . Click\n the link in the email to sign in.\n
\n
{\n setIsMagicLinkSent(false)\n setEmail('')\n }}\n >\n Send another link\n \n
\n ) : (\n \n )}\n \n \n
\n )\n}\n",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/components/magic-link-form.tsx",
+ "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { useState } from 'react'\n\nexport function MagicLinkForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const [isSuccess, setIsSuccess] = useState(false)\n const supabase = createClient()\n\n const handleMagicLinkRequest = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n setIsSuccess(false)\n\n try {\n const { error } = await supabase.auth.signInWithOtp({\n email,\n options: {\n // Set this to false if you do not want the user to be automatically signed up\n shouldCreateUser: true,\n emailRedirectTo: `${window.location.origin}/auth/confirm`,\n },\n })\n if (error) throw error\n setIsSuccess(true)\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n \n
\n \n Magic Link Login \n \n Enter your email to receive a magic link for passwordless login\n \n \n \n {isSuccess ? (\n \n
Check your email!
\n
\n We've sent a magic link to {email} . Click the\n link in the email to sign in.\n
\n
\n ) : (\n \n )}\n \n \n
\n )\n}\n",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/components/otp-request-form.tsx",
+ "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { useNavigate } from '@tanstack/react-router'\nimport { useState } from 'react'\n\nexport function OTPRequestForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const navigate = useNavigate()\n const supabase = createClient()\n\n const handleOTPRequest = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithOtp({\n email,\n options: {\n // Set this to false if you do not want the user to be automatically signed up\n shouldCreateUser: true,\n },\n })\n if (error) throw error\n // Navigate to OTP verification page\n navigate({ to: '/verify-otp', search: { email } })\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n \n
\n \n OTP Login \n \n Enter your email to receive a one-time password for passwordless login\n \n \n \n \n \n \n
\n )\n}\n",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/components/otp-verify-form.tsx",
+ "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport { useNavigate, useSearch } from '@tanstack/react-router'\nimport { useState } from 'react'\n\nexport function OTPVerifyForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const search = useSearch({ strict: false })\n const email = (search as { email?: string })?.email || ''\n const [otp, setOTP] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const [isResending, setIsResending] = useState(false)\n const navigate = useNavigate()\n const supabase = createClient()\n\n const handleVerifyOTP = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.verifyOtp({\n email,\n token: otp,\n type: 'email',\n })\n if (error) throw error\n // Redirect to protected page after successful verification\n navigate({ to: '/protected' })\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n const handleResendOTP = async () => {\n setIsResending(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithOtp({\n email,\n options: {\n shouldCreateUser: false,\n },\n })\n if (error) throw error\n setError(null)\n // Show success message\n alert('A new verification code has been sent to your email.')\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'Failed to resend code')\n } finally {\n setIsResending(false)\n }\n }\n\n return (\n \n
\n \n Verify Your Email \n \n Enter the 6-digit code sent to{' '}\n {email} \n \n \n \n \n \n \n
\n )\n}\n",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/routes/passwordless.tsx",
+ "content": "import { PasswordlessLoginForm } from '@/registry/default/blocks/passwordless-auth-tanstack/components/passwordless-login-form'\nimport { createFileRoute } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/passwordless')({\n component: PasswordlessPage,\n})\n\nfunction PasswordlessPage() {\n return (\n \n )\n}\n",
+ "type": "registry:file",
+ "target": "routes/passwordless.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/routes/auth/error.tsx",
+ "content": "import {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Button } from '@/registry/default/components/ui/button'\nimport { Link, createFileRoute } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/auth/error')({\n component: AuthErrorPage,\n})\n\nfunction AuthErrorPage() {\n return (\n \n
\n
\n \n Authentication Error \n There was an error authenticating your account \n \n \n \n
\n The authentication link may have expired or is invalid. Please try requesting a new\n one.\n
\n
\n Try again\n \n
\n \n \n
\n
\n )\n}\n",
+ "type": "registry:file",
+ "target": "routes/auth/error.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/routes/_protected.tsx",
+ "content": "import { fetchUserServerFn } from '@/registry/default/blocks/passwordless-auth-tanstack/lib/supabase/fetch-user-server-fn'\nimport { createFileRoute, Outlet, redirect } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/_protected')({\n beforeLoad: async () => {\n const user = await fetchUserServerFn()\n if (!user) {\n throw redirect({\n to: '/passwordless',\n })\n }\n return { user }\n },\n component: () => ,\n})\n",
+ "type": "registry:file",
+ "target": "routes/_protected.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/routes/_protected/protected.tsx",
+ "content": "import { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { createFileRoute, useRouter } from '@tanstack/react-router'\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'\n\nexport const Route = createFileRoute('/_protected/protected')({\n component: ProtectedPage,\n})\n\nfunction ProtectedPage() {\n const router = useRouter()\n const { user } = Route.useRouteContext()\n const supabase = createClient()\n\n const handleSignOut = async () => {\n await supabase.auth.signOut()\n router.navigate({ to: '/passwordless' })\n }\n\n return (\n \n
\n
\n \n Protected Page \n You are logged in \n \n \n \n
\n
Email
\n
{user.email}
\n
\n
\n
\n Sign out\n \n
\n \n \n
\n
\n )\n}\n",
+ "type": "registry:file",
+ "target": "routes/_protected/protected.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/routes/auth/confirm.ts",
+ "content": "import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/server'\nimport { type EmailOtpType } from '@supabase/supabase-js'\nimport { createFileRoute, redirect } from '@tanstack/react-router'\nimport { createServerFn } from '@tanstack/react-start'\nimport { getWebRequest } from '@tanstack/react-start/server'\n\nconst confirmFn = createServerFn({ method: 'GET' })\n .validator((searchParams: unknown) => {\n if (\n searchParams &&\n typeof searchParams === 'object' &&\n 'token_hash' in searchParams &&\n 'type' in searchParams\n ) {\n return searchParams\n }\n throw new Error('Invalid search params')\n })\n .handler(async (ctx) => {\n const request = getWebRequest()\n\n if (!request) {\n throw redirect({ to: `/auth/error`, search: { error: 'No request' } })\n }\n\n const searchParams = ctx.data\n const token_hash = searchParams['token_hash'] as string\n const type = searchParams['type'] as EmailOtpType | null\n const _next = searchParams['next'] as string | undefined\n const next = _next?.startsWith('/') ? _next : '/protected'\n\n if (token_hash && type) {\n const supabase = createClient()\n\n const { error } = await supabase.auth.verifyOtp({\n type,\n token_hash,\n })\n if (!error) {\n // redirect user to specified redirect URL or protected page\n throw redirect({ href: next })\n } else {\n // redirect the user to an error page with some instructions\n throw redirect({\n to: `/auth/error`,\n search: { error: error?.message },\n })\n }\n }\n\n // redirect the user to an error page with some instructions\n throw redirect({\n to: `/auth/error`,\n search: { error: 'No token hash or type' },\n })\n })\n\nexport const Route = createFileRoute('/auth/confirm')({\n preload: false,\n loader: (opts) => confirmFn({ data: opts.location.search }),\n})\n",
+ "type": "registry:file",
+ "target": "routes/auth/confirm.ts"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/routes/verify-otp.tsx",
+ "content": "import { OTPVerifyForm } from '@/registry/default/blocks/passwordless-auth-tanstack/components/otp-verify-form'\nimport { createFileRoute } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/verify-otp')({\n component: VerifyOTPPage,\n validateSearch: (search: Record): { email?: string } => {\n return {\n email: (search.email as string) || undefined,\n }\n },\n})\n\nfunction VerifyOTPPage() {\n return (\n \n )\n}\n",
+ "type": "registry:file",
+ "target": "routes/verify-otp.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/lib/supabase/fetch-user-server-fn.ts",
+ "content": "import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/server'\nimport type { Factor, User } from '@supabase/supabase-js'\nimport { createServerFn } from '@tanstack/react-start'\ntype SSRSafeUser = User & {\n factors: (Factor & { factor_type: 'phone' | 'totp' })[]\n}\n\nexport const fetchUserServerFn: () => Promise = createServerFn({\n method: 'GET',\n}).handler(async () => {\n const supabase = createClient()\n const { data, error } = await supabase.auth.getUser()\n\n if (error) {\n return null\n }\n\n return data.user as SSRSafeUser\n})\n",
+ "type": "registry:lib"
+ },
+ {
+ "path": "registry/default/clients/tanstack/lib/supabase/client.ts",
+ "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n",
+ "type": "registry:lib"
+ },
+ {
+ "path": "registry/default/clients/tanstack/lib/supabase/server.ts",
+ "content": "import { createServerClient } from '@supabase/ssr'\nimport { parseCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(parseCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\n}\n",
+ "type": "registry:lib"
+ }
+ ],
+ "envVars": {
+ "VITE_SUPABASE_URL": "",
+ "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": ""
+ },
+ "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`."
+}
\ No newline at end of file
diff --git a/apps/ui-library/public/r/platform-kit-nextjs.json b/apps/ui-library/public/r/platform-kit-nextjs.json
index f840a00a150..1df40283f0f 100644
--- a/apps/ui-library/public/r/platform-kit-nextjs.json
+++ b/apps/ui-library/public/r/platform-kit-nextjs.json
@@ -50,7 +50,7 @@
},
{
"path": "registry/default/platform/platform-kit-nextjs/app/api/supabase-proxy/[...path]/route.ts",
- "content": "import { NextResponse } from 'next/server'\n\nasync function forwardToSupabaseAPI(request: Request, method: string, params: { path: string[] }) {\n if (!process.env.SUPABASE_MANAGEMENT_API_TOKEN) {\n console.error('Supabase Management API token is not configured.')\n return NextResponse.json({ message: 'Server configuration error.' }, { status: 500 })\n }\n\n const { path } = params\n const apiPath = path.join('/')\n\n const url = new URL(request.url)\n url.protocol = 'https'\n url.hostname = 'api.supabase.com'\n url.port = '443'\n url.pathname = apiPath\n\n const projectRef = path[2]\n\n // Implement your permission check here (e.g. check if the user is a member of the project)\n // In this example, everyone can access all projects\n const userHasPermissionForProject = Boolean(projectRef)\n\n if (!userHasPermissionForProject) {\n return NextResponse.json(\n { message: 'You do not have permission to access this project.' },\n { status: 403 }\n )\n }\n\n try {\n const forwardHeaders: HeadersInit = {\n Authorization: `Bearer ${process.env.SUPABASE_MANAGEMENT_API_TOKEN}`,\n }\n\n // Copy relevant headers from the original request\n const contentType = request.headers.get('content-type')\n if (contentType) {\n forwardHeaders['Content-Type'] = contentType\n }\n\n const fetchOptions: RequestInit = {\n method,\n headers: forwardHeaders,\n }\n\n // Include body for methods that support it\n if (method !== 'GET' && method !== 'HEAD') {\n try {\n const body = await request.text()\n if (body) {\n fetchOptions.body = body\n }\n } catch (error) {\n // Handle cases where body is not readable\n console.warn('Could not read request body:', error)\n }\n }\n\n const response = await fetch(url, fetchOptions)\n\n // Get response body\n const responseText = await response.text()\n let responseData\n\n try {\n responseData = responseText ? JSON.parse(responseText) : null\n } catch {\n responseData = responseText\n }\n\n // Return the response with the same status\n return NextResponse.json(responseData, { status: response.status })\n } catch (error: any) {\n console.error('Supabase API proxy error:', error)\n const errorMessage = error.message || 'An unexpected error occurred.'\n return NextResponse.json({ message: errorMessage }, { status: 500 })\n }\n}\n\nexport async function GET(request: Request, { params }: { params: Promise<{ path: string[] }> }) {\n const resolvedParams = await params\n return forwardToSupabaseAPI(request, 'GET', resolvedParams)\n}\n\nexport async function HEAD(request: Request, { params }: { params: Promise<{ path: string[] }> }) {\n const resolvedParams = await params\n return forwardToSupabaseAPI(request, 'HEAD', resolvedParams)\n}\n\nexport async function POST(request: Request, { params }: { params: Promise<{ path: string[] }> }) {\n const resolvedParams = await params\n return forwardToSupabaseAPI(request, 'POST', resolvedParams)\n}\n\nexport async function PUT(request: Request, { params }: { params: Promise<{ path: string[] }> }) {\n const resolvedParams = await params\n return forwardToSupabaseAPI(request, 'PUT', resolvedParams)\n}\n\nexport async function DELETE(\n request: Request,\n { params }: { params: Promise<{ path: string[] }> }\n) {\n const resolvedParams = await params\n return forwardToSupabaseAPI(request, 'DELETE', resolvedParams)\n}\n\nexport async function PATCH(request: Request, { params }: { params: Promise<{ path: string[] }> }) {\n const resolvedParams = await params\n return forwardToSupabaseAPI(request, 'PATCH', resolvedParams)\n}\n",
+ "content": "import { NextResponse } from 'next/server'\n\nasync function forwardToSupabaseAPI(request: Request, method: string, params: { path: string[] }) {\n // eslint-disable-next-line turbo/no-undeclared-env-vars\n if (!process.env.SUPABASE_MANAGEMENT_API_TOKEN) {\n console.error('Supabase Management API token is not configured.')\n return NextResponse.json({ message: 'Server configuration error.' }, { status: 500 })\n }\n\n const { path } = params\n const apiPath = path.join('/')\n\n const url = new URL(request.url)\n url.protocol = 'https'\n url.hostname = 'api.supabase.com'\n url.port = '443'\n url.pathname = apiPath\n\n const projectRef = path[2]\n\n // Implement your permission check here (e.g. check if the user is a member of the project)\n // In this example, everyone can access all projects\n const userHasPermissionForProject = Boolean(projectRef)\n\n if (!userHasPermissionForProject) {\n return NextResponse.json(\n { message: 'You do not have permission to access this project.' },\n { status: 403 }\n )\n }\n\n try {\n const forwardHeaders: HeadersInit = {\n // eslint-disable-next-line turbo/no-undeclared-env-vars\n Authorization: `Bearer ${process.env.SUPABASE_MANAGEMENT_API_TOKEN}`,\n }\n\n // Copy relevant headers from the original request\n const contentType = request.headers.get('content-type')\n if (contentType) {\n forwardHeaders['Content-Type'] = contentType\n }\n\n const fetchOptions: RequestInit = {\n method,\n headers: forwardHeaders,\n }\n\n // Include body for methods that support it\n if (method !== 'GET' && method !== 'HEAD') {\n try {\n const body = await request.text()\n if (body) {\n fetchOptions.body = body\n }\n } catch (error) {\n // Handle cases where body is not readable\n console.warn('Could not read request body:', error)\n }\n }\n\n const response = await fetch(url, fetchOptions)\n\n // Get response body\n const responseText = await response.text()\n let responseData\n\n try {\n responseData = responseText ? JSON.parse(responseText) : null\n } catch {\n responseData = responseText\n }\n\n // Return the response with the same status\n return NextResponse.json(responseData, { status: response.status })\n } catch (error: any) {\n console.error('Supabase API proxy error:', error)\n const errorMessage = error.message || 'An unexpected error occurred.'\n return NextResponse.json({ message: errorMessage }, { status: 500 })\n }\n}\n\nexport async function GET(request: Request, { params }: { params: Promise<{ path: string[] }> }) {\n const resolvedParams = await params\n return forwardToSupabaseAPI(request, 'GET', resolvedParams)\n}\n\nexport async function HEAD(request: Request, { params }: { params: Promise<{ path: string[] }> }) {\n const resolvedParams = await params\n return forwardToSupabaseAPI(request, 'HEAD', resolvedParams)\n}\n\nexport async function POST(request: Request, { params }: { params: Promise<{ path: string[] }> }) {\n const resolvedParams = await params\n return forwardToSupabaseAPI(request, 'POST', resolvedParams)\n}\n\nexport async function PUT(request: Request, { params }: { params: Promise<{ path: string[] }> }) {\n const resolvedParams = await params\n return forwardToSupabaseAPI(request, 'PUT', resolvedParams)\n}\n\nexport async function DELETE(\n request: Request,\n { params }: { params: Promise<{ path: string[] }> }\n) {\n const resolvedParams = await params\n return forwardToSupabaseAPI(request, 'DELETE', resolvedParams)\n}\n\nexport async function PATCH(request: Request, { params }: { params: Promise<{ path: string[] }> }) {\n const resolvedParams = await params\n return forwardToSupabaseAPI(request, 'PATCH', resolvedParams)\n}\n",
"type": "registry:page",
"target": "app/api/supabase-proxy/[...path]/route.ts"
},
@@ -106,7 +106,7 @@
},
{
"path": "registry/default/platform/platform-kit-nextjs/components/supabase-manager/suggestions.tsx",
- "content": "'use client'\n\nimport { useGetSuggestions } from '@/registry/default/platform/platform-kit-nextjs/hooks/use-suggestions'\nimport { Alert, AlertDescription, AlertTitle } from '@/registry/default/components/ui/alert'\nimport { Terminal } from 'lucide-react'\nimport { Badge } from '@/registry/default/components/ui/badge'\nimport { useMemo } from 'react'\n\nimport ReactMarkdown from 'react-markdown'\nimport { Skeleton } from '@/registry/default/components/ui/skeleton'\n\nexport function SuggestionsManager({ projectRef }: { projectRef: string }) {\n const { data: suggestions, isLoading, error } = useGetSuggestions(projectRef)\n\n const sortedSuggestions = useMemo(() => {\n if (!suggestions) return []\n const levelOrder = { ERROR: 1, WARN: 2, INFO: 3 }\n return [...suggestions].sort((a: any, b: any) => {\n const levelA = levelOrder[a.level as keyof typeof levelOrder] || 99\n const levelB = levelOrder[b.level as keyof typeof levelOrder] || 99\n return levelA - levelB\n })\n }, [suggestions])\n\n const getBadgeVariant = (level: 'ERROR' | 'WARN' | 'INFO') => {\n switch (level) {\n case 'ERROR':\n return 'destructive'\n case 'WARN':\n return 'secondary'\n default:\n return 'outline'\n }\n }\n\n return (\n \n
Suggestions \n
\n Improve your project's security and performance.\n
\n {isLoading && (\n
\n \n \n \n \n
\n )}\n {error && (\n
\n \n Error fetching suggestions \n \n {(error as any)?.message || 'An unexpected error occurred. Please try again.'}\n \n \n )}\n {suggestions && (\n
\n {sortedSuggestions.length > 0 ? (\n
\n {sortedSuggestions.map((suggestion: any) => (\n
\n
\n
\n
{suggestion.title} \n
\n \n {suggestion.level}\n \n {suggestion.type && (\n \n {suggestion.type.charAt(0).toUpperCase() + suggestion.type.slice(1)}\n \n )}\n
\n
\n
\n
\n {children}\n \n ) : (\n \n {children}\n \n )\n },\n }}\n >\n {suggestion.detail}\n \n
\n
\n
\n ))}\n
\n ) : (\n
\n \n No suggestions found \n \n Your project looks good! No suggestions at this time.\n \n \n )}\n
\n )}\n
\n )\n}\n",
+ "content": "'use client'\n\nimport { useGetSuggestions } from '@/registry/default/platform/platform-kit-nextjs/hooks/use-suggestions'\nimport { Alert, AlertDescription, AlertTitle } from '@/registry/default/components/ui/alert'\nimport { Terminal } from 'lucide-react'\nimport { Badge } from '@/registry/default/components/ui/badge'\nimport { useMemo } from 'react'\n\nimport ReactMarkdown from 'react-markdown'\nimport { Skeleton } from '@/registry/default/components/ui/skeleton'\n\nexport function SuggestionsManager({ projectRef }: { projectRef: string }) {\n const { data: suggestions, isLoading, error } = useGetSuggestions(projectRef)\n\n const sortedSuggestions = useMemo(() => {\n if (!suggestions) return []\n const levelOrder = { ERROR: 1, WARN: 2, INFO: 3 }\n return [...suggestions].sort((a: any, b: any) => {\n const levelA = levelOrder[a.level as keyof typeof levelOrder] || 99\n const levelB = levelOrder[b.level as keyof typeof levelOrder] || 99\n return levelA - levelB\n })\n }, [suggestions])\n\n const getBadgeVariant = (level: 'ERROR' | 'WARN' | 'INFO') => {\n switch (level) {\n case 'ERROR':\n return 'destructive'\n case 'WARN':\n return 'secondary'\n default:\n return 'outline'\n }\n }\n\n return (\n \n
Suggestions \n
\n Improve your project's security and performance.\n
\n {isLoading && (\n
\n \n \n \n \n
\n )}\n {error && (\n
\n \n Error fetching suggestions \n \n {(error as any)?.message || 'An unexpected error occurred. Please try again.'}\n \n \n )}\n {suggestions && (\n
\n {sortedSuggestions.length > 0 ? (\n
\n {sortedSuggestions.map((suggestion: any) => (\n
\n
\n
\n
{suggestion.title} \n
\n \n {suggestion.level}\n \n {suggestion.type && (\n \n {suggestion.type.charAt(0).toUpperCase() + suggestion.type.slice(1)}\n \n )}\n
\n
\n
\n
\n {children}\n \n ) : (\n \n {children}\n \n )\n },\n }}\n >\n {suggestion.detail}\n \n
\n
\n
\n ))}\n
\n ) : (\n
\n \n No suggestions found \n \n Your project looks good! No suggestions at this time.\n \n \n )}\n
\n )}\n
\n )\n}\n",
"type": "registry:component"
},
{
diff --git a/apps/ui-library/public/r/registry.json b/apps/ui-library/public/r/registry.json
index 595c4484a22..5943b01a622 100644
--- a/apps/ui-library/public/r/registry.json
+++ b/apps/ui-library/public/r/registry.json
@@ -319,6 +319,295 @@
"VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": ""
}
},
+ {
+ "name": "passwordless-auth-nextjs",
+ "type": "registry:block",
+ "title": "Passwordless Auth for Next.js and Supabase",
+ "description": "Passwordless authentication for Next.js using Supabase Auth. Supports both Magic Link and OTP (One-Time Password) login methods.",
+ "registryDependencies": [
+ "button",
+ "card",
+ "input",
+ "label",
+ "link"
+ ],
+ "dependencies": [
+ "@supabase/ssr@latest",
+ "@supabase/supabase-js@latest"
+ ],
+ "files": [
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/components/magic-link-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/components/otp-request-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/components/otp-verify-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/components/passwordless-login-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/auth/magic-link/page.tsx",
+ "type": "registry:page",
+ "target": "app/auth/magic-link/page.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/auth/otp/page.tsx",
+ "type": "registry:page",
+ "target": "app/auth/otp/page.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/auth/verify-otp/page.tsx",
+ "type": "registry:page",
+ "target": "app/auth/verify-otp/page.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/auth/passwordless/page.tsx",
+ "type": "registry:page",
+ "target": "app/auth/passwordless/page.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/auth/confirm/route.ts",
+ "type": "registry:page",
+ "target": "app/auth/confirm/route.ts"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/auth/error/page.tsx",
+ "type": "registry:page",
+ "target": "app/auth/error/page.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/protected/page.tsx",
+ "type": "registry:page",
+ "target": "app/protected/page.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/middleware.ts",
+ "type": "registry:file",
+ "target": "middleware.ts"
+ },
+ {
+ "path": "registry/default/clients/nextjs/lib/supabase/client.ts",
+ "type": "registry:lib"
+ },
+ {
+ "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts",
+ "type": "registry:lib"
+ },
+ {
+ "path": "registry/default/clients/nextjs/lib/supabase/server.ts",
+ "type": "registry:lib"
+ }
+ ],
+ "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.",
+ "envVars": {
+ "NEXT_PUBLIC_SUPABASE_URL": "",
+ "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": ""
+ }
+ },
+ {
+ "name": "passwordless-auth-react",
+ "type": "registry:block",
+ "title": "Passwordless Auth for React and Supabase",
+ "description": "Passwordless authentication for React using Supabase Auth. Supports both Magic Link and OTP (One-Time Password) login methods. Components only, no routing included.",
+ "registryDependencies": [
+ "button",
+ "card",
+ "input",
+ "label"
+ ],
+ "dependencies": [
+ "@supabase/supabase-js@latest"
+ ],
+ "files": [
+ {
+ "path": "registry/default/blocks/passwordless-auth-react/components/passwordless-login-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react/components/magic-link-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react/components/otp-request-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react/components/otp-verify-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/clients/react/lib/supabase/client.ts",
+ "type": "registry:lib"
+ }
+ ],
+ "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.",
+ "envVars": {
+ "VITE_SUPABASE_URL": "",
+ "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": ""
+ }
+ },
+ {
+ "name": "passwordless-auth-react-router",
+ "type": "registry:block",
+ "title": "Passwordless Auth for React Router and Supabase",
+ "description": "Passwordless authentication for React Router using Supabase Auth. Supports both Magic Link and OTP (One-Time Password) login methods.",
+ "registryDependencies": [
+ "button",
+ "card",
+ "input",
+ "label"
+ ],
+ "dependencies": [
+ "@supabase/ssr@latest",
+ "@react-router/dev@latest",
+ "@react-router/fs-routes@latest",
+ "@supabase/supabase-js@latest"
+ ],
+ "files": [
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/components/passwordless-login-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/components/magic-link-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/components/otp-request-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/components/otp-verify-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/app/routes/auth.confirm.tsx",
+ "type": "registry:file",
+ "target": "app/routes/auth.confirm.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/app/routes/auth.error.tsx",
+ "type": "registry:file",
+ "target": "app/routes/auth.error.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/app/routes/passwordless.tsx",
+ "type": "registry:file",
+ "target": "app/routes/passwordless.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/app/routes/verify-otp.tsx",
+ "type": "registry:file",
+ "target": "app/routes/verify-otp.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/app/routes/protected.tsx",
+ "type": "registry:file",
+ "target": "app/routes/protected.tsx"
+ },
+ {
+ "path": "registry/default/clients/react-router/lib/supabase/client.ts",
+ "type": "registry:lib"
+ },
+ {
+ "path": "registry/default/clients/react-router/lib/supabase/server.ts",
+ "type": "registry:lib"
+ }
+ ],
+ "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.",
+ "envVars": {
+ "VITE_SUPABASE_URL": "",
+ "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": ""
+ }
+ },
+ {
+ "name": "passwordless-auth-tanstack",
+ "type": "registry:block",
+ "title": "Passwordless Auth for TanStack and Supabase",
+ "description": "Passwordless authentication for TanStack using Supabase Auth. Supports both Magic Link and OTP (One-Time Password) login methods.",
+ "registryDependencies": [
+ "button",
+ "card",
+ "input",
+ "label"
+ ],
+ "dependencies": [
+ "@supabase/ssr@latest",
+ "@supabase/supabase-js@latest"
+ ],
+ "files": [
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/components/passwordless-login-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/components/magic-link-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/components/otp-request-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/components/otp-verify-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/routes/passwordless.tsx",
+ "type": "registry:file",
+ "target": "routes/passwordless.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/routes/auth/error.tsx",
+ "type": "registry:file",
+ "target": "routes/auth/error.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/routes/_protected.tsx",
+ "type": "registry:file",
+ "target": "routes/_protected.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/routes/_protected/protected.tsx",
+ "type": "registry:file",
+ "target": "routes/_protected/protected.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/routes/auth/confirm.ts",
+ "type": "registry:file",
+ "target": "routes/auth/confirm.ts"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/routes/verify-otp.tsx",
+ "type": "registry:file",
+ "target": "routes/verify-otp.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/lib/supabase/fetch-user-server-fn.ts",
+ "type": "registry:lib"
+ },
+ {
+ "path": "registry/default/clients/tanstack/lib/supabase/client.ts",
+ "type": "registry:lib"
+ },
+ {
+ "path": "registry/default/clients/tanstack/lib/supabase/server.ts",
+ "type": "registry:lib"
+ }
+ ],
+ "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.",
+ "envVars": {
+ "VITE_SUPABASE_URL": "",
+ "VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY": ""
+ }
+ },
{
"name": "social-auth-nextjs",
"type": "registry:block",
diff --git a/apps/ui-library/registry/blocks.ts b/apps/ui-library/registry/blocks.ts
index fc993d76be1..657601e1e8e 100644
--- a/apps/ui-library/registry/blocks.ts
+++ b/apps/ui-library/registry/blocks.ts
@@ -14,6 +14,11 @@ import passwordBasedAuthReactRouter from './default/blocks/password-based-auth-r
import passwordBasedAuthReact from './default/blocks/password-based-auth-react/registry-item.json' with { type: 'json' }
import passwordBasedAuthTanstack from './default/blocks/password-based-auth-tanstack/registry-item.json' with { type: 'json' }
+import passwordlessAuthNextjs from './default/blocks/passwordless-auth-nextjs/registry-item.json' with { type: 'json' }
+import passwordlessAuthReactRouter from './default/blocks/passwordless-auth-react-router/registry-item.json' with { type: 'json' }
+import passwordlessAuthReact from './default/blocks/passwordless-auth-react/registry-item.json' with { type: 'json' }
+import passwordlessAuthTanstack from './default/blocks/passwordless-auth-tanstack/registry-item.json' with { type: 'json' }
+
import socialAuthNextjs from './default/blocks/social-auth-nextjs/registry-item.json' with { type: 'json' }
import socialAuthReactRouter from './default/blocks/social-auth-react-router/registry-item.json' with { type: 'json' }
import socialAuthReact from './default/blocks/social-auth-react/registry-item.json' with { type: 'json' }
@@ -42,6 +47,11 @@ export const blocks = [
registryItemAppend(passwordBasedAuthReactRouter as RegistryItem, [reactRouterClient!]),
registryItemAppend(passwordBasedAuthTanstack as RegistryItem, [tanstackClient!]),
+ registryItemAppend(passwordlessAuthNextjs as RegistryItem, [nextjsClient!]),
+ registryItemAppend(passwordlessAuthReact as RegistryItem, [reactClient!]),
+ registryItemAppend(passwordlessAuthReactRouter as RegistryItem, [reactRouterClient!]),
+ registryItemAppend(passwordlessAuthTanstack as RegistryItem, [tanstackClient!]),
+
registryItemAppend(socialAuthNextjs as RegistryItem, [nextjsClient!]),
registryItemAppend(socialAuthReact as RegistryItem, [reactClient!]),
registryItemAppend(socialAuthReactRouter as RegistryItem, [reactRouterClient!]),
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/GETTING_STARTED.md b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/GETTING_STARTED.md
new file mode 100644
index 00000000000..fb8e67888da
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/GETTING_STARTED.md
@@ -0,0 +1,398 @@
+# Getting Started with Passwordless Authentication
+
+This guide will help you set up passwordless authentication in your Next.js application in under 10 minutes.
+
+## Prerequisites
+
+- A Next.js 13+ application (App Router)
+- A Supabase project ([create one here](https://supabase.com/dashboard))
+- Node.js 18+ installed
+
+## Step-by-Step Setup
+
+### Step 1: Install Dependencies
+
+```bash
+npm install @supabase/supabase-js @supabase/ssr
+```
+
+### Step 2: Configure Environment Variables
+
+Create a `.env.local` file in your project root:
+
+```env
+NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
+NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
+```
+
+You can find these values in your Supabase Dashboard:
+
+1. Go to [Supabase Dashboard](https://supabase.com/dashboard)
+2. Select your project
+3. Go to **Settings** > **API**
+4. Copy the **Project URL** and **anon/public key**
+
+### Step 3: Configure Supabase Authentication
+
+#### A. Enable Email Authentication
+
+1. In Supabase Dashboard, go to **Authentication** > **Providers**
+2. Ensure **Email** is enabled (it's enabled by default)
+
+#### B. Configure Redirect URLs
+
+1. Go to **Authentication** > **URL Configuration**
+2. Add your redirect URLs:
+
+**For Development:**
+
+```
+Site URL: http://localhost:3000
+Redirect URLs:
+ - http://localhost:3000/auth/confirm
+ - http://localhost:3000/protected
+```
+
+**For Production:**
+
+```
+Site URL: https://yourdomain.com
+Redirect URLs:
+ - https://yourdomain.com/auth/confirm
+ - https://yourdomain.com/protected
+```
+
+#### C. Configure Email Template (Choose One)
+
+**Option 1: Magic Link (Default)**
+Go to **Authentication** > **Email Templates** > **Magic Link**
+
+Use this template:
+
+```html
+Magic Link
+Follow this link to login:
+Log In
+```
+
+**Option 2: OTP**
+Go to **Authentication** > **Email Templates** > **Magic Link** (yes, same template)
+
+Replace with this template:
+
+```html
+One Time Login Code
+Please enter this code to sign in:
+{{ .Token }}
+This code will expire in 1 hour.
+```
+
+**Option 3: Support Both**
+You can create a more sophisticated template that shows both the token and link.
+
+### Step 4: Create Supabase Client
+
+Create `lib/supabase/client.ts`:
+
+```typescript
+import { createBrowserClient } from '@supabase/ssr'
+
+export function createClient() {
+ return createBrowserClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
+ )
+}
+```
+
+Create `lib/supabase/server.ts`:
+
+```typescript
+import { createServerClient } from '@supabase/ssr'
+import { cookies } from 'next/headers'
+
+export async function createClient() {
+ const cookieStore = await cookies()
+
+ return createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ getAll() {
+ return cookieStore.getAll()
+ },
+ setAll(cookiesToSet) {
+ try {
+ cookiesToSet.forEach(({ name, value, options }) =>
+ cookieStore.set(name, value, options)
+ )
+ } catch {
+ // The `setAll` method was called from a Server Component.
+ // This can be ignored if you have middleware refreshing
+ // user sessions.
+ }
+ },
+ },
+ }
+ )
+}
+```
+
+### Step 5: Install UI Components
+
+This block uses shadcn/ui components. Install the required components:
+
+```bash
+npx shadcn@latest add button card input label
+```
+
+### Step 6: Copy the Components
+
+Copy the passwordless authentication components to your project:
+
+```bash
+# Create directories
+mkdir -p components/auth
+mkdir -p app/auth/{magic-link,otp,verify-otp,confirm,error,passwordless}
+mkdir -p app/protected
+
+# Copy components (adjust paths as needed)
+cp registry/default/blocks/passwordless-auth-nextjs/components/*.tsx components/auth/
+cp registry/default/blocks/passwordless-auth-nextjs/app/auth/*/page.tsx app/auth/
+cp registry/default/blocks/passwordless-auth-nextjs/app/auth/confirm/route.ts app/auth/confirm/
+```
+
+Or manually copy the files from the `passwordless-auth-nextjs` block.
+
+### Step 7: Add Middleware (Optional but Recommended)
+
+Create or update `middleware.ts` in your project root:
+
+```typescript
+import { createServerClient } from '@supabase/ssr'
+import { NextResponse, type NextRequest } from 'next/server'
+
+export async function middleware(request: NextRequest) {
+ let supabaseResponse = NextResponse.next({
+ request,
+ })
+
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ getAll() {
+ return request.cookies.getAll()
+ },
+ setAll(cookiesToSet) {
+ cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
+ supabaseResponse = NextResponse.next({
+ request,
+ })
+ cookiesToSet.forEach(({ name, value, options }) =>
+ supabaseResponse.cookies.set(name, value, options)
+ )
+ },
+ },
+ }
+ )
+
+ const {
+ data: { user },
+ } = await supabase.auth.getUser()
+
+ // Protect routes that start with /protected
+ if (request.nextUrl.pathname.startsWith('/protected') && !user) {
+ const url = request.nextUrl.clone()
+ url.pathname = '/auth/passwordless'
+ return NextResponse.redirect(url)
+ }
+
+ return supabaseResponse
+}
+
+export const config = {
+ matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
+}
+```
+
+### Step 8: Create Your First Login Page
+
+Choose one of the following implementations:
+
+#### Option A: PasswordlessLoginForm with Magic Link (Recommended)
+
+Create `app/auth/login/page.tsx`:
+
+```tsx
+import { PasswordlessLoginForm } from '@/components/auth/passwordless-login-form'
+
+export default function LoginPage() {
+ return (
+
+ )
+}
+```
+
+**Note:** You can change `method="magic-link"` to `method="otp"` to use OTP authentication instead.
+
+#### Option B: Magic Link Only
+
+Create `app/auth/login/page.tsx`:
+
+```tsx
+import { MagicLinkForm } from '@/components/auth/magic-link-form'
+
+export default function LoginPage() {
+ return (
+
+ )
+}
+```
+
+#### Option C: OTP Only
+
+Create `app/auth/login/page.tsx`:
+
+```tsx
+import { OTPRequestForm } from '@/components/auth/otp-request-form'
+
+export default function LoginPage() {
+ return (
+
+ )
+}
+```
+
+### Step 9: Create a Protected Page
+
+Create `app/protected/page.tsx`:
+
+```tsx
+import { createClient } from '@/lib/supabase/server'
+import { redirect } from 'next/navigation'
+import { Button } from '@/components/ui/button'
+
+export default async function ProtectedPage() {
+ const supabase = await createClient()
+ const {
+ data: { user },
+ } = await supabase.auth.getUser()
+
+ if (!user) {
+ redirect('/auth/login')
+ }
+
+ const handleSignOut = async () => {
+ 'use server'
+ const supabase = await createClient()
+ await supabase.auth.signOut()
+ redirect('/auth/login')
+ }
+
+ return (
+
+
+
Protected Page
+
Welcome, {user.email}!
+
+
+
+ )
+}
+```
+
+### Step 10: Test Your Implementation
+
+1. Start your development server:
+
+```bash
+npm run dev
+```
+
+2. Navigate to `http://localhost:3000/auth/login`
+
+3. Enter your email address
+
+4. Check your email for either:
+
+ - A magic link (click it to sign in)
+ - An OTP code (enter it on the verification page)
+
+5. You should be redirected to `/protected` after successful authentication
+
+## Common Issues and Solutions
+
+### Issue: Email not arriving
+
+**Solutions:**
+
+- Check your spam folder
+- Verify email provider settings in Supabase Dashboard
+- Check Supabase logs: **Authentication** > **Logs**
+
+### Issue: "Invalid redirect URL" error
+
+**Solutions:**
+
+- Verify the redirect URL is whitelisted in Supabase Dashboard
+- Ensure the URL matches exactly (including http/https)
+- Check for trailing slashes
+
+### Issue: "User not found" error
+
+**Solutions:**
+
+- Set `shouldCreateUser: true` to allow automatic user creation
+- Or manually create the user in Supabase Dashboard first
+
+### Issue: Middleware not working
+
+**Solutions:**
+
+- Ensure middleware.ts is in the project root
+- Check the matcher configuration
+- Restart your development server
+
+## Next Steps
+
+1. **Customize the UI**: Modify the components to match your brand
+2. **Add Analytics**: Track authentication events
+3. **Configure Email Templates**: Customize the email design
+4. **Add Social Auth**: Combine with OAuth providers
+5. **Implement Role-Based Access**: Add user roles and permissions
+
+## Additional Resources
+
+- [Full Documentation](./README.md)
+- [Usage Guide](./USAGE.md)
+- [Supabase Auth Docs](https://supabase.com/docs/guides/auth)
+- [Next.js App Router](https://nextjs.org/docs/app)
+
+## Need Help?
+
+- [Supabase Discord](https://discord.supabase.com)
+- [GitHub Issues](https://github.com/supabase/supabase/issues)
+- [Supabase Documentation](https://supabase.com/docs)
+
+---
+
+**Congratulations!** You now have passwordless email authentication set up in your Next.js application. 🎉
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/INDEX.md b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/INDEX.md
new file mode 100644
index 00000000000..db24cb479d8
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/INDEX.md
@@ -0,0 +1,286 @@
+# Passwordless Authentication - Complete Documentation Index
+
+## Overview
+
+This is a complete passwordless authentication solution for Next.js applications using Supabase Auth. It supports both Magic Link and OTP (One-Time Password) authentication methods.
+
+## Documentation Files
+
+### 📚 Getting Started
+- **[GETTING_STARTED.md](./GETTING_STARTED.md)** - Step-by-step setup guide (10 minutes)
+ - Installation instructions
+ - Configuration steps
+ - Your first login page
+ - Troubleshooting
+
+### 📖 Main Documentation
+- **[README.md](./README.md)** - Complete feature documentation
+ - Features overview
+ - Components reference
+ - Setup instructions
+ - Configuration options
+ - Security considerations
+
+### 🎯 Quick Reference
+- **[QUICK_REFERENCE.md](./QUICK_REFERENCE.md)** - Quick lookup guide
+ - Component comparison table
+ - API reference
+ - Configuration checklist
+ - Common code snippets
+ - Error messages reference
+
+### 📘 Usage Guide
+- **[USAGE.md](./USAGE.md)** - Detailed usage instructions
+ - Authentication methods comparison
+ - Component API
+ - Advanced configuration
+ - Best practices
+ - Migration guides
+
+### 🔧 Integration Examples
+- **[INTEGRATION_EXAMPLES.md](./INTEGRATION_EXAMPLES.md)** - Real-world implementations
+ - E-commerce application
+ - SaaS dashboard
+ - Blog platform
+ - Multi-tenant application
+ - Progressive Web App
+
+## Components
+
+### Core Components
+
+#### 1. PasswordlessLoginForm (Recommended)
+A unified form supporting both Magic Link and OTP authentication via the `method` prop.
+
+**File**: `components/passwordless-login-form.tsx`
+
+**Use when**: You want a single form that can handle either authentication method.
+
+```tsx
+import { PasswordlessLoginForm } from '@/components/auth/passwordless-login-form'
+
+// Magic Link (default)
+
+
+// OTP
+
+```
+
+#### 2. MagicLinkForm
+Standalone Magic Link authentication form.
+
+**File**: `components/magic-link-form.tsx`
+
+**Use when**: You only want to offer Magic Link authentication.
+
+```tsx
+import { MagicLinkForm } from '@/components/auth/magic-link-form'
+```
+
+#### 3. OTPRequestForm
+Request a one-time password via email.
+
+**File**: `components/otp-request-form.tsx`
+
+**Use when**: First step of OTP flow, or standalone OTP-only auth.
+
+```tsx
+import { OTPRequestForm } from '@/components/auth/otp-request-form'
+```
+
+#### 4. OTPVerifyForm
+Verify the OTP code received via email.
+
+**File**: `components/otp-verify-form.tsx`
+
+**Use when**: Second step of OTP flow for code verification.
+
+```tsx
+import { OTPVerifyForm } from '@/components/auth/otp-verify-form'
+```
+
+## Pages
+
+### Authentication Pages
+
+- `app/auth/passwordless/page.tsx` - Combined login form
+- `app/auth/magic-link/page.tsx` - Magic Link only
+- `app/auth/otp/page.tsx` - OTP request
+- `app/auth/verify-otp/page.tsx` - OTP verification
+- `app/auth/error/page.tsx` - Error handling
+- `app/auth/confirm/route.ts` - Magic Link confirmation handler
+
+### Example Pages
+
+- `app/protected/page.tsx` - Example protected page
+
+## Key Features
+
+✅ **Magic Link Authentication**
+- One-click email authentication
+- Automatic session creation
+- Configurable redirect URLs
+
+✅ **OTP Authentication**
+- 6-digit code verification
+- Resend functionality
+- Auto-format input
+
+✅ **Unified Form**
+- Prop-based method selection
+- Single integration point
+- Consistent UI/UX
+
+✅ **Security**
+- Rate limiting
+- Token expiration
+- HTTPS enforcement
+- Secure session management
+
+✅ **User Experience**
+- Clear error messages
+- Loading states
+- Success confirmations
+- Responsive design
+
+✅ **TypeScript**
+- Full type safety
+- IntelliSense support
+- Type definitions included
+
+## Quick Start
+
+### 1. Install Dependencies
+```bash
+npm install @supabase/supabase-js @supabase/ssr
+```
+
+### 2. Set Environment Variables
+```env
+NEXT_PUBLIC_SUPABASE_URL=your-project-url
+NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
+```
+
+### 3. Configure Supabase
+- Add redirect URLs in Dashboard
+- Configure email template
+- Enable email authentication
+
+### 4. Add Component
+```tsx
+import { PasswordlessLoginForm } from '@/components/auth/passwordless-login-form'
+
+export default function LoginPage() {
+ return
+}
+```
+
+## File Structure
+
+```
+passwordless-auth-nextjs/
+├── components/
+│ ├── magic-link-form.tsx
+│ ├── otp-request-form.tsx
+│ ├── otp-verify-form.tsx
+│ └── passwordless-login-form.tsx
+├── app/
+│ ├── auth/
+│ │ ├── confirm/route.ts
+│ │ ├── error/page.tsx
+│ │ ├── magic-link/page.tsx
+│ │ ├── otp/page.tsx
+│ │ ├── verify-otp/page.tsx
+│ │ └── passwordless/page.tsx
+│ └── protected/page.tsx
+├── middleware.ts
+├── registry-item.json
+├── README.md
+├── GETTING_STARTED.md
+├── QUICK_REFERENCE.md
+├── USAGE.md
+├── INTEGRATION_EXAMPLES.md
+└── INDEX.md (this file)
+```
+
+## Component Decision Tree
+
+```
+Do you need passwordless auth?
+│
+├─ Yes → Do you want to offer both methods?
+│ │
+│ ├─ Yes → Use PasswordlessLoginForm
+│ │
+│ └─ No → Which method?
+│ │
+│ ├─ Magic Link → Use MagicLinkForm
+│ │
+│ └─ OTP → Use OTPRequestForm + OTPVerifyForm
+│
+└─ No → Consider password-based auth instead
+```
+
+## Common Use Cases
+
+| Use Case | Recommended Component | Documentation |
+|----------|----------------------|---------------|
+| Simple login | `PasswordlessLoginForm` | GETTING_STARTED.md |
+| E-commerce | `MagicLinkForm` | INTEGRATION_EXAMPLES.md |
+| Mobile app | `OTPRequestForm` + `OTPVerifyForm` | USAGE.md |
+| SaaS platform | `PasswordlessLoginForm` | INTEGRATION_EXAMPLES.md |
+| Blog comments | `MagicLinkForm` | INTEGRATION_EXAMPLES.md |
+| Admin panel | `OTPRequestForm` + `OTPVerifyForm` | USAGE.md |
+
+## Configuration Matrix
+
+| Feature | MagicLinkForm | OTPRequestForm + OTPVerifyForm | PasswordlessLoginForm |
+|---------|---------------|--------------------------------|----------------------|
+| One-click login | ✅ | ❌ | ✅ (with `method="magic-link"`) |
+| Code entry | ❌ | ✅ | ✅ (with `method="otp"`) |
+| Method selection | Fixed | Fixed | Via prop |
+| Email template changes | Optional | Required | Depends on method |
+| Pages needed | 1 | 2 | 1 |
+| Complexity | Low | Medium | Low |
+
+## Support
+
+### Documentation
+- Start with **GETTING_STARTED.md** for setup
+- Use **QUICK_REFERENCE.md** for quick lookups
+- Read **USAGE.md** for detailed usage
+- Check **INTEGRATION_EXAMPLES.md** for real-world patterns
+
+### External Resources
+- [Supabase Auth Docs](https://supabase.com/docs/guides/auth)
+- [Next.js Documentation](https://nextjs.org/docs)
+- [Supabase Discord](https://discord.supabase.com)
+
+### Troubleshooting
+See the Troubleshooting sections in:
+- GETTING_STARTED.md (setup issues)
+- USAGE.md (usage issues)
+- QUICK_REFERENCE.md (error messages)
+
+## Version Compatibility
+
+| Package | Version |
+|---------|---------|
+| Next.js | 13.x, 14.x, 15.x |
+| React | 18.x |
+| @supabase/supabase-js | 2.x |
+| @supabase/ssr | 0.x |
+
+## License
+
+This component block is part of the Supabase UI Library and follows the same license as the parent project.
+
+---
+
+**Need help?** Start with [GETTING_STARTED.md](./GETTING_STARTED.md) for a 10-minute setup guide.
+
+**Have questions?** Check [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) for quick answers.
+
+**Ready to implement?** Read [USAGE.md](./USAGE.md) for detailed instructions.
+
+**Looking for examples?** See [INTEGRATION_EXAMPLES.md](./INTEGRATION_EXAMPLES.md) for real-world patterns.
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/INTEGRATION_EXAMPLES.md b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/INTEGRATION_EXAMPLES.md
new file mode 100644
index 00000000000..45ab6390739
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/INTEGRATION_EXAMPLES.md
@@ -0,0 +1,639 @@
+# Integration Examples
+
+Real-world examples of integrating passwordless email authentication into various Next.js application patterns.
+
+## Table of Contents
+
+1. [E-commerce Application](#e-commerce-application)
+2. [SaaS Dashboard](#saas-dashboard)
+3. [Blog Platform](#blog-platform)
+4. [Multi-tenant Application](#multi-tenant-application)
+5. [Progressive Web App](#progressive-web-app)
+
+## E-commerce Application
+
+### Scenario
+
+Users need to sign in to view order history and checkout.
+
+### Implementation
+
+**app/checkout/page.tsx**
+
+```tsx
+import { createClient } from '@/lib/supabase/server'
+import { redirect } from 'next/navigation'
+import { CheckoutForm } from '@/components/checkout-form'
+
+export default async function CheckoutPage() {
+ const supabase = await createClient()
+ const {
+ data: { user },
+ } = await supabase.auth.getUser()
+
+ // Redirect to login if not authenticated
+ if (!user) {
+ redirect('/auth/login?redirect=/checkout')
+ }
+
+ return
+}
+```
+
+**app/auth/login/page.tsx**
+
+```tsx
+'use client'
+
+import { PasswordlessLoginForm } from '@/components/auth/passwordless-login-form'
+import { useSearchParams, useRouter } from 'next/navigation'
+import { useEffect } from 'react'
+import { createClient } from '@/lib/supabase/client'
+
+export default function LoginPage() {
+ const searchParams = useSearchParams()
+ const router = useRouter()
+ const redirect = searchParams.get('redirect') || '/account'
+
+ useEffect(() => {
+ const supabase = createClient()
+
+ // Listen for auth changes
+ const {
+ data: { subscription },
+ } = supabase.auth.onAuthStateChange((event, session) => {
+ if (event === 'SIGNED_IN' && session) {
+ router.push(redirect)
+ }
+ })
+
+ return () => subscription.unsubscribe()
+ }, [redirect, router])
+
+ return (
+
+
+
Sign in to continue
+
Sign in to view your cart and complete checkout
+
+
+
+ )
+}
+```
+
+## SaaS Dashboard
+
+### Scenario
+
+Multi-step onboarding after first login with role-based access.
+
+### Implementation
+
+**app/auth/login/page.tsx**
+
+```tsx
+import { PasswordlessLoginForm } from '@/components/auth/passwordless-login-form'
+
+export default function LoginPage() {
+ return (
+
+ {/* Left side - Login form */}
+
+
+
+
Welcome back
+
Sign in to your account to continue
+
+
+
+
+
+ {/* Right side - Marketing content */}
+
+
+
Secure, passwordless authentication
+
No passwords to remember. Just enter your email.
+
+
+
+ )
+}
+```
+
+**lib/auth/check-onboarding.ts**
+
+```typescript
+import { createClient } from '@/lib/supabase/server'
+
+export async function checkOnboardingStatus() {
+ const supabase = await createClient()
+ const {
+ data: { user },
+ } = await supabase.auth.getUser()
+
+ if (!user) return null
+
+ // Check if user has completed onboarding
+ const { data: profile } = await supabase
+ .from('profiles')
+ .select('onboarding_completed')
+ .eq('id', user.id)
+ .single()
+
+ return {
+ user,
+ needsOnboarding: !profile?.onboarding_completed,
+ }
+}
+```
+
+**app/dashboard/page.tsx**
+
+```tsx
+import { checkOnboardingStatus } from '@/lib/auth/check-onboarding'
+import { redirect } from 'next/navigation'
+import { DashboardContent } from '@/components/dashboard-content'
+
+export default async function DashboardPage() {
+ const status = await checkOnboardingStatus()
+
+ if (!status) {
+ redirect('/auth/login?redirect=/dashboard')
+ }
+
+ if (status.needsOnboarding) {
+ redirect('/onboarding')
+ }
+
+ return
+}
+```
+
+## Blog Platform
+
+### Scenario
+
+Optional authentication for commenting, with guest browsing.
+
+### Implementation
+
+**app/blog/[slug]/page.tsx**
+
+```tsx
+import { createClient } from '@/lib/supabase/server'
+import { BlogPost } from '@/components/blog-post'
+import { CommentSection } from '@/components/comment-section'
+import { AuthPrompt } from '@/components/auth-prompt'
+
+export default async function BlogPostPage({ params }: { params: { slug: string } }) {
+ const supabase = await createClient()
+
+ // Get blog post (public)
+ const { data: post } = await supabase.from('posts').select('*').eq('slug', params.slug).single()
+
+ // Check auth status
+ const {
+ data: { user },
+ } = await supabase.auth.getUser()
+
+ return (
+
+
+
+ {user ? (
+
+ ) : (
+
+ )}
+
+ )
+}
+```
+
+**components/auth-prompt.tsx**
+
+```tsx
+import { Button } from '@/components/ui/button'
+import { Link } from '@/components/ui/link'
+
+export function AuthPrompt({ message }: { message: string }) {
+ return (
+
+
{message}
+
+
+ Sign in
+
+
+ Create account
+
+
+
+ )
+}
+```
+
+## Multi-tenant Application
+
+### Scenario
+
+Different authentication flows for different organizations/tenants.
+
+### Implementation
+
+**app/[tenant]/auth/login/page.tsx**
+
+```tsx
+import { PasswordlessLoginForm } from '@/components/auth/passwordless-login-form'
+import { notFound } from 'next/navigation'
+import { getTenantConfig } from '@/lib/tenants'
+
+export default async function TenantLoginPage({ params }: { params: { tenant: string } }) {
+ const tenantConfig = await getTenantConfig(params.tenant)
+
+ if (!tenantConfig) {
+ notFound()
+ }
+
+ return (
+
+
+ {/* Tenant branding */}
+
+
+
{tenantConfig.name}
+
Sign in to your account
+
+
+
+
+ {/* Tenant-specific footer */}
+
+
+
+ )
+}
+```
+
+**lib/tenants.ts**
+
+```typescript
+import { createClient } from '@/lib/supabase/server'
+
+export async function getTenantConfig(slug: string) {
+ const supabase = await createClient()
+
+ const { data: tenant } = await supabase.from('tenants').select('*').eq('slug', slug).single()
+
+ return tenant
+}
+
+export async function getUserTenant(userId: string) {
+ const supabase = await createClient()
+
+ const { data } = await supabase
+ .from('user_tenants')
+ .select('tenant:tenants(*)')
+ .eq('user_id', userId)
+ .single()
+
+ return data?.tenant
+}
+```
+
+**middleware.ts**
+
+```typescript
+import { createServerClient } from '@supabase/ssr'
+import { NextResponse, type NextRequest } from 'next/server'
+
+export async function middleware(request: NextRequest) {
+ let supabaseResponse = NextResponse.next({ request })
+
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ getAll() {
+ return request.cookies.getAll()
+ },
+ setAll(cookiesToSet) {
+ cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
+ supabaseResponse = NextResponse.next({ request })
+ cookiesToSet.forEach(({ name, value, options }) =>
+ supabaseResponse.cookies.set(name, value, options)
+ )
+ },
+ },
+ }
+ )
+
+ const {
+ data: { user },
+ } = await supabase.auth.getUser()
+
+ // Extract tenant from URL
+ const pathParts = request.nextUrl.pathname.split('/')
+ const tenant = pathParts[1]
+
+ // Protected tenant routes
+ if (pathParts[2] === 'dashboard' && !user) {
+ return NextResponse.redirect(new URL(`/${tenant}/auth/login`, request.url))
+ }
+
+ // Verify user belongs to tenant
+ if (user && pathParts[2] === 'dashboard') {
+ const { data: membership } = await supabase
+ .from('user_tenants')
+ .select('tenant_id, tenants(slug)')
+ .eq('user_id', user.id)
+ .single()
+
+ if (membership?.tenants?.slug !== tenant) {
+ return NextResponse.redirect(new URL(`/${tenant}/auth/login?error=unauthorized`, request.url))
+ }
+ }
+
+ return supabaseResponse
+}
+
+export const config = {
+ matcher: ['/:tenant/dashboard/:path*', '/:tenant/auth/:path*'],
+}
+```
+
+## Progressive Web App
+
+### Scenario
+
+Offline-first PWA with authentication state persistence.
+
+### Implementation
+
+**lib/auth/auth-provider.tsx**
+
+```tsx
+'use client'
+
+import { createContext, useContext, useEffect, useState } from 'react'
+import { createClient } from '@/lib/supabase/client'
+import type { User } from '@supabase/supabase-js'
+
+interface AuthContextType {
+ user: User | null
+ loading: boolean
+ signOut: () => Promise
+}
+
+const AuthContext = createContext({
+ user: null,
+ loading: true,
+ signOut: async () => {},
+})
+
+export function AuthProvider({ children }: { children: React.ReactNode }) {
+ const [user, setUser] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const supabase = createClient()
+
+ useEffect(() => {
+ // Check active session
+ supabase.auth.getSession().then(({ data: { session } }) => {
+ setUser(session?.user ?? null)
+ setLoading(false)
+ })
+
+ // Listen for auth changes
+ const {
+ data: { subscription },
+ } = supabase.auth.onAuthStateChange((_event, session) => {
+ setUser(session?.user ?? null)
+ setLoading(false)
+ })
+
+ return () => subscription.unsubscribe()
+ }, [supabase.auth])
+
+ const signOut = async () => {
+ await supabase.auth.signOut()
+ setUser(null)
+ }
+
+ return {children}
+}
+
+export const useAuth = () => {
+ const context = useContext(AuthContext)
+ if (!context) {
+ throw new Error('useAuth must be used within AuthProvider')
+ }
+ return context
+}
+```
+
+**app/layout.tsx**
+
+```tsx
+import { AuthProvider } from '@/lib/auth/auth-provider'
+import { OfflineIndicator } from '@/components/offline-indicator'
+
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+ )
+}
+```
+
+**components/protected-route.tsx**
+
+```tsx
+'use client'
+
+import { useAuth } from '@/lib/auth/auth-provider'
+import { useRouter } from 'next/navigation'
+import { useEffect } from 'react'
+
+export function ProtectedRoute({ children }: { children: React.ReactNode }) {
+ const { user, loading } = useAuth()
+ const router = useRouter()
+
+ useEffect(() => {
+ if (!loading && !user) {
+ router.push('/auth/login')
+ }
+ }, [user, loading, router])
+
+ if (loading) {
+ return Loading...
+ }
+
+ if (!user) {
+ return null
+ }
+
+ return <>{children}>
+}
+```
+
+**app/offline/page.tsx**
+
+```tsx
+'use client'
+
+import { useAuth } from '@/lib/auth/auth-provider'
+import { Card } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+
+export default function OfflinePage() {
+ const { user } = useAuth()
+
+ return (
+
+
+ You're offline
+
+ No internet connection detected. Some features may be unavailable.
+
+
+ {user && (
+
+
✓ You're signed in as {user.email}
+
+ )}
+
+ window.location.reload()}>
+ Try again
+
+
+
+ )
+}
+```
+
+## Best Practices for Each Pattern
+
+### E-commerce
+
+- Store return URL for post-login redirect
+- Show items in cart even when not logged in
+- Prompt for login at checkout
+- Remember cart items after login
+
+### SaaS
+
+- Implement role-based access control
+- Check subscription status
+- Handle team/organization memberships
+- Redirect to onboarding for new users
+
+### Blog/Content
+
+- Allow anonymous browsing
+- Gate premium content behind auth
+- Show auth prompts contextually
+- Remember reading position
+
+### Multi-tenant
+
+- Validate user belongs to tenant
+- Custom branding per tenant
+- Tenant-specific routing
+- Isolated data per tenant
+
+### PWA
+
+- Cache authentication state
+- Handle offline gracefully
+- Show connection status
+- Sync when online
+
+## Common Patterns
+
+### Redirect After Login
+
+```tsx
+// Store intended destination
+const intendedUrl = request.nextUrl.pathname
+redirect(`/auth/login?redirect=${encodeURIComponent(intendedUrl)}`)
+
+// Restore after login
+const redirect = searchParams.get('redirect') || '/dashboard'
+router.push(redirect)
+```
+
+### Role-Based Access
+
+```typescript
+async function checkUserRole(userId: string, requiredRole: string) {
+ const supabase = await createClient()
+
+ const { data: user_role } = await supabase
+ .from('user_roles')
+ .select('role')
+ .eq('user_id', userId)
+ .single()
+
+ return user_role?.role === requiredRole
+}
+```
+
+### Session Refresh
+
+```typescript
+// Automatically refresh session
+useEffect(() => {
+ const {
+ data: { subscription },
+ } = supabase.auth.onAuthStateChange(async (event, session) => {
+ if (event === 'TOKEN_REFRESHED') {
+ console.log('Session refreshed')
+ }
+ })
+
+ return () => subscription.unsubscribe()
+}, [])
+```
+
+## Testing Integration
+
+```typescript
+// __tests__/auth-integration.test.ts
+import { render, screen, waitFor } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { PasswordlessLoginForm } from '@/components/auth/passwordless-login-form'
+
+describe('Passwordless Auth Integration', () => {
+ it('should send magic link when email submitted', async () => {
+ render( )
+
+ const emailInput = screen.getByLabelText(/email/i)
+ const submitButton = screen.getByRole('button', { name: /send magic link/i })
+
+ await userEvent.type(emailInput, 'test@example.com')
+ await userEvent.click(submitButton)
+
+ await waitFor(() => {
+ expect(screen.getByText(/check your email/i)).toBeInTheDocument()
+ })
+ })
+})
+```
+
+---
+
+These examples demonstrate how to integrate passwordless authentication into various application architectures. Adapt them to your specific needs!
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/QUICK_REFERENCE.md b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/QUICK_REFERENCE.md
new file mode 100644
index 00000000000..8d9761fbe31
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/QUICK_REFERENCE.md
@@ -0,0 +1,312 @@
+# Quick Reference - Passwordless Authentication
+
+## Components Overview
+
+| Component | Purpose | Use Case |
+| ----------------------- | --------------------------------------- | -------------------------------- |
+| `MagicLinkForm` | Email-only form that sends a magic link | Simple, one-click authentication |
+| `OTPRequestForm` | Request an OTP code via email | Multi-step verification |
+| `OTPVerifyForm` | Verify the OTP code | Second step after OTPRequestForm |
+| `PasswordlessLoginForm` | Unified form supporting both methods | Prop-controlled method selection |
+
+## Quick Implementation
+
+### Magic Link (Simplest)
+
+```tsx
+import { MagicLinkForm } from '@/components/auth/magic-link-form'
+
+export default function LoginPage() {
+ return
+}
+```
+
+### OTP Flow (Two Pages)
+
+**Page 1: Request OTP**
+
+```tsx
+import { OTPRequestForm } from '@/components/auth/otp-request-form'
+
+export default function RequestPage() {
+ return
+}
+```
+
+**Page 2: Verify OTP**
+
+```tsx
+import { OTPVerifyForm } from '@/components/auth/otp-verify-form'
+import { Suspense } from 'react'
+
+export default function VerifyPage() {
+ return (
+
+
+
+ )
+}
+```
+
+### PasswordlessLoginForm (Recommended)
+
+```tsx
+import { PasswordlessLoginForm } from '@/components/auth/passwordless-login-form'
+
+export default function LoginPage() {
+ // Magic Link (default)
+ return
+
+ // Or OTP
+ // return
+}
+```
+
+## API Reference
+
+### signInWithOtp (Magic Link)
+
+```typescript
+const { data, error } = await supabase.auth.signInWithOtp({
+ email: 'user@example.com',
+ options: {
+ shouldCreateUser: true,
+ emailRedirectTo: 'https://yourapp.com/auth/confirm',
+ },
+})
+```
+
+### signInWithOtp (OTP)
+
+```typescript
+const { data, error } = await supabase.auth.signInWithOtp({
+ email: 'user@example.com',
+ options: {
+ shouldCreateUser: true,
+ },
+})
+```
+
+### verifyOtp
+
+```typescript
+const { data, error } = await supabase.auth.verifyOtp({
+ email: 'user@example.com',
+ token: '123456',
+ type: 'email',
+})
+```
+
+### verifyOtp (Magic Link - Server)
+
+```typescript
+const { error } = await supabase.auth.verifyOtp({
+ type: 'email',
+ token_hash: 'hash_from_url',
+})
+```
+
+## Configuration Checklist
+
+- [ ] Environment variables set
+- [ ] Redirect URLs configured in Supabase Dashboard
+- [ ] Email template configured (Magic Link OR OTP)
+- [ ] Supabase client created (client.ts and server.ts)
+- [ ] Middleware added (optional)
+- [ ] UI components installed (button, card, input, label)
+- [ ] Auth components copied to project
+
+## File Structure
+
+```
+your-app/
+├── lib/
+│ └── supabase/
+│ ├── client.ts
+│ └── server.ts
+├── components/
+│ └── auth/
+│ ├── magic-link-form.tsx
+│ ├── otp-request-form.tsx
+│ ├── otp-verify-form.tsx
+│ └── passwordless-login-form.tsx
+├── app/
+│ ├── auth/
+│ │ ├── confirm/
+│ │ │ └── route.ts
+│ │ ├── error/
+│ │ │ └── page.tsx
+│ │ ├── magic-link/
+│ │ │ └── page.tsx
+│ │ ├── otp/
+│ │ │ └── page.tsx
+│ │ ├── verify-otp/
+│ │ │ └── page.tsx
+│ │ └── passwordless/
+│ │ └── page.tsx
+│ └── protected/
+│ └── page.tsx
+├── middleware.ts
+└── .env.local
+```
+
+## Common Code Snippets
+
+### Check if User is Authenticated
+
+```typescript
+const supabase = await createClient()
+const {
+ data: { user },
+} = await supabase.auth.getUser()
+
+if (!user) {
+ redirect('/auth/login')
+}
+```
+
+### Sign Out
+
+```typescript
+const supabase = await createClient()
+await supabase.auth.signOut()
+redirect('/auth/login')
+```
+
+### Get User Session
+
+```typescript
+const supabase = await createClient()
+const {
+ data: { session },
+} = await supabase.auth.getSession()
+```
+
+### Listen to Auth Changes
+
+```typescript
+const supabase = createClient()
+
+supabase.auth.onAuthStateChange((event, session) => {
+ if (event === 'SIGNED_IN') {
+ console.log('User signed in:', session?.user)
+ }
+ if (event === 'SIGNED_OUT') {
+ console.log('User signed out')
+ }
+})
+```
+
+## Email Templates
+
+### Magic Link Template
+
+```html
+Magic Link
+Click here to sign in:
+Sign In
+```
+
+### OTP Template
+
+```html
+Your Login Code
+Enter this code to sign in:
+{{ .Token }}
+Expires in 1 hour
+```
+
+## Environment Variables
+
+```env
+NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
+NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
+```
+
+## Supabase Dashboard URLs
+
+- **Project Settings**: Settings > API
+- **Auth Configuration**: Authentication > Providers
+- **URL Configuration**: Authentication > URL Configuration
+- **Email Templates**: Authentication > Email Templates
+- **Rate Limits**: Authentication > Rate Limits
+- **Auth Logs**: Authentication > Logs
+
+## Default Settings
+
+| Setting | Default Value | Configurable |
+| --------------------- | ------------------- | ----------------- |
+| OTP Expiration | 1 hour | Yes (max 24h) |
+| Rate Limit | 1 req/60s per email | Yes |
+| Magic Link Expiration | 1 hour | Yes |
+| Auto Sign-Up | Enabled | Yes (per request) |
+
+## Error Messages
+
+| Error | Cause | Solution |
+| --------------------------- | ----------------------- | -------------------------------- |
+| "Email rate limit exceeded" | Too many requests | Wait 60 seconds |
+| "Invalid redirect URL" | URL not whitelisted | Add to Supabase Dashboard |
+| "Invalid token" | Expired or used token | Request new token |
+| "User not found" | shouldCreateUser: false | Set to true or create user first |
+
+## Testing Checklist
+
+- [ ] Magic link arrives in email
+- [ ] Magic link redirects correctly
+- [ ] OTP code arrives in email
+- [ ] OTP verification works
+- [ ] Expired tokens are rejected
+- [ ] Invalid tokens show error
+- [ ] Rate limiting works
+- [ ] Protected routes redirect
+- [ ] Sign out works
+- [ ] Middleware protects routes
+
+## Performance Tips
+
+1. Use Suspense for OTP verify page
+2. Implement loading states
+3. Add error boundaries
+4. Use optimistic UI updates
+5. Cache auth state when appropriate
+
+## Security Best Practices
+
+1. Always use HTTPS in production
+2. Implement rate limiting
+3. Monitor auth logs
+4. Set appropriate token expiration
+5. Validate email addresses
+6. Use secure cookies
+7. Enable CSRF protection
+
+## Browser Support
+
+- ✅ Chrome/Edge (Chromium)
+- ✅ Firefox
+- ✅ Safari
+- ✅ Mobile browsers
+- ⚠️ IE11 (not supported)
+
+## Dependencies
+
+```json
+{
+ "@supabase/supabase-js": "^2.x.x",
+ "@supabase/ssr": "^0.x.x",
+ "next": "^13.x.x || ^14.x.x || ^15.x.x",
+ "react": "^18.x.x"
+}
+```
+
+## Useful Links
+
+- [Full Documentation](./README.md)
+- [Getting Started Guide](./GETTING_STARTED.md)
+- [Usage Guide](./USAGE.md)
+- [Supabase Auth Docs](https://supabase.com/docs/guides/auth)
+
+---
+
+**Pro Tip**: Start with `PasswordlessLoginForm` - it gives users both options and you can always switch to individual forms later!
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/README.md b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/README.md
new file mode 100644
index 00000000000..33d7af5f0ad
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/README.md
@@ -0,0 +1,293 @@
+# Passwordless Authentication for Next.js
+
+A complete passwordless authentication solution for Next.js applications using Supabase Auth. This block includes support for both Magic Link and OTP (One-Time Password) login methods.
+
+## Features
+
+- **Magic Link Login**: Users receive a clickable link in their email to sign in
+- **OTP Login**: Users receive a 6-digit code to enter for authentication
+- **Combined Form**: Single form that supports both methods
+- **Auto Sign-Up**: Automatically creates user accounts on first login (configurable)
+- **Protected Routes**: Middleware-protected pages requiring authentication
+- **Error Handling**: Comprehensive error states and user feedback
+- **TypeScript**: Full type safety with TypeScript
+
+## Components
+
+### 1. MagicLinkForm
+
+A standalone form for Magic Link authentication.
+
+```tsx
+import { MagicLinkForm } from '@/registry/default/blocks/passwordless-auth-nextjs/components/magic-link-form'
+
+export default function LoginPage() {
+ return
+}
+```
+
+### 2. OTPRequestForm
+
+Request a one-time password to be sent via email.
+
+```tsx
+import { OTPRequestForm } from '@/registry/default/blocks/passwordless-auth-nextjs/components/otp-request-form'
+
+export default function OTPPage() {
+ return
+}
+```
+
+### 3. OTPVerifyForm
+
+Verify the OTP code received via email.
+
+```tsx
+import { OTPVerifyForm } from '@/registry/default/blocks/passwordless-auth-nextjs/components/otp-verify-form'
+
+export default function VerifyPage() {
+ return
+}
+```
+
+### 4. PasswordlessLoginForm (Recommended)
+
+A unified form that supports both Magic Link and OTP authentication. Use the `method` prop to specify which authentication method to use.
+
+```tsx
+import { PasswordlessLoginForm } from '@/registry/default/blocks/passwordless-auth-nextjs/components/passwordless-login-form'
+
+export default function LoginPage() {
+ return
+}
+```
+
+**Props:**
+- `method?: 'magic-link' | 'otp'` - Authentication method (default: `'magic-link'`)
+
+**Examples:**
+
+```tsx
+// Magic Link authentication
+
+
+// OTP authentication
+
+```
+
+## Setup
+
+### 1. Environment Variables
+
+Ensure you have the following environment variables in your `.env.local`:
+
+```env
+NEXT_PUBLIC_SUPABASE_URL=your-project-url
+NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
+```
+
+### 2. Supabase Configuration
+
+#### Configure Redirect URLs
+
+In your Supabase Dashboard:
+
+1. Go to **Authentication** > **URL Configuration**
+2. Add your site URL (e.g., `http://localhost:3000` for development)
+3. Add redirect URLs:
+ - `http://localhost:3000/auth/confirm`
+ - `http://localhost:3000/protected`
+
+#### Email Templates
+
+For **Magic Link** (default):
+The default email template works out of the box. For PKCE flow, update the Magic Link email template:
+
+```html
+Magic Link
+Follow this link to login:
+Log In
+```
+
+For **OTP**:
+Update the Magic Link email template to show the OTP code:
+
+```html
+One time login code
+Please enter this code: {{ .Token }}
+```
+
+### 3. Middleware
+
+Copy the included `middleware.ts` to your project root to protect routes:
+
+```typescript
+// middleware.ts
+export async function middleware(request: NextRequest) {
+ // ... (see middleware.ts in this block)
+}
+
+export const config = {
+ matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
+}
+```
+
+## Usage Examples
+
+### Basic Magic Link Login
+
+```tsx
+// app/auth/login/page.tsx
+import { MagicLinkForm } from '@/registry/default/blocks/passwordless-auth-nextjs/components/magic-link-form'
+
+export default function LoginPage() {
+ return (
+
+
+
+ )
+}
+```
+
+### OTP Flow
+
+```tsx
+// app/auth/otp/page.tsx
+import { OTPRequestForm } from '@/registry/default/blocks/passwordless-auth-nextjs/components/otp-request-form'
+
+export default function OTPPage() {
+ return (
+
+
+
+ )
+}
+
+// app/auth/verify-otp/page.tsx
+import { OTPVerifyForm } from '@/registry/default/blocks/passwordless-auth-nextjs/components/otp-verify-form'
+import { Suspense } from 'react'
+
+export default function VerifyPage() {
+ return (
+
+
+
+
+
+ )
+}
+```
+
+### Combined Passwordless Form (Recommended)
+
+```tsx
+// app/auth/passwordless/page.tsx
+import { PasswordlessLoginForm } from '@/registry/default/blocks/passwordless-auth-nextjs/components/passwordless-login-form'
+
+export default function PasswordlessPage() {
+ return (
+
+ )
+}
+```
+
+## Configuration Options
+
+### Disable Auto Sign-Up
+
+If you want to prevent automatic user creation, set `shouldCreateUser` to `false`:
+
+```typescript
+const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ shouldCreateUser: false, // Only allow existing users to sign in
+ },
+})
+```
+
+### Custom Redirect
+
+Customize where users are redirected after authentication:
+
+```typescript
+const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ emailRedirectTo: `${window.location.origin}/dashboard`, // Custom redirect
+ },
+})
+```
+
+### OTP Expiration
+
+By default:
+
+- OTPs can be requested once every 60 seconds
+- OTPs expire after 1 hour
+
+Configure these in your Supabase Dashboard:
+**Authentication** > **Providers** > **Email** > **Email OTP Expiration**
+
+## Protected Routes
+
+Create protected pages that require authentication:
+
+```tsx
+// app/protected/page.tsx
+import { createClient } from '@/lib/supabase/server'
+import { redirect } from 'next/navigation'
+
+export default async function ProtectedPage() {
+ const supabase = await createClient()
+ const {
+ data: { user },
+ } = await supabase.auth.getUser()
+
+ if (!user) {
+ redirect('/auth/passwordless')
+ }
+
+ return (
+
+
Protected Content
+
Welcome, {user.email}!
+
+ )
+}
+```
+
+## Security Considerations
+
+1. **Rate Limiting**: Supabase enforces rate limits (1 request per 60 seconds by default)
+2. **Expiration**: Magic Links and OTPs expire after 1 hour
+3. **HTTPS Required**: Always use HTTPS in production
+4. **Environment Variables**: Never commit `.env` files to version control
+
+## Troubleshooting
+
+### Magic Link not working
+
+1. Check your email template includes the correct redirect URL
+2. Verify redirect URLs are configured in Supabase Dashboard
+3. Check email spam folder
+
+### OTP not arriving
+
+1. Verify email template is configured to show `{{ .Token }}`
+2. Check Supabase Auth email provider settings
+3. Check rate limiting (wait 60 seconds between requests)
+
+### Authentication errors
+
+1. Check browser console for detailed error messages
+2. Verify environment variables are set correctly
+3. Ensure Supabase client is initialized properly
+
+## Learn More
+
+- [Supabase Passwordless Auth Docs](https://supabase.com/docs/guides/auth/auth-email-passwordless)
+- [Magic Link Documentation](https://supabase.com/docs/guides/auth/auth-email-passwordless#with-magic-link)
+- [OTP Documentation](https://supabase.com/docs/guides/auth/auth-email-passwordless#with-otp)
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/USAGE.md b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/USAGE.md
new file mode 100644
index 00000000000..10addd79a7d
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/USAGE.md
@@ -0,0 +1,468 @@
+# Passwordless Authentication - Usage Guide
+
+This guide provides detailed instructions for implementing passwordless authentication in your Next.js application using Supabase Auth.
+
+## Table of Contents
+
+1. [Quick Start](#quick-start)
+2. [Authentication Methods](#authentication-methods)
+3. [Component API](#component-api)
+4. [Advanced Configuration](#advanced-configuration)
+5. [Best Practices](#best-practices)
+
+## Quick Start
+
+### Installation
+
+1. Copy the block to your project:
+
+```bash
+npx shadcn@latest add passwordless-auth-nextjs
+```
+
+2. Set up environment variables:
+
+```env
+NEXT_PUBLIC_SUPABASE_URL=your-project-url
+NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
+```
+
+3. Configure Supabase redirect URLs in your dashboard:
+ - `http://localhost:3000/auth/confirm` (development)
+ - `https://yourdomain.com/auth/confirm` (production)
+
+### Basic Implementation
+
+```tsx
+import { PasswordlessLoginForm } from '@/components/passwordless-login-form'
+
+export default function LoginPage() {
+ return
+}
+```
+
+## Authentication Methods
+
+### 1. Magic Link
+
+Magic Links are one-time use URLs sent to the user's email. When clicked, they automatically authenticate the user.
+
+**Advantages:**
+
+- Seamless user experience (one click to login)
+- No need to remember or type codes
+- Works across devices
+
+**Disadvantages:**
+
+- Requires user to access their email
+- May be caught by spam filters
+- Links expire after 1 hour
+
+**Implementation:**
+
+```tsx
+import { MagicLinkForm } from '@/components/magic-link-form'
+
+export default function MagicLinkLoginPage() {
+ return (
+
+
+
+ )
+}
+```
+
+**Email Template Configuration:**
+
+For implicit flow (default):
+
+```html
+Magic Link
+Follow this link to login:
+Log In
+```
+
+For PKCE flow:
+
+```html
+Magic Link
+Follow this link to login:
+Log In
+```
+
+### 2. OTP (One-Time Password)
+
+OTPs are 6-digit codes sent to the user's email that they manually enter.
+
+**Advantages:**
+
+- More control (user types the code)
+- Works better for mobile apps
+- Familiar to users
+
+**Disadvantages:**
+
+- Extra step (user must type code)
+- Possible typos
+- Codes expire after 1 hour
+
+**Implementation:**
+
+```tsx
+// Step 1: Request OTP
+import { OTPRequestForm } from '@/components/otp-request-form'
+
+export default function RequestOTPPage() {
+ return (
+
+
+
+ )
+}
+
+// Step 2: Verify OTP
+import { OTPVerifyForm } from '@/components/otp-verify-form'
+import { Suspense } from 'react'
+
+export default function VerifyOTPPage() {
+ return (
+ Loading...}>
+
+
+
+
+ )
+}
+```
+
+**Email Template Configuration:**
+
+```html
+One time login code
+Please enter this code: {{ .Token }}
+This code will expire in 1 hour.
+```
+
+### 3. PasswordlessLoginForm (Recommended)
+
+A unified authentication form that supports both Magic Link and OTP. Use the `method` prop to specify which authentication method to use.
+
+```tsx
+import { PasswordlessLoginForm } from '@/components/passwordless-login-form'
+
+export default function LoginPage() {
+ return (
+
+ )
+}
+```
+
+**With OTP:**
+
+```tsx
+
+```
+
+## Component API
+
+### MagicLinkForm
+
+**Props:**
+
+```typescript
+interface MagicLinkFormProps extends React.ComponentPropsWithoutRef<'div'> {
+ className?: string
+}
+```
+
+**Events:**
+
+- On success: Displays confirmation message
+- On error: Shows error message inline
+
+**Example with custom styling:**
+
+```tsx
+
+```
+
+### OTPRequestForm
+
+**Props:**
+
+```typescript
+interface OTPRequestFormProps extends React.ComponentPropsWithoutRef<'div'> {
+ className?: string
+}
+```
+
+**Behavior:**
+
+- On success: Redirects to `/auth/verify-otp?email={email}`
+- On error: Shows error message inline
+
+### OTPVerifyForm
+
+**Props:**
+
+```typescript
+interface OTPVerifyFormProps extends React.ComponentPropsWithoutRef<'div'> {
+ className?: string
+}
+```
+
+**Features:**
+
+- Auto-formats OTP input (numbers only, max 6 digits)
+- Resend functionality
+- Email display from query parameter
+
+**Example:**
+
+```tsx
+// The email is passed via query parameter
+// URL: /auth/verify-otp?email=user@example.com
+
+```
+
+### PasswordlessLoginForm
+
+**Props:**
+
+```typescript
+interface PasswordlessLoginFormProps extends React.ComponentPropsWithoutRef<'div'> {
+ method?: 'magic-link' | 'otp'
+ className?: string
+}
+```
+
+**Features:**
+
+- Single form supporting both Magic Link and OTP
+- Method controlled via prop (defaults to `'magic-link'`)
+- Success state management
+- Automatic redirect for OTP verification
+
+## Advanced Configuration
+
+### Custom Redirect URLs
+
+Customize where users are redirected after authentication:
+
+```typescript
+// In your component
+const handleLogin = async () => {
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ emailRedirectTo: `${window.location.origin}/dashboard`,
+ },
+ })
+}
+```
+
+### Disable Auto Sign-Up
+
+Prevent new user creation, only allow existing users:
+
+```typescript
+const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ shouldCreateUser: false,
+ },
+})
+```
+
+### Custom OTP Expiration
+
+Configure in Supabase Dashboard:
+
+1. Go to **Authentication** > **Providers** > **Email**
+2. Find **Email OTP Expiration**
+3. Set custom duration (max 24 hours)
+
+### Rate Limiting
+
+Customize rate limiting in Supabase Dashboard:
+
+1. Go to **Authentication** > **Rate Limits**
+2. Adjust the rate limits for email authentication
+
+Default: 1 request per 60 seconds per email address
+
+## Best Practices
+
+### 1. Error Handling
+
+Always handle errors gracefully:
+
+```tsx
+try {
+ const { error } = await supabase.auth.signInWithOtp({ email })
+ if (error) throw error
+ // Success handling
+} catch (error) {
+ // Show user-friendly error message
+ if (error.message.includes('rate limit')) {
+ setError('Please wait a moment before requesting another code')
+ } else {
+ setError('An error occurred. Please try again.')
+ }
+}
+```
+
+### 2. Loading States
+
+Show loading indicators during API calls:
+
+```tsx
+const [isLoading, setIsLoading] = useState(false)
+
+const handleSubmit = async () => {
+ setIsLoading(true)
+ try {
+ // API call
+ } finally {
+ setIsLoading(false)
+ }
+}
+
+return {isLoading ? 'Sending...' : 'Send Link'}
+```
+
+### 3. Security
+
+- Always use HTTPS in production
+- Never expose API keys in client-side code
+- Implement rate limiting
+- Monitor authentication logs
+- Set appropriate token expiration times
+
+### 4. User Experience
+
+**For Magic Links:**
+
+- Clear instructions: "Check your email for a magic link"
+- Mention spam folder
+- Option to resend
+
+**For OTPs:**
+
+- Auto-focus OTP input field
+- Auto-submit when 6 digits entered
+- Clear visual feedback
+- Resend option with countdown
+
+### 5. Email Templates
+
+Make your emails clear and branded:
+
+```html
+
+
+
+
+ Your Login Code
+ Enter this code to sign in to your account:
+
+ {{ .Token }}
+
+
+ This code will expire in 1 hour. If you didn't request this, please ignore this email.
+
+
+
+
+```
+
+### 6. Testing
+
+Test various scenarios:
+
+- Valid email addresses
+- Invalid email addresses
+- Expired tokens/OTPs
+- Multiple rapid requests (rate limiting)
+- Different browsers and devices
+- Email delivery times
+- Spam folder handling
+
+### 7. Analytics
+
+Track key metrics:
+
+- Magic Link vs OTP usage
+- Success/failure rates
+- Time to complete authentication
+- Drop-off points in the flow
+
+## Troubleshooting
+
+### Problem: Magic Link not working
+
+**Solutions:**
+
+1. Verify redirect URLs in Supabase Dashboard
+2. Check email template configuration
+3. Ensure HTTPS in production
+4. Check browser console for errors
+
+### Problem: OTP not received
+
+**Solutions:**
+
+1. Check email provider settings in Supabase
+2. Verify email template shows `{{ .Token }}`
+3. Check spam folder
+4. Verify rate limiting hasn't been triggered
+
+### Problem: "Invalid token" error
+
+**Solutions:**
+
+1. Check if token has expired (default: 1 hour)
+2. Ensure user hasn't already used the token
+3. Verify system clock is synchronized
+4. Check for typos in OTP entry
+
+### Problem: Redirect not working
+
+**Solutions:**
+
+1. Verify `emailRedirectTo` URL is whitelisted
+2. Check middleware configuration
+3. Ensure URL is absolute, not relative
+4. Verify Next.js routing configuration
+
+## Migration Guide
+
+### From Password-Based Auth
+
+Replace password fields with email-only:
+
+```diff
+- import { LoginForm } from '@/components/login-form'
++ import { PasswordlessLoginForm } from '@/components/passwordless-login-form'
+
+export default function LoginPage() {
+- return
++ return
+}
+```
+
+### From Other Passwordless Solutions
+
+1. Replace API calls with Supabase methods
+2. Update email templates
+3. Configure redirect URLs
+4. Test thoroughly
+
+## Additional Resources
+
+- [Supabase Auth Documentation](https://supabase.com/docs/guides/auth)
+- [Next.js App Router Guide](https://nextjs.org/docs/app)
+- [Email Template Best Practices](https://supabase.com/docs/guides/auth/auth-email-templates)
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/auth/confirm/route.ts b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/auth/confirm/route.ts
new file mode 100644
index 00000000000..b704c1b365e
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/auth/confirm/route.ts
@@ -0,0 +1,26 @@
+import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server'
+import { type EmailOtpType } from '@supabase/supabase-js'
+import { type NextRequest, NextResponse } from 'next/server'
+
+export async function GET(request: NextRequest) {
+ const { searchParams } = new URL(request.url)
+ const token_hash = searchParams.get('token_hash')
+ const type = searchParams.get('type') as EmailOtpType | null
+ const next = searchParams.get('next') ?? '/protected'
+
+ if (token_hash && type) {
+ const supabase = await createClient()
+
+ const { error } = await supabase.auth.verifyOtp({
+ type,
+ token_hash,
+ })
+ if (!error) {
+ // Redirect user to specified redirect URL or default to protected page
+ return NextResponse.redirect(new URL(next, request.url))
+ }
+ }
+
+ // Redirect the user to an error page with some instructions
+ return NextResponse.redirect(new URL('/auth/error', request.url))
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/auth/error/page.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/auth/error/page.tsx
new file mode 100644
index 00000000000..5524910542c
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/auth/error/page.tsx
@@ -0,0 +1,35 @@
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { Button } from '@/registry/default/components/ui/button'
+import { Link } from '@/registry/default/components/ui/link'
+
+export default function AuthErrorPage() {
+ return (
+
+
+
+
+ Authentication Error
+ There was an error authenticating your account
+
+
+
+
+ The authentication link may have expired or is invalid. Please try requesting a new
+ one.
+
+
+ Try again
+
+
+
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/auth/magic-link/page.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/auth/magic-link/page.tsx
new file mode 100644
index 00000000000..15429e0f7d8
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/auth/magic-link/page.tsx
@@ -0,0 +1,11 @@
+import { MagicLinkForm } from '@/registry/default/blocks/passwordless-auth-nextjs/components/magic-link-form'
+
+export default function MagicLinkPage() {
+ return (
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/auth/otp/page.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/auth/otp/page.tsx
new file mode 100644
index 00000000000..0c561e0a98e
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/auth/otp/page.tsx
@@ -0,0 +1,11 @@
+import { OTPRequestForm } from '@/registry/default/blocks/passwordless-auth-nextjs/components/otp-request-form'
+
+export default function OTPRequestPage() {
+ return (
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/auth/passwordless/page.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/auth/passwordless/page.tsx
new file mode 100644
index 00000000000..4528e0aed05
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/auth/passwordless/page.tsx
@@ -0,0 +1,11 @@
+import { PasswordlessLoginForm } from '@/registry/default/blocks/passwordless-auth-nextjs/components/passwordless-login-form'
+
+export default function PasswordlessLoginPage() {
+ return (
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/auth/verify-otp/page.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/auth/verify-otp/page.tsx
new file mode 100644
index 00000000000..90f1a17c678
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/auth/verify-otp/page.tsx
@@ -0,0 +1,20 @@
+import { OTPVerifyForm } from '@/registry/default/blocks/passwordless-auth-nextjs/components/otp-verify-form'
+import { Suspense } from 'react'
+
+function OTPVerifyContent() {
+ return (
+
+ )
+}
+
+export default function OTPVerifyPage() {
+ return (
+ Loading...}>
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/protected/page.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/protected/page.tsx
new file mode 100644
index 00000000000..fcb5e40e91b
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/app/protected/page.tsx
@@ -0,0 +1,59 @@
+import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server'
+import { redirect } from 'next/navigation'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { Button } from '@/registry/default/components/ui/button'
+
+export default async function ProtectedPage() {
+ const supabase = await createClient()
+
+ const {
+ data: { user },
+ } = await supabase.auth.getUser()
+
+ if (!user) {
+ redirect('/auth/passwordless')
+ }
+
+ const handleSignOut = async () => {
+ 'use server'
+ const supabase = await createClient()
+ await supabase.auth.signOut()
+ redirect('/auth/passwordless')
+ }
+
+ return (
+
+
+
+
+ Protected Page
+ You are logged in
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/components/magic-link-form.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/components/magic-link-form.tsx
new file mode 100644
index 00000000000..6a6d1ab73ea
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/components/magic-link-form.tsx
@@ -0,0 +1,92 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
+import { Button } from '@/registry/default/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { Input } from '@/registry/default/components/ui/input'
+import { Label } from '@/registry/default/components/ui/label'
+import { useState } from 'react'
+
+export function MagicLinkForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
+ const [email, setEmail] = useState('')
+ const [error, setError] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const [isSuccess, setIsSuccess] = useState(false)
+
+ const handleMagicLinkRequest = async (e: React.FormEvent) => {
+ e.preventDefault()
+ const supabase = createClient()
+ setIsLoading(true)
+ setError(null)
+ setIsSuccess(false)
+
+ try {
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ // Set this to false if you do not want the user to be automatically signed up
+ shouldCreateUser: true,
+ emailRedirectTo: `${window.location.origin}/auth/confirm`,
+ },
+ })
+ if (error) throw error
+ setIsSuccess(true)
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : 'An error occurred')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ Magic Link Login
+
+ Enter your email to receive a magic link for passwordless login
+
+
+
+ {isSuccess ? (
+
+
Check your email!
+
+ We've sent a magic link to {email} . Click the
+ link in the email to sign in.
+
+
+ ) : (
+
+ )}
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/components/otp-request-form.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/components/otp-request-form.tsx
new file mode 100644
index 00000000000..2ef5b4bb732
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/components/otp-request-form.tsx
@@ -0,0 +1,82 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
+import { Button } from '@/registry/default/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { Input } from '@/registry/default/components/ui/input'
+import { Label } from '@/registry/default/components/ui/label'
+import { useRouter } from 'next/navigation'
+import { useState } from 'react'
+
+export function OTPRequestForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
+ const [email, setEmail] = useState('')
+ const [error, setError] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const router = useRouter()
+
+ const handleOTPRequest = async (e: React.FormEvent) => {
+ e.preventDefault()
+ const supabase = createClient()
+ setIsLoading(true)
+ setError(null)
+
+ try {
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ // Set this to false if you do not want the user to be automatically signed up
+ shouldCreateUser: true,
+ },
+ })
+ if (error) throw error
+ // Navigate to OTP verification page
+ router.push(`/auth/verify-otp?email=${encodeURIComponent(email)}`)
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : 'An error occurred')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ OTP Login
+
+ Enter your email to receive a one-time password for passwordless login
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/components/otp-verify-form.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/components/otp-verify-form.tsx
new file mode 100644
index 00000000000..00b4065c265
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/components/otp-verify-form.tsx
@@ -0,0 +1,122 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
+import { Button } from '@/registry/default/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { Input } from '@/registry/default/components/ui/input'
+import { Label } from '@/registry/default/components/ui/label'
+import { useRouter, useSearchParams } from 'next/navigation'
+import { useState } from 'react'
+
+export function OTPVerifyForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
+ const searchParams = useSearchParams()
+ const email = searchParams.get('email') || ''
+ const [otp, setOTP] = useState('')
+ const [error, setError] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const [isResending, setIsResending] = useState(false)
+ const router = useRouter()
+
+ const handleVerifyOTP = async (e: React.FormEvent) => {
+ e.preventDefault()
+ const supabase = createClient()
+ setIsLoading(true)
+ setError(null)
+
+ try {
+ const { error } = await supabase.auth.verifyOtp({
+ email,
+ token: otp,
+ type: 'email',
+ })
+ if (error) throw error
+ // Redirect to protected page after successful verification
+ router.push('/protected')
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : 'An error occurred')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const handleResendOTP = async () => {
+ const supabase = createClient()
+ setIsResending(true)
+ setError(null)
+
+ try {
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ shouldCreateUser: false,
+ },
+ })
+ if (error) throw error
+ setError(null)
+ // Show success message
+ alert('A new verification code has been sent to your email.')
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : 'Failed to resend code')
+ } finally {
+ setIsResending(false)
+ }
+ }
+
+ return (
+
+
+
+ Verify Your Email
+
+ Enter the 6-digit code sent to{' '}
+ {email}
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/components/passwordless-login-form.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/components/passwordless-login-form.tsx
new file mode 100644
index 00000000000..23d38e6cf40
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/components/passwordless-login-form.tsx
@@ -0,0 +1,130 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
+import { Button } from '@/registry/default/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { Input } from '@/registry/default/components/ui/input'
+import { Label } from '@/registry/default/components/ui/label'
+import { useRouter } from 'next/navigation'
+import { useState } from 'react'
+
+type LoginMethod = 'magic-link' | 'otp'
+
+interface PasswordlessLoginFormProps extends React.ComponentPropsWithoutRef<'div'> {
+ method?: LoginMethod
+}
+
+export function PasswordlessLoginForm({
+ className,
+ method = 'magic-link',
+ ...props
+}: PasswordlessLoginFormProps) {
+ const [email, setEmail] = useState('')
+ const [error, setError] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const [isMagicLinkSent, setIsMagicLinkSent] = useState(false)
+ const router = useRouter()
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ const supabase = createClient()
+ setIsLoading(true)
+ setError(null)
+ setIsMagicLinkSent(false)
+
+ try {
+ if (method === 'magic-link') {
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ shouldCreateUser: true,
+ emailRedirectTo: `${window.location.origin}/auth/confirm`,
+ },
+ })
+ if (error) throw error
+ setIsMagicLinkSent(true)
+ } else {
+ // OTP method
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ shouldCreateUser: true,
+ },
+ })
+ if (error) throw error
+ // Navigate to OTP verification page
+ router.push(`/auth/verify-otp?email=${encodeURIComponent(email)}`)
+ }
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : 'An error occurred')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ Sign up
+ Sign in or create a new account
+
+
+ {isMagicLinkSent ? (
+
+
Check your email!
+
+ We've sent a magic link to {email} . Click
+ the link in the email to sign in.
+
+
{
+ setIsMagicLinkSent(false)
+ setEmail('')
+ }}
+ >
+ Send another link
+
+
+ ) : (
+
+ )}
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/middleware.ts b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/middleware.ts
new file mode 100644
index 00000000000..33077a02bb8
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/middleware.ts
@@ -0,0 +1,46 @@
+import { createServerClient } from '@supabase/ssr'
+import { NextResponse, type NextRequest } from 'next/server'
+
+export async function middleware(request: NextRequest) {
+ let supabaseResponse = NextResponse.next({
+ request,
+ })
+
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ getAll() {
+ return request.cookies.getAll()
+ },
+ setAll(cookiesToSet) {
+ cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
+ supabaseResponse = NextResponse.next({
+ request,
+ })
+ cookiesToSet.forEach(({ name, value, options }) =>
+ supabaseResponse.cookies.set(name, value, options)
+ )
+ },
+ },
+ }
+ )
+
+ const {
+ data: { user },
+ } = await supabase.auth.getUser()
+
+ // Protect /protected route
+ if (request.nextUrl.pathname.startsWith('/protected') && !user) {
+ const url = request.nextUrl.clone()
+ url.pathname = '/auth/passwordless'
+ return NextResponse.redirect(url)
+ }
+
+ return supabaseResponse
+}
+
+export const config = {
+ matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/registry-item.json b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/registry-item.json
new file mode 100644
index 00000000000..281bcf29220
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-nextjs/registry-item.json
@@ -0,0 +1,66 @@
+{
+ "name": "passwordless-auth-nextjs",
+ "type": "registry:block",
+ "title": "Passwordless Auth for Next.js and Supabase",
+ "description": "Passwordless authentication for Next.js using Supabase Auth. Supports both Magic Link and OTP (One-Time Password) login methods.",
+ "registryDependencies": ["button", "card", "input", "label", "link"],
+ "dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"],
+ "files": [
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/components/magic-link-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/components/otp-request-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/components/otp-verify-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/components/passwordless-login-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/auth/magic-link/page.tsx",
+ "type": "registry:page",
+ "target": "app/auth/magic-link/page.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/auth/otp/page.tsx",
+ "type": "registry:page",
+ "target": "app/auth/otp/page.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/auth/verify-otp/page.tsx",
+ "type": "registry:page",
+ "target": "app/auth/verify-otp/page.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/auth/passwordless/page.tsx",
+ "type": "registry:page",
+ "target": "app/auth/passwordless/page.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/auth/confirm/route.ts",
+ "type": "registry:page",
+ "target": "app/auth/confirm/route.ts"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/auth/error/page.tsx",
+ "type": "registry:page",
+ "target": "app/auth/error/page.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/app/protected/page.tsx",
+ "type": "registry:page",
+ "target": "app/protected/page.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-nextjs/middleware.ts",
+ "type": "registry:file",
+ "target": "middleware.ts"
+ }
+ ]
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/app/routes/auth.confirm.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/app/routes/auth.confirm.tsx
new file mode 100644
index 00000000000..4e159979b8b
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/app/routes/auth.confirm.tsx
@@ -0,0 +1,27 @@
+import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'
+import { type EmailOtpType } from '@supabase/supabase-js'
+import { type LoaderFunctionArgs, redirect } from 'react-router'
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ const requestUrl = new URL(request.url)
+ const token_hash = requestUrl.searchParams.get('token_hash')
+ const type = requestUrl.searchParams.get('type') as EmailOtpType | null
+ const _next = requestUrl.searchParams.get('next')
+ const next = _next?.startsWith('/') ? _next : '/protected'
+
+ if (token_hash && type) {
+ const { supabase, headers } = createClient(request)
+ const { error } = await supabase.auth.verifyOtp({
+ type,
+ token_hash,
+ })
+ if (!error) {
+ return redirect(next, { headers })
+ } else {
+ return redirect(`/auth/error?error=${error?.message}`)
+ }
+ }
+
+ // redirect the user to an error page with some instructions
+ return redirect(`/auth/error?error=No token hash or type`)
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/app/routes/auth.error.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/app/routes/auth.error.tsx
new file mode 100644
index 00000000000..13321b70361
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/app/routes/auth.error.tsx
@@ -0,0 +1,35 @@
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { Button } from '@/registry/default/components/ui/button'
+import { Link } from 'react-router'
+
+export default function AuthErrorPage() {
+ return (
+
+
+
+
+ Authentication Error
+ There was an error authenticating your account
+
+
+
+
+ The authentication link may have expired or is invalid. Please try requesting a new
+ one.
+
+
+ Try again
+
+
+
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/app/routes/passwordless.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/app/routes/passwordless.tsx
new file mode 100644
index 00000000000..6f0dc9badc8
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/app/routes/passwordless.tsx
@@ -0,0 +1,11 @@
+import { PasswordlessLoginForm } from '@/registry/default/blocks/passwordless-auth-react-router/components/passwordless-login-form'
+
+export default function PasswordlessPage() {
+ return (
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/app/routes/protected.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/app/routes/protected.tsx
new file mode 100644
index 00000000000..0ecd6b4e8a8
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/app/routes/protected.tsx
@@ -0,0 +1,55 @@
+import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'
+import { Button } from '@/registry/default/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { type LoaderFunctionArgs, redirect, useLoaderData } from 'react-router'
+
+export const loader = async ({ request }: LoaderFunctionArgs) => {
+ const { supabase } = createClient(request)
+
+ const { data, error } = await supabase.auth.getUser()
+ if (error || !data?.user) {
+ return redirect('/passwordless')
+ }
+
+ return data
+}
+
+export default function ProtectedPage() {
+ let data = useLoaderData()
+
+ return (
+
+
+
+
+ Protected Page
+ You are logged in
+
+
+
+
+
Email
+
{data.user.email}
+
+
+
User ID
+
{data.user.id}
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/app/routes/verify-otp.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/app/routes/verify-otp.tsx
new file mode 100644
index 00000000000..95ea73144a4
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/app/routes/verify-otp.tsx
@@ -0,0 +1,11 @@
+import { OTPVerifyForm } from '@/registry/default/blocks/passwordless-auth-react-router/components/otp-verify-form'
+
+export default function VerifyOTPPage() {
+ return (
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/components/magic-link-form.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/components/magic-link-form.tsx
new file mode 100644
index 00000000000..3b8de4622e0
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/components/magic-link-form.tsx
@@ -0,0 +1,92 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+import { createClient } from '@/registry/default/clients/react-router/lib/supabase/client'
+import { Button } from '@/registry/default/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { Input } from '@/registry/default/components/ui/input'
+import { Label } from '@/registry/default/components/ui/label'
+import { useState } from 'react'
+
+export function MagicLinkForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
+ const [email, setEmail] = useState('')
+ const [error, setError] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const [isSuccess, setIsSuccess] = useState(false)
+ const supabase = createClient()
+
+ const handleMagicLinkRequest = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setIsLoading(true)
+ setError(null)
+ setIsSuccess(false)
+
+ try {
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ // Set this to false if you do not want the user to be automatically signed up
+ shouldCreateUser: true,
+ emailRedirectTo: `${window.location.origin}/auth/confirm`,
+ },
+ })
+ if (error) throw error
+ setIsSuccess(true)
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : 'An error occurred')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ Magic Link Login
+
+ Enter your email to receive a magic link for passwordless login
+
+
+
+ {isSuccess ? (
+
+
Check your email!
+
+ We've sent a magic link to {email} . Click the
+ link in the email to sign in.
+
+
+ ) : (
+
+ )}
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/components/otp-request-form.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/components/otp-request-form.tsx
new file mode 100644
index 00000000000..dd946513166
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/components/otp-request-form.tsx
@@ -0,0 +1,82 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+import { createClient } from '@/registry/default/clients/react-router/lib/supabase/client'
+import { Button } from '@/registry/default/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { Input } from '@/registry/default/components/ui/input'
+import { Label } from '@/registry/default/components/ui/label'
+import { useNavigate } from 'react-router'
+import { useState } from 'react'
+
+export function OTPRequestForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
+ const [email, setEmail] = useState('')
+ const [error, setError] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const navigate = useNavigate()
+ const supabase = createClient()
+
+ const handleOTPRequest = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setIsLoading(true)
+ setError(null)
+
+ try {
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ // Set this to false if you do not want the user to be automatically signed up
+ shouldCreateUser: true,
+ },
+ })
+ if (error) throw error
+ // Navigate to OTP verification page
+ navigate(`/verify-otp?email=${encodeURIComponent(email)}`)
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : 'An error occurred')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ OTP Login
+
+ Enter your email to receive a one-time password for passwordless login
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/components/otp-verify-form.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/components/otp-verify-form.tsx
new file mode 100644
index 00000000000..513e3adb314
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/components/otp-verify-form.tsx
@@ -0,0 +1,121 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+import { createClient } from '@/registry/default/clients/react-router/lib/supabase/client'
+import { Button } from '@/registry/default/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { Input } from '@/registry/default/components/ui/input'
+import { Label } from '@/registry/default/components/ui/label'
+import { useNavigate, useSearchParams } from 'react-router'
+import { useState } from 'react'
+
+export function OTPVerifyForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
+ const [searchParams] = useSearchParams()
+ const email = searchParams.get('email') || ''
+ const [otp, setOTP] = useState('')
+ const [error, setError] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const [isResending, setIsResending] = useState(false)
+ const navigate = useNavigate()
+ const supabase = createClient()
+
+ const handleVerifyOTP = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setIsLoading(true)
+ setError(null)
+
+ try {
+ const { error } = await supabase.auth.verifyOtp({
+ email,
+ token: otp,
+ type: 'email',
+ })
+ if (error) throw error
+ // Redirect to protected page after successful verification
+ navigate('/protected')
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : 'An error occurred')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const handleResendOTP = async () => {
+ setIsResending(true)
+ setError(null)
+
+ try {
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ shouldCreateUser: false,
+ },
+ })
+ if (error) throw error
+ setError(null)
+ // Show success message
+ alert('A new verification code has been sent to your email.')
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : 'Failed to resend code')
+ } finally {
+ setIsResending(false)
+ }
+ }
+
+ return (
+
+
+
+ Verify Your Email
+
+ Enter the 6-digit code sent to{' '}
+ {email}
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/components/passwordless-login-form.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/components/passwordless-login-form.tsx
new file mode 100644
index 00000000000..868611efe90
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/components/passwordless-login-form.tsx
@@ -0,0 +1,130 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+import { createClient } from '@/registry/default/clients/react-router/lib/supabase/client'
+import { Button } from '@/registry/default/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { Input } from '@/registry/default/components/ui/input'
+import { Label } from '@/registry/default/components/ui/label'
+import { useNavigate } from 'react-router'
+import { useState } from 'react'
+
+type LoginMethod = 'magic-link' | 'otp'
+
+interface PasswordlessLoginFormProps extends React.ComponentPropsWithoutRef<'div'> {
+ method?: LoginMethod
+}
+
+export function PasswordlessLoginForm({
+ className,
+ method = 'magic-link',
+ ...props
+}: PasswordlessLoginFormProps) {
+ const [email, setEmail] = useState('')
+ const [error, setError] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const [isMagicLinkSent, setIsMagicLinkSent] = useState(false)
+ const navigate = useNavigate()
+ const supabase = createClient()
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setIsLoading(true)
+ setError(null)
+ setIsMagicLinkSent(false)
+
+ try {
+ if (method === 'magic-link') {
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ shouldCreateUser: true,
+ emailRedirectTo: `${window.location.origin}/auth/confirm`,
+ },
+ })
+ if (error) throw error
+ setIsMagicLinkSent(true)
+ } else {
+ // OTP method
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ shouldCreateUser: true,
+ },
+ })
+ if (error) throw error
+ // Navigate to OTP verification page
+ navigate(`/verify-otp?email=${encodeURIComponent(email)}`)
+ }
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : 'An error occurred')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ Sign up
+ Sign in or create a new account
+
+
+ {isMagicLinkSent ? (
+
+
Check your email!
+
+ We've sent a magic link to {email} . Click
+ the link in the email to sign in.
+
+
{
+ setIsMagicLinkSent(false)
+ setEmail('')
+ }}
+ >
+ Send another link
+
+
+ ) : (
+
+ )}
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/registry-item.json b/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/registry-item.json
new file mode 100644
index 00000000000..4df86835838
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-react-router/registry-item.json
@@ -0,0 +1,55 @@
+{
+ "name": "passwordless-auth-react-router",
+ "type": "registry:block",
+ "title": "Passwordless Auth for React Router and Supabase",
+ "description": "Passwordless authentication for React Router using Supabase Auth. Supports both Magic Link and OTP (One-Time Password) login methods.",
+ "registryDependencies": ["button", "card", "input", "label"],
+ "dependencies": [
+ "@supabase/ssr@latest",
+ "@react-router/dev@latest",
+ "@react-router/fs-routes@latest"
+ ],
+ "files": [
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/components/passwordless-login-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/components/magic-link-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/components/otp-request-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/components/otp-verify-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/app/routes/auth.confirm.tsx",
+ "type": "registry:file",
+ "target": "app/routes/auth.confirm.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/app/routes/auth.error.tsx",
+ "type": "registry:file",
+ "target": "app/routes/auth.error.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/app/routes/passwordless.tsx",
+ "type": "registry:file",
+ "target": "app/routes/passwordless.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/app/routes/verify-otp.tsx",
+ "type": "registry:file",
+ "target": "app/routes/verify-otp.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react-router/app/routes/protected.tsx",
+ "type": "registry:file",
+ "target": "app/routes/protected.tsx"
+ }
+ ]
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-react/components/magic-link-form.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-react/components/magic-link-form.tsx
new file mode 100644
index 00000000000..d20d9316492
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-react/components/magic-link-form.tsx
@@ -0,0 +1,92 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+import { createClient } from '@/registry/default/clients/react/lib/supabase/client'
+import { Button } from '@/registry/default/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { Input } from '@/registry/default/components/ui/input'
+import { Label } from '@/registry/default/components/ui/label'
+import { useState } from 'react'
+
+export function MagicLinkForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
+ const [email, setEmail] = useState('')
+ const [error, setError] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const [isSuccess, setIsSuccess] = useState(false)
+ const supabase = createClient()
+
+ const handleMagicLinkRequest = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setIsLoading(true)
+ setError(null)
+ setIsSuccess(false)
+
+ try {
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ // Set this to false if you do not want the user to be automatically signed up
+ shouldCreateUser: true,
+ emailRedirectTo: `${window.location.origin}/auth/confirm`,
+ },
+ })
+ if (error) throw error
+ setIsSuccess(true)
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : 'An error occurred')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ Magic Link Login
+
+ Enter your email to receive a magic link for passwordless login
+
+
+
+ {isSuccess ? (
+
+
Check your email!
+
+ We've sent a magic link to {email} . Click the
+ link in the email to sign in.
+
+
+ ) : (
+
+ )}
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-react/components/otp-request-form.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-react/components/otp-request-form.tsx
new file mode 100644
index 00000000000..32d6e3af2d1
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-react/components/otp-request-form.tsx
@@ -0,0 +1,86 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+import { createClient } from '@/registry/default/clients/react/lib/supabase/client'
+import { Button } from '@/registry/default/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { Input } from '@/registry/default/components/ui/input'
+import { Label } from '@/registry/default/components/ui/label'
+import { useState } from 'react'
+
+interface OTPRequestFormProps extends React.ComponentPropsWithoutRef<'div'> {
+ onOtpRequested?: (email: string) => void
+}
+
+export function OTPRequestForm({ className, onOtpRequested, ...props }: OTPRequestFormProps) {
+ const [email, setEmail] = useState('')
+ const [error, setError] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const supabase = createClient()
+
+ const handleOTPRequest = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setIsLoading(true)
+ setError(null)
+
+ try {
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ // Set this to false if you do not want the user to be automatically signed up
+ shouldCreateUser: true,
+ },
+ })
+ if (error) throw error
+ // Call callback to notify parent component
+ if (onOtpRequested) {
+ onOtpRequested(email)
+ }
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : 'An error occurred')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ OTP Login
+
+ Enter your email to receive a one-time password for passwordless login
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-react/components/otp-verify-form.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-react/components/otp-verify-form.tsx
new file mode 100644
index 00000000000..a90b3d3c503
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-react/components/otp-verify-form.tsx
@@ -0,0 +1,124 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+import { createClient } from '@/registry/default/clients/react/lib/supabase/client'
+import { Button } from '@/registry/default/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { Input } from '@/registry/default/components/ui/input'
+import { Label } from '@/registry/default/components/ui/label'
+import { useState } from 'react'
+
+interface OTPVerifyFormProps extends React.ComponentPropsWithoutRef<'div'> {
+ email: string
+ onVerified?: () => void
+}
+
+export function OTPVerifyForm({ className, email, onVerified, ...props }: OTPVerifyFormProps) {
+ const [otp, setOTP] = useState('')
+ const [error, setError] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const [isResending, setIsResending] = useState(false)
+ const supabase = createClient()
+
+ const handleVerifyOTP = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setIsLoading(true)
+ setError(null)
+
+ try {
+ const { error } = await supabase.auth.verifyOtp({
+ email,
+ token: otp,
+ type: 'email',
+ })
+ if (error) throw error
+ // Call callback to notify parent component
+ if (onVerified) {
+ onVerified()
+ }
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : 'An error occurred')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const handleResendOTP = async () => {
+ setIsResending(true)
+ setError(null)
+
+ try {
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ shouldCreateUser: false,
+ },
+ })
+ if (error) throw error
+ setError(null)
+ // Show success message
+ alert('A new verification code has been sent to your email.')
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : 'Failed to resend code')
+ } finally {
+ setIsResending(false)
+ }
+ }
+
+ return (
+
+
+
+ Verify Your Email
+
+ Enter the 6-digit code sent to{' '}
+ {email}
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-react/components/passwordless-login-form.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-react/components/passwordless-login-form.tsx
new file mode 100644
index 00000000000..4b3fe54979e
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-react/components/passwordless-login-form.tsx
@@ -0,0 +1,132 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+import { createClient } from '@/registry/default/clients/react/lib/supabase/client'
+import { Button } from '@/registry/default/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { Input } from '@/registry/default/components/ui/input'
+import { Label } from '@/registry/default/components/ui/label'
+import { useState } from 'react'
+
+type LoginMethod = 'magic-link' | 'otp'
+
+interface PasswordlessLoginFormProps extends React.ComponentPropsWithoutRef<'div'> {
+ method?: LoginMethod
+ onOtpRequested?: (email: string) => void
+}
+
+export function PasswordlessLoginForm({
+ className,
+ method = 'magic-link',
+ onOtpRequested,
+ ...props
+}: PasswordlessLoginFormProps) {
+ const [email, setEmail] = useState('')
+ const [error, setError] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const [isMagicLinkSent, setIsMagicLinkSent] = useState(false)
+ const supabase = createClient()
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setIsLoading(true)
+ setError(null)
+ setIsMagicLinkSent(false)
+
+ try {
+ if (method === 'magic-link') {
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ shouldCreateUser: true,
+ emailRedirectTo: `${window.location.origin}/auth/confirm`,
+ },
+ })
+ if (error) throw error
+ setIsMagicLinkSent(true)
+ } else {
+ // OTP method
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ shouldCreateUser: true,
+ },
+ })
+ if (error) throw error
+ // Call callback to notify parent component
+ if (onOtpRequested) {
+ onOtpRequested(email)
+ }
+ }
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : 'An error occurred')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ Sign up
+ Sign in or create a new account
+
+
+ {isMagicLinkSent ? (
+
+
Check your email!
+
+ We've sent a magic link to {email} . Click
+ the link in the email to sign in.
+
+
{
+ setIsMagicLinkSent(false)
+ setEmail('')
+ }}
+ >
+ Send another link
+
+
+ ) : (
+
+ )}
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-react/registry-item.json b/apps/ui-library/registry/default/blocks/passwordless-auth-react/registry-item.json
new file mode 100644
index 00000000000..6dbf26c4357
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-react/registry-item.json
@@ -0,0 +1,26 @@
+{
+ "name": "passwordless-auth-react",
+ "type": "registry:block",
+ "title": "Passwordless Auth for React and Supabase",
+ "description": "Passwordless authentication for React using Supabase Auth. Supports both Magic Link and OTP (One-Time Password) login methods. Components only, no routing included.",
+ "registryDependencies": ["button", "card", "input", "label"],
+ "dependencies": ["@supabase/supabase-js@latest"],
+ "files": [
+ {
+ "path": "registry/default/blocks/passwordless-auth-react/components/passwordless-login-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react/components/magic-link-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react/components/otp-request-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-react/components/otp-verify-form.tsx",
+ "type": "registry:component"
+ }
+ ]
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/components/magic-link-form.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/components/magic-link-form.tsx
new file mode 100644
index 00000000000..5ce9e9c7ded
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/components/magic-link-form.tsx
@@ -0,0 +1,92 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'
+import { Button } from '@/registry/default/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { Input } from '@/registry/default/components/ui/input'
+import { Label } from '@/registry/default/components/ui/label'
+import { useState } from 'react'
+
+export function MagicLinkForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
+ const [email, setEmail] = useState('')
+ const [error, setError] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const [isSuccess, setIsSuccess] = useState(false)
+ const supabase = createClient()
+
+ const handleMagicLinkRequest = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setIsLoading(true)
+ setError(null)
+ setIsSuccess(false)
+
+ try {
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ // Set this to false if you do not want the user to be automatically signed up
+ shouldCreateUser: true,
+ emailRedirectTo: `${window.location.origin}/auth/confirm`,
+ },
+ })
+ if (error) throw error
+ setIsSuccess(true)
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : 'An error occurred')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ Magic Link Login
+
+ Enter your email to receive a magic link for passwordless login
+
+
+
+ {isSuccess ? (
+
+
Check your email!
+
+ We've sent a magic link to {email} . Click the
+ link in the email to sign in.
+
+
+ ) : (
+
+ )}
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/components/otp-request-form.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/components/otp-request-form.tsx
new file mode 100644
index 00000000000..9570ee1839c
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/components/otp-request-form.tsx
@@ -0,0 +1,82 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'
+import { Button } from '@/registry/default/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { Input } from '@/registry/default/components/ui/input'
+import { Label } from '@/registry/default/components/ui/label'
+import { useNavigate } from '@tanstack/react-router'
+import { useState } from 'react'
+
+export function OTPRequestForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
+ const [email, setEmail] = useState('')
+ const [error, setError] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const navigate = useNavigate()
+ const supabase = createClient()
+
+ const handleOTPRequest = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setIsLoading(true)
+ setError(null)
+
+ try {
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ // Set this to false if you do not want the user to be automatically signed up
+ shouldCreateUser: true,
+ },
+ })
+ if (error) throw error
+ // Navigate to OTP verification page
+ navigate({ to: '/verify-otp', search: { email } })
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : 'An error occurred')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ OTP Login
+
+ Enter your email to receive a one-time password for passwordless login
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/components/otp-verify-form.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/components/otp-verify-form.tsx
new file mode 100644
index 00000000000..d4fbd52372a
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/components/otp-verify-form.tsx
@@ -0,0 +1,121 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'
+import { Button } from '@/registry/default/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { Input } from '@/registry/default/components/ui/input'
+import { Label } from '@/registry/default/components/ui/label'
+import { useNavigate, useSearch } from '@tanstack/react-router'
+import { useState } from 'react'
+
+export function OTPVerifyForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
+ const search = useSearch({ strict: false })
+ const email = (search as { email?: string })?.email || ''
+ const [otp, setOTP] = useState('')
+ const [error, setError] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const [isResending, setIsResending] = useState(false)
+ const navigate = useNavigate()
+ const supabase = createClient()
+
+ const handleVerifyOTP = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setIsLoading(true)
+ setError(null)
+
+ try {
+ const { error } = await supabase.auth.verifyOtp({
+ email,
+ token: otp,
+ type: 'email',
+ })
+ if (error) throw error
+ // Redirect to protected page after successful verification
+ navigate({ to: '/protected' })
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : 'An error occurred')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const handleResendOTP = async () => {
+ setIsResending(true)
+ setError(null)
+
+ try {
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ shouldCreateUser: false,
+ },
+ })
+ if (error) throw error
+ setError(null)
+ // Show success message
+ alert('A new verification code has been sent to your email.')
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : 'Failed to resend code')
+ } finally {
+ setIsResending(false)
+ }
+ }
+
+ return (
+
+
+
+ Verify Your Email
+
+ Enter the 6-digit code sent to{' '}
+ {email}
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/components/passwordless-login-form.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/components/passwordless-login-form.tsx
new file mode 100644
index 00000000000..199858f18b1
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/components/passwordless-login-form.tsx
@@ -0,0 +1,130 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'
+import { Button } from '@/registry/default/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { Input } from '@/registry/default/components/ui/input'
+import { Label } from '@/registry/default/components/ui/label'
+import { useNavigate } from '@tanstack/react-router'
+import { useState } from 'react'
+
+type LoginMethod = 'magic-link' | 'otp'
+
+interface PasswordlessLoginFormProps extends React.ComponentPropsWithoutRef<'div'> {
+ method?: LoginMethod
+}
+
+export function PasswordlessLoginForm({
+ className,
+ method = 'magic-link',
+ ...props
+}: PasswordlessLoginFormProps) {
+ const [email, setEmail] = useState('')
+ const [error, setError] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const [isMagicLinkSent, setIsMagicLinkSent] = useState(false)
+ const navigate = useNavigate()
+ const supabase = createClient()
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setIsLoading(true)
+ setError(null)
+ setIsMagicLinkSent(false)
+
+ try {
+ if (method === 'magic-link') {
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ shouldCreateUser: true,
+ emailRedirectTo: `${window.location.origin}/auth/confirm`,
+ },
+ })
+ if (error) throw error
+ setIsMagicLinkSent(true)
+ } else {
+ // OTP method
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ shouldCreateUser: true,
+ },
+ })
+ if (error) throw error
+ // Navigate to OTP verification page
+ navigate({ to: '/verify-otp', search: { email } })
+ }
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : 'An error occurred')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ Sign up
+ Sign in or create a new account
+
+
+ {isMagicLinkSent ? (
+
+
Check your email!
+
+ We've sent a magic link to {email} . Click
+ the link in the email to sign in.
+
+
{
+ setIsMagicLinkSent(false)
+ setEmail('')
+ }}
+ >
+ Send another link
+
+
+ ) : (
+
+ )}
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/lib/supabase/fetch-user-server-fn.ts b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/lib/supabase/fetch-user-server-fn.ts
new file mode 100644
index 00000000000..9092fdd5a02
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/lib/supabase/fetch-user-server-fn.ts
@@ -0,0 +1,19 @@
+import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/server'
+import type { Factor, User } from '@supabase/supabase-js'
+import { createServerFn } from '@tanstack/react-start'
+type SSRSafeUser = User & {
+ factors: (Factor & { factor_type: 'phone' | 'totp' })[]
+}
+
+export const fetchUserServerFn: () => Promise = createServerFn({
+ method: 'GET',
+}).handler(async () => {
+ const supabase = createClient()
+ const { data, error } = await supabase.auth.getUser()
+
+ if (error) {
+ return null
+ }
+
+ return data.user as SSRSafeUser
+})
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/registry-item.json b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/registry-item.json
new file mode 100644
index 00000000000..9fe5a708cd2
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/registry-item.json
@@ -0,0 +1,60 @@
+{
+ "name": "passwordless-auth-tanstack",
+ "type": "registry:block",
+ "title": "Passwordless Auth for TanStack and Supabase",
+ "description": "Passwordless authentication for TanStack using Supabase Auth. Supports both Magic Link and OTP (One-Time Password) login methods.",
+ "registryDependencies": ["button", "card", "input", "label"],
+ "dependencies": ["@supabase/ssr@latest"],
+ "files": [
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/components/passwordless-login-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/components/magic-link-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/components/otp-request-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/components/otp-verify-form.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/routes/passwordless.tsx",
+ "type": "registry:file",
+ "target": "routes/passwordless.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/routes/auth/error.tsx",
+ "type": "registry:file",
+ "target": "routes/auth/error.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/routes/_protected.tsx",
+ "type": "registry:file",
+ "target": "routes/_protected.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/routes/_protected/protected.tsx",
+ "type": "registry:file",
+ "target": "routes/_protected/protected.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/routes/auth/confirm.ts",
+ "type": "registry:file",
+ "target": "routes/auth/confirm.ts"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/routes/verify-otp.tsx",
+ "type": "registry:file",
+ "target": "routes/verify-otp.tsx"
+ },
+ {
+ "path": "registry/default/blocks/passwordless-auth-tanstack/lib/supabase/fetch-user-server-fn.ts",
+ "type": "registry:lib"
+ }
+ ]
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/routes/_protected.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/routes/_protected.tsx
new file mode 100644
index 00000000000..8d3345b3b53
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/routes/_protected.tsx
@@ -0,0 +1,15 @@
+import { fetchUserServerFn } from '@/registry/default/blocks/passwordless-auth-tanstack/lib/supabase/fetch-user-server-fn'
+import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/_protected')({
+ beforeLoad: async () => {
+ const user = await fetchUserServerFn()
+ if (!user) {
+ throw redirect({
+ to: '/passwordless',
+ })
+ }
+ return { user }
+ },
+ component: () => ,
+})
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/routes/_protected/protected.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/routes/_protected/protected.tsx
new file mode 100644
index 00000000000..14bd75481fe
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/routes/_protected/protected.tsx
@@ -0,0 +1,53 @@
+import { Button } from '@/registry/default/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { createFileRoute, useRouter } from '@tanstack/react-router'
+import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'
+
+export const Route = createFileRoute('/_protected/protected')({
+ component: ProtectedPage,
+})
+
+function ProtectedPage() {
+ const router = useRouter()
+ const { user } = Route.useRouteContext()
+ const supabase = createClient()
+
+ const handleSignOut = async () => {
+ await supabase.auth.signOut()
+ router.navigate({ to: '/passwordless' })
+ }
+
+ return (
+
+
+
+
+ Protected Page
+ You are logged in
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/routes/auth/confirm.ts b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/routes/auth/confirm.ts
new file mode 100644
index 00000000000..4cb23b35752
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/routes/auth/confirm.ts
@@ -0,0 +1,61 @@
+import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/server'
+import { type EmailOtpType } from '@supabase/supabase-js'
+import { createFileRoute, redirect } from '@tanstack/react-router'
+import { createServerFn } from '@tanstack/react-start'
+import { getWebRequest } from '@tanstack/react-start/server'
+
+const confirmFn = createServerFn({ method: 'GET' })
+ .validator((searchParams: unknown) => {
+ if (
+ searchParams &&
+ typeof searchParams === 'object' &&
+ 'token_hash' in searchParams &&
+ 'type' in searchParams
+ ) {
+ return searchParams
+ }
+ throw new Error('Invalid search params')
+ })
+ .handler(async (ctx) => {
+ const request = getWebRequest()
+
+ if (!request) {
+ throw redirect({ to: `/auth/error`, search: { error: 'No request' } })
+ }
+
+ const searchParams = ctx.data
+ const token_hash = searchParams['token_hash'] as string
+ const type = searchParams['type'] as EmailOtpType | null
+ const _next = searchParams['next'] as string | undefined
+ const next = _next?.startsWith('/') ? _next : '/protected'
+
+ if (token_hash && type) {
+ const supabase = createClient()
+
+ const { error } = await supabase.auth.verifyOtp({
+ type,
+ token_hash,
+ })
+ if (!error) {
+ // redirect user to specified redirect URL or protected page
+ throw redirect({ href: next })
+ } else {
+ // redirect the user to an error page with some instructions
+ throw redirect({
+ to: `/auth/error`,
+ search: { error: error?.message },
+ })
+ }
+ }
+
+ // redirect the user to an error page with some instructions
+ throw redirect({
+ to: `/auth/error`,
+ search: { error: 'No token hash or type' },
+ })
+ })
+
+export const Route = createFileRoute('/auth/confirm')({
+ preload: false,
+ loader: (opts) => confirmFn({ data: opts.location.search }),
+})
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/routes/auth/error.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/routes/auth/error.tsx
new file mode 100644
index 00000000000..6684d819435
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/routes/auth/error.tsx
@@ -0,0 +1,39 @@
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/registry/default/components/ui/card'
+import { Button } from '@/registry/default/components/ui/button'
+import { Link, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/auth/error')({
+ component: AuthErrorPage,
+})
+
+function AuthErrorPage() {
+ return (
+
+
+
+
+ Authentication Error
+ There was an error authenticating your account
+
+
+
+
+ The authentication link may have expired or is invalid. Please try requesting a new
+ one.
+
+
+ Try again
+
+
+
+
+
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/routes/passwordless.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/routes/passwordless.tsx
new file mode 100644
index 00000000000..84c633cf27d
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/routes/passwordless.tsx
@@ -0,0 +1,16 @@
+import { PasswordlessLoginForm } from '@/registry/default/blocks/passwordless-auth-tanstack/components/passwordless-login-form'
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/passwordless')({
+ component: PasswordlessPage,
+})
+
+function PasswordlessPage() {
+ return (
+
+ )
+}
diff --git a/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/routes/verify-otp.tsx b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/routes/verify-otp.tsx
new file mode 100644
index 00000000000..20f1553b42f
--- /dev/null
+++ b/apps/ui-library/registry/default/blocks/passwordless-auth-tanstack/routes/verify-otp.tsx
@@ -0,0 +1,21 @@
+import { OTPVerifyForm } from '@/registry/default/blocks/passwordless-auth-tanstack/components/otp-verify-form'
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/verify-otp')({
+ component: VerifyOTPPage,
+ validateSearch: (search: Record): { email?: string } => {
+ return {
+ email: (search.email as string) || undefined,
+ }
+ },
+})
+
+function VerifyOTPPage() {
+ return (
+
+ )
+}
diff --git a/apps/ui-library/registry/default/examples/magic-link-auth.tsx b/apps/ui-library/registry/default/examples/magic-link-auth.tsx
new file mode 100644
index 00000000000..ece69d9a266
--- /dev/null
+++ b/apps/ui-library/registry/default/examples/magic-link-auth.tsx
@@ -0,0 +1,13 @@
+import { MagicLinkForm } from '@/registry/default/blocks/passwordless-auth-nextjs/components/magic-link-form'
+
+const MagicLinkAuthDemo = () => {
+ return (
+
+ )
+}
+
+export default MagicLinkAuthDemo
diff --git a/apps/ui-library/registry/default/examples/otp-auth.tsx b/apps/ui-library/registry/default/examples/otp-auth.tsx
new file mode 100644
index 00000000000..a206d409e33
--- /dev/null
+++ b/apps/ui-library/registry/default/examples/otp-auth.tsx
@@ -0,0 +1,13 @@
+import { OTPRequestForm } from '@/registry/default/blocks/passwordless-auth-nextjs/components/otp-request-form'
+
+const OTPAuthDemo = () => {
+ return (
+
+ )
+}
+
+export default OTPAuthDemo
diff --git a/apps/ui-library/registry/default/examples/passwordless-auth.tsx b/apps/ui-library/registry/default/examples/passwordless-auth.tsx
new file mode 100644
index 00000000000..0c1f8ac073c
--- /dev/null
+++ b/apps/ui-library/registry/default/examples/passwordless-auth.tsx
@@ -0,0 +1,13 @@
+import { PasswordlessLoginForm } from '@/registry/default/blocks/passwordless-auth-nextjs/components/passwordless-login-form'
+
+const PasswordlessAuthDemo = () => {
+ return (
+
+ )
+}
+
+export default PasswordlessAuthDemo
diff --git a/apps/ui-library/registry/examples.ts b/apps/ui-library/registry/examples.ts
index c7a160cd627..31774d3f3f6 100644
--- a/apps/ui-library/registry/examples.ts
+++ b/apps/ui-library/registry/examples.ts
@@ -34,6 +34,39 @@ export const examples: RegistryItem[] = [
},
],
},
+ {
+ name: 'passwordless-auth-demo',
+ type: 'registry:example',
+ registryDependencies: [],
+ files: [
+ {
+ path: 'registry/default/examples/passwordless-auth.tsx',
+ type: 'registry:example',
+ },
+ ],
+ },
+ {
+ name: 'magic-link-auth-demo',
+ type: 'registry:example',
+ registryDependencies: [],
+ files: [
+ {
+ path: 'registry/default/examples/magic-link-auth.tsx',
+ type: 'registry:example',
+ },
+ ],
+ },
+ {
+ name: 'otp-auth-demo',
+ type: 'registry:example',
+ registryDependencies: [],
+ files: [
+ {
+ path: 'registry/default/examples/otp-auth.tsx',
+ type: 'registry:example',
+ },
+ ],
+ },
{
name: 'current-user-avatar-demo',
type: 'registry:example',
diff --git a/apps/www/public/images/solutions/convex/convex-hero-dark.svg b/apps/www/public/images/solutions/convex/convex-hero-dark.svg
index 918db4b7884..96b56fd04d6 100644
--- a/apps/www/public/images/solutions/convex/convex-hero-dark.svg
+++ b/apps/www/public/images/solutions/convex/convex-hero-dark.svg
@@ -1,4 +1,5 @@
+<<<<<<< Updated upstream
@@ -32,22 +33,60 @@
+=======
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+>>>>>>> Stashed changes
+<<<<<<< Updated upstream
+=======
+
+
+
+
+>>>>>>> Stashed changes
+<<<<<<< Updated upstream
@@ -63,5 +102,25 @@
+=======
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+>>>>>>> Stashed changes
diff --git a/apps/www/public/images/solutions/convex/convex-hero-light.svg b/apps/www/public/images/solutions/convex/convex-hero-light.svg
index 490312642ca..f192ae1d1a7 100644
--- a/apps/www/public/images/solutions/convex/convex-hero-light.svg
+++ b/apps/www/public/images/solutions/convex/convex-hero-light.svg
@@ -1,4 +1,5 @@
+<<<<<<< Updated upstream
@@ -31,12 +32,40 @@
+=======
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+>>>>>>> Stashed changes
+<<<<<<< Updated upstream
@@ -54,6 +83,25 @@
+=======
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+>>>>>>> Stashed changes