Passwords are messy. Users forget them, reset flows break, and security teams keep telling us to add more rules (uppercase, symbols, no reuse). For developers, this means complexity. For users, it means frustration.
There’s a cleaner way: magic links.
With Scalekit, you can implement passwordless login in Next.js 15 using nothing more than API routes and middleware. Let’s see how.
Why magic links?
At Scalekit, we’ve seen teams run into the same problems:
- Password resets flood support.
- SMS-based codes lag or fail at scale.
- Session state spreads across multiple services, making incidents hard to debug.
Magic links fix this by collapsing login into three simple server-side steps:
- Issue a link
- Verify it
- Create a session
That’s it. No passwords, no SMS gateways, no half-baked tokens in the browser.
Step 1: Sending a link
In Next.js, create an API route /api/send-magic-link
that accepts an email and calls Scalekit to generate a passwordless request:
// /api/send-magic-link/route.ts
export async function POST(req: NextRequest) {
const { email } = await req.json()
const resp = await client.passwordless.createAuthRequest({
email,
passwordlessType: 'MAGIC_LINK',
expiresIn: 600,
})
const res = NextResponse.json({ ok: true })
res.cookies.set('sk_auth_request_id', resp.authRequestId, { httpOnly: true, secure: true })
return res
}
The cookie (sk_auth_request_id
) ties the link back to this origin so clients can’t fake it.
Step 2: Verifying
When the user clicks the link, your /api/verify-magic-link
route checks the token:
export async function POST(req: NextRequest) {
const { link_token, auth_request_id } = await req.json()
const result = await client.passwordless.verifyAuthRequest({ linkToken: link_token, authRequestId: auth_request_id })
return NextResponse.json({ email: result.email })
}
Step 3: creating a session
Finally, issue a short-lived JWT stored in an HttpOnly
cookie:
const token = jwt.sign({ email }, process.env.SESSION_JWT_SECRET, { expiresIn: '30m' })
res.cookies.set('sk_session', token, {
httpOnly: true,
secure: true,
sameSite: 'lax'
})
Middleware can now enforce that any protected route requires a valid JWT before it runs.
Why this works
Server-first: all sensitive logic (link creation, verification, session minting) happens in the backend.
Client-agnostic: the same API works for web apps, mobile apps, even CLI tools.
Observable: logs tell you who requested, who verified, and when the session was issued.
This design trims moving parts and makes login flows something you can trust and monitor.
Full guide
This post only scratches the surface. The full tutorial covers:
- Rate limiting (stop bots from spamming send/verify).
- Security headers (CSP, HSTS, etc).
- Structured logging with correlation IDs.
- Redis/SQL persistence for production.
👉 Read the complete step-by-step guide here
Your turn
Have you implemented passwordless login in your projects? Magic links, OTPs, or something else?
Share your setup, lessons, or gotchas in the comments: Other devs (and future you) will thank you.
Top comments (0)