PriceLogicPriceLogic

The AI Platform for Marketplace Sellers

© Copyright 2025 PriceLogic, Inc. All Rights Reserved.

About
  • Blog
  • Contact
Product
  • Pricing
Legal
  • Terms of Service
  • Privacy Policy
  • Cookie Policy
  • Getting started with PriceLogic
    • Quick Start
    • Project Structure
    • Configuration
  • Diana API
  • Email & Password
  • Database
    • Database Overview
    • Migrations
    • Row Level Security
    • Querying Data
    • Functions & Triggers
  • OAuth
  • Features
    • Features Overview
    • Team Collaboration
    • File Uploads
  • Magic Links
  • Billing & Payments
    • Billing Overview
    • Pricing Plans
    • Webhook Integration

Magic Links

Passwordless authentication with email magic links.

Note: This is mock/placeholder content for demonstration purposes.

Magic links provide passwordless authentication by sending a one-time link to the user's email.

How It Works

  1. User enters their email address
  2. System sends an email with a unique link
  3. User clicks the link in their email
  4. User is automatically signed in

Benefits

  • No password to remember - Better UX
  • More secure - No password to steal
  • Lower friction - Faster sign-up process
  • Email verification - Confirms email ownership

Implementation

Magic Link Form

'use client';

import { useForm } from 'react-hook-form';
import { sendMagicLinkAction } from '../_lib/actions';

export function MagicLinkForm() {
  const { register, handleSubmit, formState: { isSubmitting } } = useForm();
  const [sent, setSent] = useState(false);

  const onSubmit = async (data) => {
    const result = await sendMagicLinkAction(data);

    if (result.success) {
      setSent(true);
    }
  };

  if (sent) {
    return (
      <div className="text-center">
        <h2>Check your email</h2>
        <p>We've sent you a magic link to sign in.</p>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label>Email address</label>
        <input
          type="email"
          {...register('email', { required: true })}
          placeholder="you@example.com"
        />
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Sending...' : 'Send magic link'}
      </button>
    </form>
  );
}

Server Action

'use server';

import { enhanceAction } from '@kit/next/actions';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { z } from 'zod';

export const sendMagicLinkAction = enhanceAction(
  async (data) => {
    const client = getSupabaseServerClient();
    const origin = process.env.NEXT_PUBLIC_SITE_URL!;

    const { error } = await client.auth.signInWithOtp({
      email: data.email,
      options: {
        emailRedirectTo: `${origin}/auth/callback`,
        shouldCreateUser: true,
      },
    });

    if (error) throw error;

    return {
      success: true,
      message: 'Check your email for the magic link',
    };
  },
  {
    schema: z.object({
      email: z.string().email(),
    }),
  }
);

Configuration

Enable in Supabase

  1. Go to Authentication → Providers → Email
  2. Enable "Enable Email Provider"
  3. Enable "Enable Email Confirmations"

Configure Email Template

Customize the magic link email in Supabase Dashboard:

  1. Go to Authentication → Email Templates
  2. Select "Magic Link"
  3. Customize the template:
<h2>Sign in to {{ .SiteURL }}</h2>
<p>Click the link below to sign in:</p>
<p><a href="{{ .ConfirmationURL }}">Sign in</a></p>
<p>This link expires in {{ .TokenExpiryHours }} hours.</p>

Callback Handler

Handle the magic link callback:

// app/auth/callback/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  const requestUrl = new URL(request.url);
  const token_hash = requestUrl.searchParams.get('token_hash');
  const type = requestUrl.searchParams.get('type');

  if (token_hash && type === 'magiclink') {
    const cookieStore = cookies();
    const supabase = createRouteHandlerClient({ cookies: () => cookieStore });

    const { error } = await supabase.auth.verifyOtp({
      token_hash,
      type: 'magiclink',
    });

    if (!error) {
      return NextResponse.redirect(new URL('/home', request.url));
    }
  }

  // Return error if verification failed
  return NextResponse.redirect(
    new URL('/auth/sign-in?error=invalid_link', request.url)
  );
}

Advanced Features

Custom Redirect

Specify where users go after clicking the link:

await client.auth.signInWithOtp({
  email: data.email,
  options: {
    emailRedirectTo: `${origin}/onboarding`,
  },
});

