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

@deeblr/auth-adapter-kit

v0.4.0

Published

The Universal Adapter System for Deeblr Auth: adapter registry, factory, manager, capability detection, extended repository interfaces, and a reusable conformance test suite every adapter can be validated against.

Readme

@deeblr/auth-adapter-kit

The Universal Adapter System for Deeblr Auth: registry, factory, manager, capability detection, extended repository interfaces, and a reusable conformance test suite. This is the infrastructure that makes "any database, any storage engine, without touching Deeblr Auth" actually true.

Install

npm install @deeblr/auth-adapter-kit

The contract you're implementing

Deeblr Auth's core dependency-inversion boundary hasn't changed since it was written: DatabaseAdapter<TUser> from @deeblr/auth-types — five required methods (createUser, getUserById, getUserByEmail, updateUser, deleteUser) plus optional session methods (createSession, getSession, updateSession, deleteSession, deleteSessionsForUser). This package doesn't replace that contract — it's the infrastructure around it.

How adapters work

Your adapter class
  implements DatabaseAdapter<TUser>          <- required, from @deeblr/auth-types
  implements UserRepository<TUser>?          <- optional: search/soft-delete/batch
  implements TransactionCapable?             <- optional: transaction()
  implements CapabilityAware?                <- optional: declare non-inferable traits
        |
        v
  detectCapabilities(adapter)                <- infers + reads explicit declarations
        |
        v
  AdapterManager                             <- owns lifecycle, guards feature calls
        |
        v
  new DeeblrAuthCore({ adapter })            <- works exactly as it always has

An adapter that implements only the five required methods works fully — every optional interface is additive. Nothing in @deeblr/auth-core, @deeblr/auth-session, or @deeblr/auth-jwt was changed to build this system; they still only ever call the base DatabaseAdapter contract they always have.

Building a custom adapter

import type { DatabaseAdapter, CreateUserInput, UpdateUserInput, BaseUser } from "@deeblr/auth-types";
import type { CapabilityAware, TransactionCapable, UserRepository } from "@deeblr/auth-adapter-kit";

class MyAdapter<TUser extends BaseUser = BaseUser>
  implements DatabaseAdapter<TUser>, UserRepository<TUser>, TransactionCapable, CapabilityAware
{
  readonly name = "my-adapter";

  // Declare anything not inferable from method presence.
  readonly capabilities = { supportsJson: true, supportsTTL: false };

  async createUser(input: CreateUserInput): Promise<TUser> { /* ... */ }
  async getUserById(id: string): Promise<TUser | null> { /* ... */ }
  async getUserByEmail(email: string): Promise<TUser | null> { /* ... */ }
  async updateUser(id: string, input: UpdateUserInput<TUser>): Promise<TUser> { /* ... */ }
  async deleteUser(id: string): Promise<void> { /* ... */ }

  // Optional — implement only what your store can reasonably do:
  async searchUsers(query: string) { /* ... */ }
  async transaction<T>(fn: (tx: unknown) => Promise<T>): Promise<T> { /* ... */ }
}

Then validate it and see what it reports:

import { validateAdapter, detectCapabilities } from "@deeblr/auth-adapter-kit";

const adapter = new MyAdapter();
validateAdapter(adapter); // throws AdapterValidationError if the base contract isn't satisfied
console.log(detectCapabilities(adapter));
// { supportsTransactions: true, supportsSearch: true, supportsJson: true, ... }

Testing your adapter — the conformance suite

Every adapter should pass the SAME tests. Import the suite, point it at a factory that builds a fresh instance, and run it:

// my-adapter/tests/conformance.test.mjs
import { runAdapterConformanceTests } from "@deeblr/auth-adapter-kit";
import { MyAdapter } from "../dist/index.js";

runAdapterConformanceTests({
  name: "MyAdapter",
  createAdapter: () => new MyAdapter(),
  cleanup: async (adapter) => { await adapter.disconnect?.(); },
});
node --test tests/conformance.test.mjs

