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

customer-registration

v0.0.113

Published

Medusa plugin that overrides store customer registration, enforces email/phone verification flags, and provides OTP management module.

Readme

Medusa Plugin: Customer Registration & OTP Verification

A comprehensive Medusa v2 plugin that provides OTP-based verification for email and phone functionality, with support for flexible registration identifiers, phone-only authentication, contact change flows, and account deletion.

Features

  • Unified OTP API: Single endpoints for sending and verifying OTPs
  • Token-based Verification: Secure JWT token system for OTP verification
  • Multiple Verification Types: Email verification and phone verification
  • Workflow-based Processing: Automatic handling of verification flags
  • Password Reset Support: Notification subscriber for Medusa's built-in password reset flow
  • Flexible Configuration: Per-purpose channel configuration (email/SMS)
  • Throttling & Rate Limiting: Built-in protection against OTP spam
  • Database Migrations: Automatic schema updates for verification columns
  • Account Deletion Request Flow: Two-step OTP flow to request and confirm account deletion with optional cancel flow
  • Phone-only Authentication: phonepass auth provider for registering and logging in with phone + password (no email required)
  • Authenticated Contact Change: Dedicated OTP-verified routes for updating phone or email — new value is embedded in the signed JWT, no metadata staging required

Quick Start

  1. Install the plugin:
npm install customer-registration
  1. Add to medusa-config.ts:
import { defineConfig } from "@medusajs/framework/utils"

export default defineConfig({
  plugins: [
    {
      resolve: "customer-registration",
      options: {
        storefrontUrl: "http://localhost:8000",

        registration: {
          identifier: "phone", // "email" | "phone" | "both"
          require_verification: true,
        },
        login: {
          identifier: "phone", // defaults to registration.identifier
        },

        password_reset: {
          template: "src/templates/emails/password-reset.html",
          subject: "Reset Your Password",
        },

        account_deletion_request: {
          template: "src/templates/emails/account-deletion-request.html",
          subject: "Confirm your account deletion request",
          scheduled_days: 7,
        },
        account_deletion_cancel: {
          template: "src/templates/emails/account-deletion-cancel.html",
          subject: "Confirm cancellation of account deletion",
        },

        email_verification: {
          channel: "email",
          subject: "Verify your email",
        },
        phone_verification: {
          channel: "sms",
        },
      },
    },
  ],
})
  1. Run migrations:
npx medusa db:migrate
  1. Use the API — see API Endpoints below.

📖 For complete documentation, see USAGE.md


Installation

Local Development

  1. Publish the plugin to local registry:
cd plugins/customer-registration
npx medusa plugin:publish
  1. Install in your Medusa application:
cd ../../test-medusa
npx medusa plugin:add customer-registration
  1. Register the plugin in medusa-config.ts (see Quick Start above).

  2. Start development mode (in plugin directory):

cd plugins/customer-registration
npx medusa plugin:develop
  1. Start your Medusa application:
cd ../../test-medusa
yarn dev

Configuration

registration.identifier

Controls which field is required at sign-up:

| Value | Behaviour | |---|---| | "email" (default) | Email required; standard Medusa registration | | "phone" | Phone required; no email needed; uses phonepass provider | | "both" | Both email and phone required |

login.identifier

Controls which auth providers are accepted at login. Defaults to registration.identifier.

| Value | Accepted login methods | |---|---| | "email" | POST /auth/customer/emailpass only | | "phone" | POST /auth/customer/phonepass only | | "both" | Both emailpass and phonepass accepted |

Full options reference

{
  resolve: "customer-registration",
  options: {
    storefrontUrl?: string,          // Base URL for password reset links

    registration?: {
      identifier?: "email" | "phone" | "both",  // default: "email"
      require_verification?: boolean,            // default: true
    },
    login?: {
      identifier?: "email" | "phone" | "both",  // default: registration.identifier
    },

    // OTP settings (apply to all types)
    otpLength?: number,              // default: 6
    otpCharset?: "numeric" | "alphanumeric",  // default: "numeric"
    otpExpiryMinutes?: number,       // default: 15
    maxAttempts?: number,            // default: 5

    email_verification?: {
      channel: string,               // e.g. "email"
      template?: string,
      subject?: string,
      resendThrottleSeconds?: number,
      autoSendOnRegistration?: boolean,
    },
    phone_verification?: {
      channel: string,               // e.g. "sms"
      template?: string,
      resendThrottleSeconds?: number,
    },

    password_reset?: {
      template: string,              // Required when using password reset
      subject?: string,
    },
    account_deletion_request?: {
      template: string,
      subject?: string,
      scheduled_days?: number,       // default: 7
    },
    account_deletion_cancel?: {
      template: string,
      subject?: string,
    },
  },
}

API Endpoints

Registration & Initial Verification

