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

@jadedm/nestjs-verify

v0.5.0

Published

Self-hosted Twilio Verify-style OTP for NestJS. Bring your own SMS provider and store.

Readme

@jadedm/nestjs-verify

Self-hosted Twilio-Verify-style OTP for NestJS. One POST starts a verification, another checks the code. Code generation, TTL, attempt caps, cooldowns, rate limits, and abuse heuristics live in the library. You bring the SMS provider and the stores.

pnpm add @jadedm/nestjs-verify
# + at least one provider and one store-adapter package
pnpm add @jadedm/nestjs-verify-twilio @jadedm/nestjs-verify-postgres

Minimal wiring (dev)

import { Module } from '@nestjs/common';
import {
  VerifyModule,
  MockSmsProvider,
  createMemoryStores,
} from '@jadedm/nestjs-verify';

@Module({
  imports: [
    VerifyModule.forRoot({
      sms: { provider: new MockSmsProvider() },
      stores: createMemoryStores(),
      code: { fixedCode: '123456' },   // dev only; warns at boot
    }),
  ],
})
export class AppModule {}

Production wiring

import { VerifyModule } from '@jadedm/nestjs-verify';
import { TwilioSmsProvider } from '@jadedm/nestjs-verify-twilio';
import { createPostgresStores } from '@jadedm/nestjs-verify-postgres';

VerifyModule.forRootAsync({
  useFactory: async () => ({
    sms: {
      provider: new TwilioSmsProvider({
        accountSid: process.env.TWILIO_ACCOUNT_SID!,
        authToken:  process.env.TWILIO_AUTH_TOKEN!,
        from:       process.env.TWILIO_FROM!,
      }),
    },
    stores: await createPostgresStores({ connectionString: process.env.DATABASE_URL! }),
  }),
});

That's it. Two routes are mounted automatically:

POST /verify/start   { "to": "+14155552671" }
  → 201 { "sid": "vr_...", "state": "pending", "channel": "sms", "expiresAt": "..." }

POST /verify/check   { "to": "+14155552671", "code": "123456" }
  → 201 { "sid": "vr_...", "state": "approved" | "pending" | "canceled", "attemptsRemaining": N }

Configuration

VerifyModule.forRootAsync({
  inject: [ConfigService],
  useFactory: (c) => ({
    sms: { provider: ..., fallbacks: [...] },
    stores: {
      verify, abuse,        // durable
      rateLimit, cooldown, phoneIndex,   // ephemeral
    },
    code:       { length: 6, ttlSeconds: 600, fixedCode: undefined },
    attempts:   { max: 5, cooldownSeconds: 30 },
    rateLimit:  { perPhone: { count: 5, windowSeconds: 3600 } },
    abuse:      { maxDistinctPhonesPerIp: 10, velocityWindowSeconds: 300 },
    messageTemplate: 'Your code is {{code}}',
    registerController: true,
    logging:    { verbose: false },
  }),
})

The five stores

The architectural shape of 0.3.0 onward. Every piece of state lives behind a store interface.

| Store | What it holds | Backed by | |---|---|---| | VerifyStore | Pending and terminal verification records, the source of truth | Postgres, Mongo | | AbuseStore | Send-attempt history for IP/phone velocity heuristics. Optional. | Postgres, Mongo | | RateLimitStore | Fixed-window counter per phone and per IP. Atomic. | Postgres, Mongo, Redis | | CooldownStore | Per-phone cooldown between sends. Returns ms remaining. | Postgres, Mongo, Redis | | PhoneIndexStore | Phone -> sid lookup so check() does not need the sid | Postgres, Mongo, Redis |

You can use a single backend for all five, or split durable vs ephemeral (Postgres for verify and abuse, Redis for the other three) for speed.

Peers

  • @nestjs/common, @nestjs/core: 9, 10, or 11
  • reflect-metadata, rxjs

No @nestjs/cache-manager peer dep. State is managed through the store interfaces.

Provider and store adapters

