Guides · August 4, 2026 · The Postbox team

Build passwordless login with magic links

Skip passwords entirely. Email a short-lived signed link, verify it on click, and start a session. Here is the whole flow.

Passwords are a liability: users reuse them, forget them, and phish easily on them. Magic links sidestep all of that. The user types their email, you send them a one-time link, and clicking it logs them in. The only infrastructure you need is a way to send that email reliably, which is exactly what Postbox is for.

Here is the whole flow in about thirty lines.

1. Send the link

When the user submits their email, mint a short-lived signed token and email them a link that carries it. The token encodes the email and an expiry, signed with a secret only your server knows.

import crypto from "node:crypto";
import { Postbox } from "@postbox/sdk";
 
const pb = new Postbox({ apiKey: process.env.PB_KEY! });
 
function sign(email: string): string {
  const payload = `${email}:${Date.now() + 15 * 60_000}`;
  const sig = crypto.createHmac("sha256", process.env.TOKEN_SECRET!).update(payload).digest("base64url");
  return Buffer.from(`${payload}:${sig}`).toString("base64url");
}
 
async function sendMagicLink(email: string) {
  const link = `${process.env.APP_URL}/auth/verify?token=${sign(email)}`;
 
  await pb.messages.send({
    from: "[email protected]",
    to: [{ address: email }],
    subject: "Your login link",
    bodyHtml: `<p><a href="${link}">Sign in to Acme</a></p><p>This link expires in 15 minutes.</p>`,
    bodyText: `Sign in: ${link} (expires in 15 minutes)`,
  });
}

2. Verify the click

When the link is opened, re-derive the signature and check the expiry. If it holds up, the email is proven and you can start a session.

function verify(token: string): string | null {
  const [email, expiry, sig] = Buffer.from(token, "base64url").toString().split(":");
  const expected = crypto
    .createHmac("sha256", process.env.TOKEN_SECRET!)
    .update(`${email}:${expiry}`)
    .digest("base64url");
 
  const ok = crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  if (!ok || Date.now() > Number(expiry)) return null;
  return email;
}

That is the entire mechanism: a signed, expiring token delivered by email. No password store, no reset flow, no third-party identity provider.

Making it production-ready

A few things worth adding before you ship:

A complete, runnable version of this (Express, sessions, and all) lives in our examples, and the send API is documented with copy-paste snippets in every language in the docs.

authmagic-linksnode
← all posts