ZionClad
Platform
Solutions
Deal AnalyzerCRM & PipelineContactsMail CenterReports & Exports
About UsResourcesPricingContact Us
Sign InGet Started →
Menu
Platform
SolutionsDeal AnalyzerCRM & PipelineContactsMail CenterReports & Exports
About UsResourcesPricingContact UsSign InGet Started
ZionClad

Commercial real estate intelligence for research, acquisition, and disposition.

PlatformDeal AnalyzerCRM & PipelineContactsMail CenterReports & Exports
AboutHelp CenterFAQContact Us
LegalTerms of ServicePrivacy PolicyCookie PolicyContacts Use Notice
© 2026 ZionClad. All rights reserved.
HomePlatformSign In

ZionClad provides software tools and information for general business use. Nothing on this site is legal, financial, tax or investment advice. You are responsible for checking the accuracy of information you enter, import or scan, and for how you use it.

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 * as z from 'zod';

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

    await sendMagicLink({
      email: data.email,
      redirectTo: `${origin}/auth/callback`,
      createUser: true,
    });

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

Configuration

Enable Magic Links

Configure magic links in your auth configuration:

// config/auth.config.ts
export const authConfig = {
  providers: {
    emailLink: true,
  },
};

Configure Email Template

Customize the magic link email 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 { verifyMagicLink } from '@kit/auth/server';
import { NextResponse } from 'next/server';

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

  if (token) {
    try {
      await verifyMagicLink(token);
      return NextResponse.redirect(new URL('/home', request.url));
    } catch (error) {
      // Return error if verification failed
      return NextResponse.redirect(
        new URL('/auth/sign-in?error=invalid_link', request.url)
      );
    }
  }

  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 sendMagicLink({
  email: data.email,
  redirectTo: `${origin}/onboarding`,
});

Disable Auto Sign-Up

Require users to sign up first:

await sendMagicLink({
  email: data.email,
  createUser: false, // Don't create new users
});

Token Expiry

Configure link expiration (default: 1 hour) in your auth configuration:

// config/auth.config.ts
export const authConfig = {
  magicLink: {
    expiresIn: '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.');
    }

    await sendMagicLink({
      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:

// Auth service handles this automatically
// Each link can only be used once

Email Verification

Ensure emails are verified:

import { getSession } from '@kit/auth/server';

const session = await getSession();

if (!session?.user.emailVerified) {
  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