npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

auth-engine

v1.0.0

Published

A framework-agnostic, security-first authentication engine with JWT + database-backed refresh tokens.

Downloads

102

Readme

auth-engine

A framework-agnostic, security-first authentication engine with JWT access tokens and database-backed refresh token rotation.

Built for SaaS developers who need production-grade auth without vendor lock-in.

This library ships functions and interfaces — not a database. You implement two small interfaces using your own ORM (Prisma, Drizzle, raw SQL, etc.). The engine handles all the auth logic. Your database connection string lives entirely in your own code.

npm version License: MIT TypeScript


Why Auth Engine?

Most auth libraries are either too coupled to a framework or too simple for production. Auth Engine gives you a modular core that handles the hard parts — token rotation, reuse detection, session revocation — while staying completely decoupled from your framework and database.

What you get:

  • Hybrid sessions — Short-lived JWT access tokens (stateless) + database-backed refresh tokens (revocable)
  • Automatic token rotation — Refresh tokens rotate on every use
  • Reuse detection — If a stolen token is replayed, all sessions are revoked instantly
  • Framework adapters — Next.js App Router adapter included, bring your own for others
  • Database agnostic — Implement two interfaces, use any ORM or database
  • Zero vendor lock-in — Your auth, your infrastructure

Quick Start

Install

npm install auth-engine

1. Implement the repository interfaces

Auth Engine needs two things from your database — a way to find users and a way to manage refresh tokens. Implement these interfaces with your ORM of choice (Prisma, Drizzle, Knex, raw SQL, etc.):

import type { UserRepository, RefreshTokenRepository } from "auth-engine";

// Example with Prisma — your DATABASE_URL lives in your own Prisma client, not here
import { prisma } from "@/lib/prisma"; // your DB client — auth-engine never sees your connection string

const userRepository: UserRepository = {
  findByEmail: (email) => prisma.user.findUnique({ where: { email } }),
  findById: (id) => prisma.user.findUnique({ where: { id } }),
  create: (data) => prisma.user.create({ data }),
  updatePassword: (userId, passwordHash) =>
    prisma.user.update({ where: { id: userId }, data: { passwordHash } }).then(() => void 0),
};

const refreshTokenRepository: RefreshTokenRepository = {
  create: (data) => prisma.refreshToken.create({ data }),
  findByHash: (tokenHash) => prisma.refreshToken.findFirst({ where: { tokenHash } }),
  revoke: (id) =>
    prisma.refreshToken.update({
      where: { id },
      data: { revoked: true, revokedAt: new Date() },
    }).then(() => void 0),
  revokeAllByUser: (userId) =>
    prisma.refreshToken.updateMany({
      where: { userId, revoked: false },
      data: { revoked: true, revokedAt: new Date() },
    }).then(() => void 0),
  deleteExpired: async (before) => {
    const result = await prisma.refreshToken.deleteMany({ where: { expiresAt: { lt: before } } });
    return result.count;
  },
};

2. Add environment variables

# Your database — used by YOUR ORM client, never by auth-engine
DATABASE_URL=postgresql://user:password@localhost:5432/mydb

# JWT signing secret — passed to createNextAuth config below
JWT_SECRET=your-random-secret-at-least-32-characters-long

auth-engine only ever receives the JWT_SECRET. Your DATABASE_URL goes directly into your Prisma/Drizzle client — the library never touches it.

3. Set up with Next.js

// lib/auth.ts
import { createNextAuth } from "auth-engine/adapters/next";

export const { handlers, withAuth, engine } = createNextAuth({
  config: {
    accessToken: {
      secret: process.env.JWT_SECRET!, // min 32 chars
      expiresIn: "10m",
    },
    refreshToken: {
      expiresInMs: 7 * 24 * 60 * 60 * 1000, // 7 days
    },
  },
  userRepository,
  refreshTokenRepository,
});

4. Create API routes

// app/api/auth/login/route.ts
import { handlers } from "@/lib/auth";

export const POST = handlers.login;
// app/api/auth/register/route.ts
import { handlers } from "@/lib/auth";

export const POST = handlers.register;
// app/api/auth/refresh/route.ts
import { handlers } from "@/lib/auth";

export const POST = handlers.refresh;
// app/api/auth/logout/route.ts
import { handlers } from "@/lib/auth";

export const POST = handlers.logout;

5. Protect routes

// app/api/me/route.ts
import { NextResponse } from "next/server";
import { withAuth } from "@/lib/auth";

export const GET = withAuth(async (req, { user }) => {
  return NextResponse.json({ user: { id: user.id, email: user.email } });
});

Architecture

┌─────────────────────────────────────┐
│   Your Application (Next.js, etc.)  │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│   Adapter Layer                     │
│   HTTP handling, cookies, responses │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│   Auth Core Engine                  │
│   Framework-agnostic business logic │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│   Repository Contracts              │
│   Interfaces only — you implement   │
└─────────────────────────────────────┘

