Blog

Feb 18, 2025

Dev

How To Capture User Details with Magic Link Signup using AuthJs

With Magic Links you don’t get the typical OAuth flow where you can modify user data inside a callback. Here's how to do it anyway.

I ran into this problem and didn’t find a clear solution in the docs—so I’m writing about it.

When using Magic Links with AuthJs for authentication, you might want to collect additional user details—like first name and last name—during signup. With magic links you don’t get the typical OAuth flow where you can modify user data inside a callback.

The solution? Save user details in the database before authentication.


The Problem

By default, when a user signs in with a magic link, AuthJs only knows their email. If you need extra details like firstName and lastName, there’s no built-in way to capture them before the magic link is sent.

Callbacks Won’t Work

AuthJs callbacks like signIn don’t run before the user receives their magic link, so we can’t modify or store user data there. Instead, we need a pre-authentication step that:

  • Saves user details in the database
  • Triggers the magic link authentication afterward

The Solution: Server Actions for Signup

1. Create a Server Action to Save User Data

Define a server action to:

  • Check if the user exists in the database.
  • Insert them if they don’t exist.
  • Trigger authentication via magic link.
"use server";

import { db } from "@/lib/db";
import { users } from "@/lib/db/schema";
import { signIn } from "@/auth";

export async function signUp(email: string, firstName: string, lastName: string) {
  if (!email || !firstName || !lastName) {
    throw new Error("Missing required fields");
  }

  // Check if user already exists
  const existingUser = await db.select().from(users).where(users.email.eq(email));

  if (!existingUser.length) {
    await db.insert(users).values({ email, firstName, lastName });
  }

  // Now trigger the magic link authentication
  await signIn("resend", { email, redirect: true });
}

2. Use This in a Client-Side Signup Form

The client component calls the server action when the form is submitted.

"use client";

import { useState, useTransition } from "react";
import { signUp } from "@/actions/auth";

export default function SignUpForm() {
  const [email, setEmail] = useState("");
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [pending, startTransition] = useTransition();

  const handleSignUp = (e: React.FormEvent) => {
    e.preventDefault();
    startTransition(async () => {
      try {
        await signUp(email, firstName, lastName);
      } catch (error) {
        alert(error.message);
      }
    });
  };

  return (
    <form>
    </form>
  );
}

3. Modify the AuthJs Session Callback

After authentication, we need to retrieve firstName and lastName from the database and add them to the session.

import { AuthOptions } from "next-auth";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import EmailProvider from "next-auth/providers/resend";
import { db } from "@/lib/db";
import { users } from "@/lib/db/schema";

export const authOptions: AuthOptions = {
  adapter: DrizzleAdapter(db),
  providers: [
    EmailProvider({
      server: process.env.RESEND_API_KEY!,
      from: "[email protected]",
    }),
  ],
  callbacks: {
    async session({ session, user }) {
      const dbUser = await db.select().from(users).where(users.email.eq(user.email));

      if (dbUser.length) {
        session.user.firstName = dbUser[0].firstName;
        session.user.lastName = dbUser[0].lastName;
      }

      return session;
    },
  },
};

This ensures:

User details are saved before authentication

Authjs gets enriched session data

Everything works smoothly with Authjs magic links

If you’re using magic links and Authjs, this is the cleanest way to capture and persist additional user details. Hope this helps!

Stuff I Read & Write

If you don't mind getting an occasional email, I'll send interesting articles to your inbox.