Quick Start
A full OTP login flow in three steps. No configuration is required to try it - in development guardo creates a throwaway Ethereal test inbox and prints a preview URL for each email (falling back to the console if Ethereal is unreachable).
import { createAuth } from "guardo";
const auth = createAuth({
jwt: { secret: process.env.JWT_SECRET! }, // must be at least 16 characters
});
// βββ Step 1: Send OTP βββββββββββββββββββββββββββββββββββββ
await auth.otp.send({ identifier: "user@example.com" });
// β OTP delivered to inbox (preview URL printed in dev)
// βββ Step 2: Verify OTP + login βββββββββββββββββββββββββββ
const { user, accessToken, refreshToken, sessionId } =
await auth.auth.loginWithOtp({
identifier: "user@example.com",
otp: "123456",
meta: { device: "chrome-mac", ip: req.ip },
});
// βββ Step 3: Protect routes ββββββββββββββββββββββββββββββ
app.get("/me", auth.middleware.express(), (req, res) => {
// Without `resolveUser`, req.user is the decoded JWT payload:
// { sub, email?, sessionId?, type, iat, exp }
res.json(req.user);
});By default req.user is the decoded token payload (sub, email?,
sessionId, type, iat, exp) - not the user object returned by
loginWithOtp(). Configure resolveUser to make
req.user your full User instead.
Want OTPs in your terminal instead of an email inbox? Pass
notifier: new ConsoleNotifier() to createAuth(). See
Notifiers.
What just happened
Send
auth.otp.send() generated a 6-digit code, stored only its SHA-256 hash, and
delivered it through the configured notifier.
Verify & log in
auth.auth.loginWithOtp() verified the code, resolved (or auto-provisioned) the
user, created a session, and issued an access + refresh token pair.
Protect
auth.middleware.express() validated the Authorization: Bearer header on every
request, attached req.user and req.session, and refreshed the sessionβs
lastActiveAt.
Next steps
- Configuration - tune TTLs, rate limits, storage, and more
- Auth Module - refresh tokens and logout
- Events - hook into the auth lifecycle for audit logging