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

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
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.
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.
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.
Protected routes go through the framework middleware, which verifies the access token and attaches the decoded user to the request.
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
Frameworks
Storage & delivery
Security
Tooling
05 · Screenshots
In the product


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.