| Concern | Package | |---|---| | Twilio SMS | @jadedm/nestjs-verify-twilio | | Postgres (all 5 stores) | @jadedm/nestjs-verify-postgres | | Mongo (all 5 stores) | @jadedm/nestjs-verify-mongo | | Redis (rate limit, cooldown, phone index) | @jadedm/nestjs-verify-redis |

Bring your own: implement SmsProvider for a new SMS vendor or VerifyStore / AbuseStore for a different database. The interfaces are tiny and re-exported from this package.

Why

Twilio Verify costs about $0.05 per verification on top of SMS. At scale that adds up, and your OTP state lives inside Twilio's tenant. This library gives you the same surface area on your own infrastructure, with provider choice and pluggable storage.

Maturity and limitations

This library is in beta. It includes secure primitives but is not yet hardened for enterprise compliance environments. Read this section before adopting it.

What is in place

  • Crypto-random code generation via crypto.randomInt.
  • Constant-time code comparison via crypto.timingSafeEqual.
  • Salted SHA-256 storage of codes at rest. The code is never persisted in clear.
  • Atomic attempt counters using UPDATE ... RETURNING (Postgres) and aggregation pipeline updates (Mongo). Lockout on max attempts happens in a single round trip.
  • Per-phone and per-IP rate limiting with fixed window semantics.
  • Per-phone cooldown after each send.
  • Distinct-phones-per-IP velocity check, configurable window.
  • Pluggable provider strategy with a fallback chain.
  • TTL on verification records: native TTL index in Mongo, schema-managed expiry in Postgres.
  • Phone normalization to E.164.
  • Phone-number redaction in this library's own log lines.

Known gaps before 1.0

These are tracked for the 1.0 milestone. They are not present in 0.x.

  1. Atomic rate-limit counters. The current cache-manager implementation does a get followed by a set and can leak one or two extra requests through under concurrency. For high-throughput deployments, swap to @nestjs/throttler with a Redis adapter, or supply your own counter that uses INCR.
  2. DTO validation with class-validator. Input validation today is manual regex on the service. Decorator-based DTO validation is planned.
  3. OpenAPI annotations on the built-in controller.
  4. Integration tests against live Postgres and Mongo using testcontainers. Current test coverage exercises the in-memory store and the Twilio retry policy only.
  5. Delivery receipt handling. The library dispatches via the SMS provider but does not yet process delivery callbacks (Twilio DLR webhooks).

Not in scope for 1.0

These may be added later or ship as separate modules. Plan deployments accordingly.

  1. OpenTelemetry spans and Prometheus metrics. Likely to arrive as separate packages so consumers can opt in.
  2. Multi-tenant isolation. Rate-limit and cooldown state is keyed by phone alone today. Two tenants in one deployment share state for a phone number that exists in both. If you need per-tenant isolation, wrap the service in your own tenant-scoping layer or open an issue describing the shape you need.
  3. Tamper-evident audit log. The audit concern will ship as a separate module. Until then, you can subscribe to send attempts via the AbuseStore interface and persist whatever shape you need.
  4. Internationalized message templates. messageTemplate is a single string today.
  5. KMS-backed code hashing. SHA-256 with a random salt is the current primitive.

How to evaluate suitability for your project

Use the library when:

  • Your verification volume is moderate (single-digit to low thousands of verifications per minute).
  • You can tolerate fixed-window rate limiting at low single-digit accuracy at peak concurrency.
  • You do not yet need provider delivery receipt processing.
  • Compliance requirements do not yet require a tamper-evident audit log.

Defer adoption when:

  • You require strict atomicity guarantees on rate limits at high concurrency.
  • You require SOC 2 or PCI evidence trails out of the box.
  • You require multi-tenant isolation of OTP state today.

If you adopt it for a use case in the second list, expect to add the missing pieces yourself or wait for the matching milestone.

Consulting

If you need integration help, a custom provider or store adapter, or fractional CTO support shipping this into production, see manishj.com.

License

MIT. Manish Jadhav (@jadedm).