The base CRUD tests (create/get/update/delete, case-insensitive email lookup) always run — every adapter must pass them. Everything else (sessions, transactions, search, soft-delete, batch) is capability-gated: the suite checks detectCapabilities() first and skips (not fails) a test for anything your adapter doesn't declare. A minimal adapter implementing only the five required methods shows a clean run of passes and honest skips — never a false failure.

Registering and discovering adapters

import { defaultAdapterRegistry, defineAdapterFactory } from "@deeblr/auth-adapter-kit";

// In your adapter package:
export default defineAdapterFactory((config) => new MyAdapter(config));
defaultAdapterRegistry.register("my-adapter", MyAdapterFactory);

// In application bootstrap code, resolving by name (e.g. from an env var):
const factory = defaultAdapterRegistry.get(process.env.DATABASE_ADAPTER);
const adapter = await factory({ connectionString: process.env.DATABASE_URL });

AdapterManager

Wraps a live instance: validates it, tracks connect()/disconnect() lifecycle state, exposes its capabilities, and provides a capability-guarded transaction():

import { AdapterManager } from "@deeblr/auth-adapter-kit";

const manager = new AdapterManager(myAdapter);
await manager.connect();

if (manager.hasCapability("supportsTransactions")) {
  await manager.transaction(async (tx) => { /* ... */ });
}

await manager.disconnect();

Reference adapters

  • @deeblr/auth-adapter-memory — real in-process adapter (search, soft-delete, batch, transactions all genuinely implemented). Good for dev/tests.
  • @deeblr/auth-adapter-sqlite — real, persistent adapter using Node's built-in node:sqlite. Zero external dependencies.

Implementing other databases

The contract is small on purpose — five required methods. Here's what each looks like against real ORM/driver APIs (these code samples use each library's actual, correct API; they're a starting point, not a shipped, tested package the way the two reference adapters above are — build and run your own conformance suite against them, per the section above).

Prisma

class PrismaAdapter implements DatabaseAdapter {
  readonly name = "prisma-adapter";
  constructor(private prisma: PrismaClient) {}

  async createUser(input: CreateUserInput) {
    return this.prisma.user.create({ data: { email: input.email, passwordHash: input.passwordHash, emailVerified: input.emailVerified ?? false } });
  }
  async getUserById(id: string) {
    return this.prisma.user.findUnique({ where: { id } });
  }
  async getUserByEmail(email: string) {
    return this.prisma.user.findFirst({ where: { email: { equals: email, mode: "insensitive" } } });
  }
  async updateUser(id: string, input: UpdateUserInput) {
    return this.prisma.user.update({ where: { id }, data: input });
  }
  async deleteUser(id: string) {
    await this.prisma.user.delete({ where: { id } });
  }
  async transaction<T>(fn: (tx: Prisma.TransactionClient) => Promise<T>) {
    return this.prisma.$transaction(fn);
  }
}

Drizzle ORM

class DrizzleAdapter implements DatabaseAdapter {
  readonly name = "drizzle-adapter";
  constructor(private db: NodePgDatabase) {}

  async createUser(input: CreateUserInput) {
    const [row] = await this.db.insert(users).values({
      email: input.email, passwordHash: input.passwordHash, emailVerified: input.emailVerified ?? false,
    }).returning();
    return row;
  }
  async getUserById(id: string) {
    const [row] = await this.db.select().from(users).where(eq(users.id, id));
    return row ?? null;
  }
  async getUserByEmail(email: string) {
    const [row] = await this.db.select().from(users).where(sql`lower(${users.email}) = lower(${email})`);
    return row ?? null;
  }
  async updateUser(id: string, input: UpdateUserInput) {
    const [row] = await this.db.update(users).set(input).where(eq(users.id, id)).returning();
    return row;
  }
  async deleteUser(id: string) {
    await this.db.delete(users).where(eq(users.id, id));
  }
  async transaction<T>(fn: (tx: NodePgDatabase) => Promise<T>) {
    return this.db.transaction(fn);
  }
}

TypeORM

class TypeOrmAdapter implements DatabaseAdapter {
  readonly name = "typeorm-adapter";
  constructor(private repo: Repository<UserEntity>, private dataSource: DataSource) {}

