---
title: "Auth.js + Resend: the 30-minute magic link auth setup"
description: "Magic link auth removes passwords without rolling your own crypto. The Auth.js + Resend pattern is the cleanest path to a real signup flow that won't embarrass you on launch day."
date_published: 2026-04-20
last_updated: 2026-04-20
canonical: https://vibecodersguidetomvp.help/blog/authjs-resend-magic-link/
author: Titan Alpha
tags: ["auth","resend","mvp"]
---

# Auth.js + Resend: the 30-minute magic link auth setup

Magic link auth removes passwords without rolling your own crypto. The Auth.js + Resend pattern is the cleanest path to a real signup flow that won't embarrass you on launch day.

> Canonical HTML: https://vibecodersguidetomvp.help/blog/authjs-resend-magic-link/
> This is the agent-friendly markdown alternate for the page above.


Authentication is the first thing founders try to build themselves and the first thing they should not.

I have lost count of the number of MVPs I've reviewed where the founder, asked about auth, says some version of "oh yeah I just did the basic email/password thing, I hash the passwords with bcrypt, it's fine." And then I look at the code and find that the session token is a 4-digit number stored in `localStorage`, password reset emails are sent in plain text, the "remember me" checkbox does nothing, and admin access is gated by checking if the email contains the string "admin."

This is the default. Don't do the default.

The shape that works for an MVP is: **Auth.js v5 with the Resend (email magic link) provider, plus optionally Google OAuth.** It takes 30 minutes to set up. The user types their email, gets a link, clicks the link, they're in. No passwords, no password reset, no "I forgot my password" support emails for you to handle.

Here's the whole flow.

## What you'll need

- A Next.js 15 app (App Router).
- A Resend account ([resend.com](https://resend.com), free tier covers 100 emails/day).
- A database adapter — for MVP, the in-memory JWT strategy is fine. You'll need a DB later for sessions to persist across devices, but skip that on day one.

## Step by step

**1. Get a Resend API key.** Sign up. Go to API Keys. Create one. Copy the value (starts with `re_`).

**2. Install:**

```bash
npm install next-auth@beta
```

**3. Add to `.env.local`:**

```
AUTH_SECRET=<openssl rand -base64 32>
RESEND_API_KEY=re_xxxxxxxxxxxxxxxx
EMAIL_FROM="App <onboarding@resend.dev>"
```

The `onboarding@resend.dev` sender works without verifying your domain. Production should switch to your own domain after you set up DNS records (see Resend's docs), but `onboarding@resend.dev` is fine for testing.

**4. Create `auth.ts` at the project root:**

```ts
import NextAuth from 'next-auth';
import Resend from 'next-auth/providers/resend';
import Google from 'next-auth/providers/google';

const providers: any[] = [];

if (process.env.RESEND_API_KEY) {
  providers.push(Resend({ from: process.env.EMAIL_FROM }));
}
if (process.env.AUTH_GOOGLE_ID && process.env.AUTH_GOOGLE_SECRET) {
  providers.push(Google);
}

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers,
  session: { strategy: 'jwt' },
  pages: { signIn: '/login' },
});
```

The conditional providers pattern means you can add or remove providers by setting/unsetting env vars — no code changes needed.

**5. Wire the auth route handler at `app/api/auth/[...nextauth]/route.ts`:**

```ts
export { GET, POST } from '@/auth';
```

That's it. Two lines.

**6. Build the login page** at `app/login/page.tsx`:

```tsx
import { signIn } from '@/auth';

export default function LoginPage() {
  return (
    <div className="max-w-md mx-auto px-6 py-20">
      <h1 className="text-3xl font-semibold">Sign in</h1>
      <p className="mt-2 opacity-75">Email yourself a one-tap link.</p>
      <form
        action={async (formData: FormData) => {
          'use server';
          await signIn('resend', { email: formData.get('email') });
        }}
        className="mt-8 flex flex-col gap-2"
      >
        <input
          name="email"
          type="email"
          required
          className="input input-bordered"
          placeholder="you@example.com"
        />
        <button className="btn btn-primary">Send magic link</button>
      </form>
    </div>
  );
}
```

The user types their email, hits "Send magic link," and Auth.js + Resend take it from there. They get an email with a link. They click. They're signed in.

**7. Gate a protected route** by calling `auth()` server-side at the top:

```tsx
import { auth } from '@/auth';
import { redirect } from 'next/navigation';

export default async function DashboardPage() {
  const session = await auth();
  if (!session?.user) redirect('/login');
  return <div>Welcome, {session.user.email}</div>;
}
```

**8. Sign-out button:**

```tsx
import { signOut } from '@/auth';

<form action={async () => { 'use server'; await signOut(); }}>
  <button className="btn btn-ghost btn-sm">Sign out</button>
</form>
```

Done. End-to-end auth in eight steps. About 30 minutes the first time.

::: poll What's your preferred auth pattern?
- Magic link (passwordless email)
- OAuth (Google / GitHub / etc.)
- Email + password (with verification)
- Passkeys
- I don't have an opinion
:::

## Why this beats the alternatives

**vs. rolling your own.** Auth.js handles session token generation, cookie security flags (`httpOnly`, `Secure`, `SameSite=Lax`), CSRF protection, OAuth state parameters, and the dozens of other things that go wrong when you build it yourself. It is maintained by people who care about this stuff full-time. You are not.

**vs. Clerk.** Clerk is great. It costs money beyond the free tier (which is real but not generous). For an MVP with under 10,000 monthly actives, Auth.js is free. The trade-off is that Auth.js makes you wire the UI yourself; Clerk gives you pre-built sign-in components. If your time is more valuable than $25/month, Clerk wins. Otherwise Auth.js does.

**vs. Supabase Auth.** Also great. Tighter coupling to Supabase the database. Use it if you're already using Supabase for everything; otherwise Auth.js is more portable.

**vs. Firebase Auth.** It's 2026, please don't.

## What about email/password?

If your users specifically expect email/password (legacy industries, older audiences), you can add the Auth.js Credentials provider on top. You'll need to hash passwords with `bcryptjs`, store them in a `users` table, and add a verification flow. This adds maybe an hour to the build. The skill bundle's auth skill (sub-skill 03) walks through it.

For most modern audiences, magic link is the better UX. Less friction at signup, no "I forgot my password" support flow, no password breach risk.

## The single most-skipped step

Before launch, use the OWASP-recommended secure cookie flags. Auth.js sets them by default — don't override them. Check your browser dev tools after signing in and confirm the session cookie shows `HttpOnly`, `Secure`, `SameSite=Lax`. If it doesn't, you've broken something.

That's the whole thing. Auth.js + Resend, 30 minutes, real auth, no passwords. Don't write your own. Use this.