| Endpoint | Method | Auth | Description | |---|---|---|---| | /store/customers | POST | Bearer (pre-customer) | Create customer account | | /store/customers/otp/send | POST | — | Send OTP for initial email/phone verification | | /store/customers/otp/verify | POST | — | Verify OTP code; returns login token on success |

Send OTP

POST /store/customers/otp/send
{
  "customer_id": "cus_...",
  "type": "phone_verification"  # or "email_verification"
}
# Response: { "token": "...", "expires_at": "..." }

Verify OTP

POST /store/customers/otp/verify
{
  "token": "<token from send>",
  "code": "482917"
}
# Response: { "verified": true, "customer": {...}, "token": "<login jwt>", "needs_login": false }

Login

| Endpoint | Method | Description | |---|---|---| | /auth/customer/emailpass | POST | Login with email + password | | /auth/customer/phonepass | POST | Login with phone + password | | /auth/customer/emailpass/register | POST | Create emailpass auth identity | | /auth/customer/phonepass/register | POST | Create phonepass auth identity |


Contact Change (Phone or Email Update)

These are the dedicated authenticated routes for changing a customer's phone or email. PATCH /store/customers/me rejects phone and email fields — all contact changes must go through these endpoints.

| Endpoint | Method | Auth | Description | |---|---|---|---| | /store/customers/me/contact | POST | Customer JWT | Request contact change — validates new value, sends OTP, returns token | | /store/customers/me/contact/verify | POST | Customer JWT | Verify OTP and apply the change |

How it works

The new phone or email is embedded directly in the signed OTP JWT. No metadata is written to the customer record during the pending state. On successful verification:

  1. customer.phone / customer.email is updated
  2. phone_verified / email_verified is set to true
  3. provider_identity.entity_id is updated for phonepass / emailpass only when the changed field is the active login.identifier — so login continues to work with the new value

Step 1 — Request the change

curl -X POST http://localhost:9000/store/customers/me/contact \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $CUSTOMER_JWT" \
  -d '{ "phone": "+15559998888" }'

# Response
{ "token": "<otp_token>", "expires_at": "2026-03-12T10:15:00.000Z" }

Only one field at a time is accepted. Sending both phone and email returns a 400.

Step 2 — Verify the OTP

curl -X POST http://localhost:9000/store/customers/me/contact/verify \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $CUSTOMER_JWT" \
  -d '{
    "token": "<otp_token from step 1>",
    "code": "482917"
  }'

# Response
{
  "customer": { "id": "cus_...", "phone": "+15559998888", ... },
  "token": "<fresh_login_jwt>"
}

The response includes a fresh login JWT so the caller is immediately re-authenticated after the change.

Security notes

  • The OTP token is signed with the server's jwtSecret — the new value cannot be tampered with
  • The verify route checks that token.customer_id === auth_context.actor_id, preventing one customer from applying another's OTP
  • provider_identity.entity_id sync has a compensation function: if a later workflow step fails the entity_id is rolled back to the original value
  • If the current phone/email is null (first-time contact assignment), the flow works identically — the field is set from null to the new value and marked verified

What PATCH /store/customers/me does now

Non-contact fields (e.g. first_name, last_name) continue to work as before. Passing phone or email in the body returns:

{
  "type": "not_allowed",
  "message": "Phone changes require OTP verification. Use POST /store/customers/me/contact to request a change."
}

Profile Update

| Endpoint | Method | Auth | Description | |---|---|---|---| | /store/customers/me | PATCH | Customer JWT | Update non-contact profile fields (first_name, last_name, etc.) |


Password Reset

| Endpoint | Method | Description | |---|---|---| | /auth/customer/emailpass/reset-password | POST | Request password reset email | | /auth/customer/emailpass/update | POST | Complete password reset with token |

# Request reset
POST /auth/customer/emailpass/reset-password
{ "identifier": "[email protected]" }

# Complete reset
POST /auth/customer/emailpass/update
{ "token": "<reset_token>", "password": "NewPassword123!" }

Account Deletion

| Endpoint | Method | Auth | Description | |---|---|---|---| | /store/customers/account-deletion/request | POST | Customer JWT | Request deletion — sends OTP, returns token | | /store/customers/account-deletion/confirm | POST | — | Confirm deletion with OTP code | | /store/customers/account-deletion/cancel-request | POST | — | Request cancellation (lookup by email/phone) | | /store/customers/account-deletion/cancel-confirm | POST | — | Confirm cancellation with OTP code | | /admin/account-deletion-requests | GET | Admin JWT | List deletion requests |

# 1. Request deletion
curl -X POST http://localhost:9000/store/customers/account-deletion/request \
  -H "Authorization: Bearer $CUSTOMER_JWT" \
  -d '{"reason": "No longer need account"}'
# Response: { "token": "...", "expires_at": "..." }

