nadun.me

Case study

Guardo

Production-ready authentication engine for Node.js and Next.js

An open-source npm package that wires OTP login, JWT access and refresh tokens, multi-device sessions and framework middleware together with security-first defaults. One secret is all the configuration it needs to start.

Role
Library design, engineering, documentation, npm release pipeline
Timeline
2026 - present
Status
Published on npm · v1.4.0 · MIT
Type
Open-source developer library
Guardo documentation home page showing the install command and a three-step OTP login example
Documentation site - install, then a full OTP login flow in three steps

1

Required config option (jwt.secret)

4

Middleware targets

3

Storage adapters

5

Core modules

01 · Problem

What was broken

Every Node.js or Next.js product needs the same authentication plumbing: send a one-time code, verify it, issue tokens, refresh them, track which devices are signed in, and protect routes. Teams either rebuild it from scratch each time or stitch together half a dozen libraries that were never designed to work together.

The details are easy to get wrong. One-time codes stored in plaintext, refresh tokens that never rotate, no way to detect a stolen token, and rate limits bolted on as an afterthought. Those mistakes rarely show up in a demo and always show up in production.

02 · Solution

What I built

Guardo packages the whole flow behind a single createAuth() call. OTP login, JWT access and refresh tokens, multi-device sessions with per-device revocation, and middleware for Express, Fastify and Next.js ship together and already know about each other.

The defaults are the secure path. Codes are hashed, compared in constant time and consumed on use. Refresh tokens rotate on every refresh, and replaying a revoked one revokes all of that user's sessions. Only the JWT secret is mandatory; everything else has a sensible default.

Storage, notification channels and OAuth providers are pluggable interfaces, so the same code runs against an in-memory store in tests and Redis in production, and sends codes through console, email, SMS or any custom channel.

03 · Architecture

How it fits together

Guardo is organised as a small set of modules exposed from one auth object, backed by two adapter interfaces and a family of framework wrappers.

OTP module

auth.otp

Generates and verifies time-limited codes with per-identifier and per-IP rate limiting

Auth module

auth.auth

Orchestrates login, token refresh and logout across the other modules

JWT module

auth.jwt

Issues and verifies access and refresh token pairs with configurable TTLs and extra claims

Session module

auth.session

Tracks multi-device sessions bound to the refresh token lifetime, with per-device revocation

OAuth module

auth.oauth

Social login for Google, GitHub and pluggable providers using PKCE and single-use CSRF state

Storage adapters

memory · ioredis · custom

In-memory for development and tests, Redis for production, or a custom StorageAdapter

Notifiers

Notifier interface

Console, Nodemailer email (Ethereal in development, SMTP in production), SMS, multi-channel or custom

Middleware

auth.middleware.*

Route protection for Express, Fastify, Next.js Edge middleware and the App Router route wrapper

Request flow

  1. auth.otp.send() generates a code, stores only its SHA-256 hash with an expiry, applies rate limits, and delivers it through the configured notifier.

  2. auth.auth.loginWithOtp() verifies the code in constant time, consumes it, resolves or creates the user via resolveUser and onNewUser hooks, and creates a session for that device.

  3. The JWT module signs a short-lived access token and a longer-lived refresh token tied to the session; in cookie mode both travel as httpOnly cookies.

  4. Protected routes go through the framework middleware, which verifies the access token and attaches the decoded user to the request.

  5. auth.auth.refresh() rotates the refresh token and session. A replayed, already-rotated token triggers reuse detection, revokes every session for that user and emits a typed event.

04 · Technologies

Stack

Language & runtime

TypeScriptNode.jsNext.js Edge runtime

Frameworks

ExpressFastifyNext.js MiddlewareNext.js App Router

Storage & delivery

Redis (ioredis)In-memory storeNodemailerSMTPSMS

Security

JWTSHA-256 OTP hashingTiming-safe comparisonPKCEhttpOnly cookies

Tooling

JestGitHub ActionsnpmDocs site

05 · Screenshots

In the product

Guardo security notes documentation page listing hashed OTP storage, timing-safe comparison, token rotation and reuse detection
Security notes - what happens under the hood, plus a production hardening checklist
Guardo Next.js middleware documentation page
Next.js integration - Edge middleware and App Router route wrapper
auth.ts
import { createAuth } from "guardo";

const auth = createAuth({
  jwt: { secret: process.env.JWT_SECRET! },
});

// 1. Send a one-time code
await auth.otp.send({ identifier: "user@example.com" });

// 2. Verify it and log in
const { user, accessToken, refreshToken, sessionId } =
  await auth.auth.loginWithOtp({
    identifier: "user@example.com",
    otp: "123456",
    meta: { device: "chrome-mac", ip: req.ip },
  });

// 3. Protect routes
app.get("/me", auth.middleware.express(), (req, res) => {
  res.json(req.user);
});

06 · Key features

What it does

OTP login

Email or SMS codes with configurable length and expiry. Codes are single-use and invalidated after five failed attempts.

JWT access + refresh

Short-lived access tokens (15 minutes by default) and rotating refresh tokens (7 days), with extra claims when you need them.

Multi-device sessions

Each login creates a session with device metadata. Revoke one device or all of them; sessions expire with their refresh token.

Four middleware targets

Drop-in route protection for Express, Fastify, Next.js Edge middleware and Next.js App Router handlers.

Pluggable storage

In-memory for local development and tests, Redis for shared production state, or implement the StorageAdapter interface.

Rate limiting built in

Separate limits for sending and verifying, per identifier and per IP, with a RateLimitError that reports retryAfterSeconds.

Typed lifecycle events

Hook into login, refresh, logout and token.reuse_detected to alert, audit or integrate with your own systems.

OAuth providers

Google and GitHub out of the box, with a pluggable provider interface, PKCE (S256) and single-use CSRF state.

Cookie mode

Keep tokens out of JavaScript-readable storage by transporting them as httpOnly cookies.

07 · Challenges

Hard parts and how I solved them

Challenge

Making the secure path the default without burying users in configuration.

Solution

Only jwt.secret is required and Guardo refuses secrets shorter than 16 characters. Hashing, timing-safe comparison, single-use codes, attempt limits and rotation are always on rather than opt-in.

Challenge

Detecting stolen refresh tokens without breaking legitimate clients that retry a request.

Solution

Every refresh rotates the token and the session. Replaying an already-rotated token is treated as reuse: all sessions for that user are revoked and a typed event lets the app alert on it.

Challenge

Running the same code in Node servers and in the Next.js Edge runtime, which has a different set of available APIs.

Solution

Framework-specific wrappers isolate runtime differences, so the core modules stay portable and a Next.js Edge middleware sits alongside Express and Fastify adapters.

Challenge

Keeping rate limits and sessions consistent across multiple server instances.

Solution

State lives behind a StorageAdapter. The Redis store shares sessions and rate limits across instances, while the in-memory store keeps tests fast and dependency-free.

Challenge

Shipping releases developers can trust.

Solution

A Jest test suite, TypeScript types for every public surface, a changelog, and a GitHub Actions release workflow that publishes tagged versions to npm.

08 · Live demo

See Guardo for yourself

An open-source npm package that wires OTP login, JWT access and refresh tokens, multi-device sessions and framework middleware together with security-first defaults. One secret is all the configuration it needs to start.