  async createUser(input: CreateUserInput) {
    return this.repo.save(this.repo.create(input));
  }
  async getUserById(id: string) {
    return this.repo.findOneBy({ id });
  }
  async getUserByEmail(email: string) {
    return this.repo.createQueryBuilder("u").where("lower(u.email) = lower(:email)", { email }).getOne();
  }
  async updateUser(id: string, input: UpdateUserInput) {
    await this.repo.update(id, input);
    return this.repo.findOneByOrFail({ id });
  }
  async deleteUser(id: string) {
    await this.repo.delete(id);
  }
  async transaction<T>(fn: (tx: EntityManager) => Promise<T>) {
    return this.dataSource.transaction(fn);
  }
}

Mongoose

class MongooseAdapter implements DatabaseAdapter {
  readonly name = "mongoose-adapter";
  constructor(private UserModel: Model<UserDocument>) {}

  async createUser(input: CreateUserInput) {
    const doc = await this.UserModel.create(input);
    return this.toUser(doc);
  }
  async getUserById(id: string) {
    const doc = await this.UserModel.findById(id);
    return doc ? this.toUser(doc) : null;
  }
  async getUserByEmail(email: string) {
    const doc = await this.UserModel.findOne({ email: new RegExp(`^${email}$`, "i") });
    return doc ? this.toUser(doc) : null;
  }
  async updateUser(id: string, input: UpdateUserInput) {
    const doc = await this.UserModel.findByIdAndUpdate(id, input, { new: true });
    return this.toUser(doc);
  }
  async deleteUser(id: string) {
    await this.UserModel.findByIdAndDelete(id);
  }
  private toUser(doc: UserDocument): BaseUser {
    return { id: doc._id.toString(), email: doc.email, passwordHash: doc.passwordHash, emailVerified: doc.emailVerified, createdAt: doc.createdAt.toISOString(), updatedAt: doc.updatedAt.toISOString() };
  }
}

MongoDB native driver

class MongoDbAdapter implements DatabaseAdapter {
  readonly name = "mongodb-adapter";
  constructor(private collection: Collection<UserDoc>) {}

  async createUser(input: CreateUserInput) {
    const now = new Date().toISOString();
    const doc = { _id: new ObjectId(), ...input, emailVerified: input.emailVerified ?? false, createdAt: now, updatedAt: now };
    await this.collection.insertOne(doc);
    return this.toUser(doc);
  }
  async getUserById(id: string) {
    const doc = await this.collection.findOne({ _id: new ObjectId(id) });
    return doc ? this.toUser(doc) : null;
  }
  async getUserByEmail(email: string) {
    const doc = await this.collection.findOne({ email: { $regex: `^${email}$`, $options: "i" } });
    return doc ? this.toUser(doc) : null;
  }
  // updateUser / deleteUser follow the same findOneAndUpdate / deleteOne pattern.
}

PostgreSQL / MySQL / MariaDB / SQL Server (raw driver, e.g. pg, mysql2, mssql)

The shape is identical across these — parameterized SQL through whichever driver's query method, with the driver's own placeholder syntax ($1 / ? / @p1):

class PostgresAdapter implements DatabaseAdapter {
  readonly name = "postgres-adapter";
  constructor(private pool: Pool) {}

  async createUser(input: CreateUserInput) {
    const { rows } = await this.pool.query(
      `INSERT INTO users (email, password_hash, email_verified) VALUES ($1, $2, $3) RETURNING *`,
      [input.email, input.passwordHash ?? null, input.emailVerified ?? false],
    );
    return this.toUser(rows[0]);
  }
  async getUserByEmail(email: string) {
    const { rows } = await this.pool.query(`SELECT * FROM users WHERE lower(email) = lower($1)`, [email]);
    return rows[0] ? this.toUser(rows[0]) : null;
  }
  async transaction<T>(fn: (client: PoolClient) => Promise<T>) {
    const client = await this.pool.connect();
    try {
      await client.query("BEGIN");
      const result = await fn(client);
      await client.query("COMMIT");
      return result;
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  }
}

@deeblr/auth-adapter-sqlite (in this monorepo) is a complete, tested, working example of exactly this pattern for SQLite specifically — read its source for a full reference implementation, not just a sketch.

License

MIT