Core rule: Lower layers never depend on upper layers. The core has zero framework imports.

Database Schema

You own the database. Auth Engine only defines the shape of the data it needs — you create the tables however you like.

Your users table must have at minimum: id, email, and passwordHash columns.

Prisma

model User {
  id            String         @id @default(uuid())
  email         String         @unique
  passwordHash  String
  refreshTokens RefreshToken[]
  createdAt     DateTime       @default(now())
}

model RefreshToken {
  id        String    @id @default(uuid())
  userId    String
  user      User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  tokenHash String    @unique
  parentId  String?
  revoked   Boolean   @default(false)
  revokedAt DateTime?
  userAgent String?
  ipHash    String?
  expiresAt DateTime
  createdAt DateTime  @default(now())
}

SQL (raw)

CREATE TABLE users (
  id            UUID PRIMARY KEY,
  email         TEXT UNIQUE NOT NULL,
  password_hash TEXT NOT NULL,
  created_at    TIMESTAMP DEFAULT NOW()
);

CREATE TABLE refresh_tokens (
  id          UUID PRIMARY KEY,
  user_id     UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  token_hash  TEXT UNIQUE NOT NULL,
  parent_id   UUID REFERENCES refresh_tokens(id),
  revoked     BOOLEAN DEFAULT FALSE,
  revoked_at  TIMESTAMP,
  user_agent  TEXT,
  ip_hash     TEXT,
  expires_at  TIMESTAMP NOT NULL,
  created_at  TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
CREATE INDEX idx_refresh_tokens_token_hash ON refresh_tokens(token_hash);

Security Model

| Threat | How Auth Engine handles it | |---|---| | Stolen access token | 10-minute expiry limits the damage window | | Stolen refresh token | Tokens are hashed (SHA-256) before storage. Rotation on every use means the stolen token becomes invalid after one refresh | | Token replay attack | Reuse detection: if a revoked token is presented, ALL of the user's sessions are revoked immediately | | XSS | Refresh tokens stored in httpOnly cookies — invisible to JavaScript | | CSRF | SameSite=strict on refresh cookie, SameSite=lax on access cookie |

Configuration Reference

interface AuthEngineConfig {
  accessToken: {
    secret: string;       // JWT signing secret (min 32 chars)
    expiresIn?: string;   // Default: "10m"
    algorithm?: string;   // Default: "HS256"
    issuer?: string;      // Optional JWT issuer claim
  };
  refreshToken: {
    expiresInMs?: number; // Default: 604800000 (7 days)
  };
  passwordHashing?: {
    saltRounds?: number;  // Default: 12
  };
}

Using the Core Directly

If you're not using Next.js or want to build your own adapter:

import { AuthEngine } from "auth-engine";

const engine = new AuthEngine({
  config: { /* ... */ },
  userRepository: myUserRepo,
  refreshTokenRepository: myTokenRepo,
});

// Use directly
const tokens = await engine.login("[email protected]", "password");
const user = await engine.verifyAccessToken(tokens.accessToken);
await engine.refresh(tokens.refreshToken);
await engine.logout(tokens.refreshToken);
await engine.revokeAllSessions(userId);

Error Handling

All errors are instances of AuthError with a consistent shape:

import { AuthError } from "auth-engine";

try {
  await engine.login(email, password);
} catch (error) {
  if (error instanceof AuthError) {
    console.log(error.code);    // "INVALID_CREDENTIALS"
    console.log(error.status);  // 401
    console.log(error.toJSON());
    // {
    //   success: false,
    //   error: { code: "INVALID_CREDENTIALS", message: "Invalid email or password.", status: 401 }
    // }
  }
}

Error codes: INVALID_CREDENTIALS, USER_NOT_FOUND, USER_ALREADY_EXISTS, ACCESS_TOKEN_EXPIRED, INVALID_ACCESS_TOKEN, INVALID_REFRESH_TOKEN, REFRESH_TOKEN_REUSED, SESSION_REVOKED

Important Notes

  • Single-device in v1: Login revokes all existing sessions. If a user logs in on a new device, their old sessions end. Multi-device support is coming in v2.
  • Rate limiting: Auth Engine does not include rate limiting. You should add it at the API layer for login and refresh endpoints.
  • HTTPS: Auth Engine sets secure: true on cookies in production. Make sure your deployment is behind HTTPS.
  • JWT secret: Use at least 32 random characters. Never commit it to source control.

Roadmap

v2 — Multi-device sessions, role-based route protection, audit event logging, device management

v3 — Redis session strategy, OAuth providers, risk detection, CSRF module, rate limiting

Contributing

Contributions are welcome. Please open an issue first to discuss what you'd like to change.

License

MIT