Disable Auto Sign-Up

Require users to sign up first:

await client.auth.signInWithOtp({
  email: data.email,
  options: {
    shouldCreateUser: false, // Don't create new users
  },
});

Token Expiry

Configure link expiration (default: 1 hour):

-- In Supabase SQL Editor
ALTER TABLE auth.users
SET default_token_lifetime = '15 minutes';

Rate Limiting

Prevent abuse by rate limiting magic link requests:

import { ratelimit } from '~/lib/rate-limit';

export const sendMagicLinkAction = enhanceAction(
  async (data, user, request) => {
    // Rate limit by IP
    const ip = request.headers.get('x-forwarded-for') || 'unknown';
    const { success } = await ratelimit.limit(ip);

    if (!success) {
      throw new Error('Too many requests. Please try again later.');
    }

    const client = getSupabaseServerClient();

    await client.auth.signInWithOtp({
      email: data.email,
    });

    return { success: true };
  },
  { schema: EmailSchema }
);

Security Considerations

Link Expiration

Magic links should expire quickly:

  • Default: 1 hour
  • Recommended: 15-30 minutes for production
  • Shorter for sensitive actions

One-Time Use

Links should be invalidated after use:

// Supabase handles this automatically
// Each link can only be used once

Email Verification

Ensure emails are verified:

const { data: { user } } = await client.auth.getUser();

if (!user.email_confirmed_at) {
  redirect('/verify-email');
}

User Experience

Loading State

Show feedback while sending:

export function MagicLinkForm() {
  const [status, setStatus] = useState<'idle' | 'sending' | 'sent'>('idle');

  const onSubmit = async (data) => {
    setStatus('sending');
    await sendMagicLinkAction(data);
    setStatus('sent');
  };

  return (
    <>
      {status === 'idle' && <EmailForm onSubmit={onSubmit} />}
      {status === 'sending' && <SendingMessage />}
      {status === 'sent' && <CheckEmailMessage />}
    </>
  );
}

Resend Link

Allow users to request a new link:

export function ResendMagicLink({ email }: { email: string }) {
  const [canResend, setCanResend] = useState(false);
  const [countdown, setCountdown] = useState(60);

  useEffect(() => {
    if (countdown > 0) {
      const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
      return () => clearTimeout(timer);
    } else {
      setCanResend(true);
    }
  }, [countdown]);

  const handleResend = async () => {
    await sendMagicLinkAction({ email });
    setCountdown(60);
    setCanResend(false);
  };

  return (
    <button onClick={handleResend} disabled={!canResend}>
      {canResend ? 'Resend link' : `Resend in ${countdown}s`}
    </button>
  );
}

Email Deliverability

SPF, DKIM, DMARC

Configure email authentication:

  1. Add SPF record to DNS
  2. Enable DKIM signing
  3. Set up DMARC policy

Custom Email Domain

Use your own domain for better deliverability:

  1. Go to Project Settings → Auth
  2. Configure custom SMTP
  3. Verify domain ownership

Monitor Bounces

Track email delivery issues:

// Handle email bounces
export async function handleEmailBounce(email: string) {
  await client.from('email_bounces').insert({
    email,
    bounced_at: new Date(),
  });

  // Notify user via other channel
}

Testing

Local Development

In development, emails go to InBucket:

http://localhost:54324

Check this URL to see magic link emails during testing.

Test Mode

Create a test link without sending email:

if (process.env.NODE_ENV === 'development') {
  console.log('Magic link URL:', confirmationUrl);
}

Best Practices

  1. Clear communication - Tell users to check spam
  2. Short expiry - 15-30 minutes for security
  3. Rate limiting - Prevent abuse
  4. Fallback option - Offer password auth as backup
  5. Custom domain - Better deliverability
  6. Monitor delivery - Track bounces and failures
  7. Resend option - Let users request new link
  8. Mobile-friendly - Ensure links work on mobile
  1. How It Works
    1. Benefits
    2. Implementation
    3. Configuration
    4. Callback Handler
    5. Advanced Features
    6. Rate Limiting
    7. Security Considerations
    8. User Experience
    9. Email Deliverability
    10. Testing
    11. Best Practices