HelloJohn / docs
Authentication

Magic Links

Enable passwordless authentication via email magic links in HelloJohn. Setup, SMTP configuration, customization, and SDK integration.

Magic links let users sign in with a single click from their email — no password required. HelloJohn sends a time-limited link that authenticates the user when they click it.

How it works

1. User enters their email address
2. HelloJohn generates a one-time token and sends a magic link via email
3. User clicks the link (valid for 15 minutes by default)
4. HelloJohn validates the token and issues access + refresh tokens
5. SDK stores tokens — user is authenticated

The link is single-use. If the user clicks it twice, the second click returns an error.

Setup

1. Configure SMTP

Magic links require email delivery. Add your SMTP credentials to your environment:

SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=noreply@example.com
SMTP_PASSWORD=your-smtp-password
SMTP_FROM=HelloJohn <noreply@example.com>
SMTP_TLS=true

For local development, use Mailpit or Mailtrap:

SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_USER=
SMTP_PASSWORD=
SMTP_TLS=false

Go to Authentication → Magic Links in the admin panel and toggle it on.

Or via API:

curl -X PATCH https://your-instance.hellojohn.dev/v2/admin/auth/config \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"magic_link_enabled": true}'

3. Configure token expiry (optional)

MAGIC_LINK_TTL=900  # seconds (default: 15 minutes)

SDK integration

import { useAuth } from '@hellojohn/react'

function LoginForm() {
  const { sendMagicLink } = useAuth()
  const [email, setEmail] = useState('')
  const [sent, setSent] = useState(false)

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    await sendMagicLink(email)
    setSent(true)
  }

  if (sent) {
    return <p>Check your email — a login link is on the way.</p>
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={e => setEmail(e.target.value)}
        placeholder="you@example.com"
        required
      />
      <button type="submit">Send magic link</button>
    </form>
  )
}
'use client'
import { useHelloJohn } from '@hellojohn/nextjs'

export function MagicLinkForm() {
  const { sendMagicLink } = useHelloJohn()

  async function action(formData: FormData) {
    const email = formData.get('email') as string
    await sendMagicLink(email, {
      redirectTo: '/dashboard'  // where to go after clicking the link
    })
  }

  return (
    <form action={action}>
      <input name="email" type="email" required />
      <button type="submit">Send link</button>
    </form>
  )
}

Handle the callback in app/auth/magic-link/route.ts:

import { handleMagicLink } from '@hellojohn/nextjs/server'

export const GET = handleMagicLink({
  redirectTo: '/dashboard',
  onError: (error) => redirect('/login?error=invalid_link')
})
const { HelloJohn } = require('@hellojohn/js')

const hj = new HelloJohn({ instanceUrl: 'https://your-instance.hellojohn.dev' })

// Send the magic link
await hj.auth.sendMagicLink('user@example.com', {
  redirectTo: 'https://yourapp.com/auth/callback'
})

// Handle the callback (in your callback route)
const { session } = await hj.auth.verifyMagicLink(token)
// token comes from the ?token= query param in the magic link URL
// Send
await hj.auth.sendMagicLink(email, { redirectTo })

// Verify (in your callback endpoint)
app.get('/auth/magic', async (req, res) => {
  const { token } = req.query
  const { session, user } = await hj.auth.verifyMagicLink(token)
  req.session.token = session.accessToken
  res.redirect('/dashboard')
})

REST API

Send magic link:

POST /v2/auth/magic-link/send
Content-Type: application/json

{
  "email": "user@example.com",
  "redirect_to": "https://yourapp.com/auth/callback"
}

Response 200 OK:

{ "message": "Magic link sent" }

Verify token:

POST /v2/auth/magic-link/verify
Content-Type: application/json

{
  "token": "ml_xxxxxxxxxxxx"
}

Response 200 OK:

{
  "access_token": "eyJ...",
  "refresh_token": "rt_...",
  "expires_in": 900,
  "user": { "id": "usr_...", "email": "user@example.com" }
}

Customizing the email template

Magic link emails can be customized from the admin panel under Emails → Templates → Magic Link.

Available variables:

VariableValue
{{.UserEmail}}Recipient's email address
{{.MagicLinkURL}}The full magic link URL
{{.AppName}}Your application name
{{.ExpiresIn}}Expiry time (e.g. "15 minutes")

Default template is HTML with a plain-text fallback. You can upload a fully custom HTML template.

Per-tenant configuration

Override magic link settings per tenant:

PATCH /v2/admin/tenants/{tenantId}/auth/config
{
  "magic_link_enabled": true,
  "magic_link_ttl": 600
}

Magic links work best for low-frequency apps (internal tools, dashboards) where users may not remember their password. For high-frequency consumer apps, email + password or OAuth are usually better choices.

Security considerations

  • Links are single-use — clicking a valid link invalidates it immediately
  • Links expire after 15 minutes by default (configurable)
  • HelloJohn rate-limits magic link requests per email address (5 per hour by default)
  • The token is a cryptographically random 32-byte value — not guessable
  • Links are bound to the redirect_to URL set at send time — the callback cannot be changed

On this page