# 2. Confirm deletion
curl -X POST http://localhost:9000/store/customers/account-deletion/confirm \
  -d '{"token": "...", "code": "123456"}'

# 3. Request cancel (no auth; email lookup)
curl -X POST http://localhost:9000/store/customers/account-deletion/cancel-request \
  -d '{"email": "[email protected]"}'

# 4. Confirm cancel
curl -X POST http://localhost:9000/store/customers/account-deletion/cancel-confirm \
  -d '{"token": "...", "code": "123456"}'

Phone-only Registration & Login (phonepass)

When registration.identifier = "phone", customers register and log in with phone + password only. No email address is required.

1. Register the provider in medusa-config.ts

import { Modules } from "@medusajs/framework/utils"

export default defineConfig({
  modules: [
    {
      resolve: "@medusajs/medusa/auth",
      options: {
        providers: [
          {
            resolve: "customer-registration/providers/phonepass",
            id: "phonepass",
          },
        ],
      },
    },
  ],
  plugins: [
    {
      resolve: "customer-registration",
      options: {
        registration: {
          identifier: "phone",
          require_verification: true,
        },
        login: {
          identifier: "phone",
        },
        phone_verification: {
          channel: "sms",
        },
      },
    },
  ],
})

2. Registration flow

# Step 1 — create phonepass auth identity
POST /auth/customer/phonepass/register
{ "phone": "+15551234567", "password": "SecretPass1!" }
# Response: { "token": "<pre-customer jwt>" }

# Step 2 — create customer record
POST /store/customers
Authorization: Bearer <token>
{ "phone": "+15551234567", "first_name": "Jane" }

# Step 3 — send phone OTP
POST /store/customers/otp/send
{ "customer_id": "cus_...", "type": "phone_verification" }
# Response: { "token": "<otp_token>", "expires_at": "..." }

# Step 4 — verify phone OTP (returns login token)
POST /store/customers/otp/verify
{ "token": "<otp_token>", "code": "482917" }
# Response: { "verified": true, "phone_verified": true, "token": "<login jwt>" }

3. Login flow

POST /auth/customer/phonepass
{ "phone": "+15551234567", "password": "SecretPass1!" }
# Response: { "token": "<jwt>" }

4. Update phone (after registration)

# Request change
POST /store/customers/me/contact
Authorization: Bearer <login jwt>
{ "phone": "+15559998888" }
# Response: { "token": "<otp_token>", "expires_at": "..." }

# Verify and apply
POST /store/customers/me/contact/verify
Authorization: Bearer <login jwt>
{ "token": "<otp_token>", "code": "739201" }
# Response: { "customer": { "phone": "+15559998888", ... }, "token": "<fresh jwt>" }

Modules

The plugin includes three modules:

  1. otp-verification — OTP generation, verification, and management
  2. customer-registration — Customer registration logic and route overrides
  3. account-deletion-request — Account deletion request lifecycle (pending → confirmed → completed/cancelled)

Workflows

| Workflow | Description | |---|---| | verify-email | Sets email_verified = true on the customer record | | verify-phone | Sets phone_verified = true on the customer record | | update-contact | Updates customer.phone / customer.email, sets verified flag, syncs provider_identity.entity_id when applicable | | send-otp | Resolves channel config, generates OTP, sends notification | | send-contact-change-otp | Generates OTP with new_value embedded in JWT, sends to the new contact address | | change-password | Updates customer password via the auth module |

Database Migrations

| Migration | Description | |---|---| | Migration20250120000000AddCustomerVerificationColumns | Adds email_verified and phone_verified columns to the customer table | | Migration20250118001000CreateOtpVerificationTable | Creates otp_verification table | | Migration20250221000000CreateAccountDeletionRequestTable | Creates account_deletion_request table | | Migration20250221000000AddAccountDeletionOtpPurposes | Extends otp_verification.purpose enum for account deletion flows |

npx medusa db:migrate

Password Reset (Helper Functions)

import {
  requestPasswordReset,
  completePasswordReset,
} from "customer-registration/helpers"

await requestPasswordReset(
  { email: "[email protected]" },
  { baseUrl: "https://store.example.com", publishableApiKey: "pk_..." }
)

await completePasswordReset(
  {
    email: "[email protected]",
    password: "NewPassword123!",
    token: "reset_token_from_email",
  },
  { baseUrl: "https://store.example.com", publishableApiKey: "pk_..." }
)

Requirements

  • Medusa v2.11.2 or higher
  • Node.js >= 20
  • Notification module configured with at least one provider (email/SMS)
  • Database migrations applied (npx medusa db:migrate)
  • phonepass provider registered in auth module (required when registration.identifier is "phone" or "both")

Documentation

  • USAGE.md — Complete usage guide with examples
  • README.md — This file (overview and quick start)

Development

# Build
npm run build

# Watch mode
npx medusa plugin:develop

License

MIT