{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreifpqwbx4hilh65vme3rdozzwud4cc7rlmn664touteakhlwjymxe4",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mokql33wjjh2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreig3evutjglbdhcbjojmkbqvcealthlo4ex4gyyt7ddlulgkglmcp4"
    },
    "mimeType": "image/webp",
    "size": 188936
  },
  "path": "/tomdevbrown/how-to-make-production-ready-otp-handling-system-1g5e",
  "publishedAt": "2026-06-18T11:24:19.000Z",
  "site": "https://dev.to",
  "tags": [
    "javascript",
    "node",
    "security",
    "tutorial"
  ],
  "textContent": "Handling an OTP (One-Time Password) flow requires a clean sequence so you don't run into race conditions, like a user trying to verify a token before it's securely saved in your state or database.\n\nHere is how you structure a production-ready OTP management lifecycle using `auth-verify`.\n\n##  Installation\n\n\n    npm install auth-verify\n\n\n##  Initialize the library:\n\n###  Step 1.\n\nFirst, bring in the library and configure the token storage. For development, memory works fine, but use redis or a database token store in production so your OTPs survive server restarts.\n\n\n\n    const AuthVerify = require(\"auth-verify\");\n    const auth = new AuthVerify({\n      storeTokens: \"memory\", // 'redis' is highly recommended for production\n      expiresIn: \"5m\"        // OTP automatically expires after 5 minutes\n    });\n\n\n##  Configure your transport channel:\n\n###  Step 2.\n\nSet up how the OTP actually gets to the user. You need to provide your service credentials (like SMTP for emails or API keys for SMS).\n\n\n\n    auth.otp.sender({\n      via: 'email',\n      sender: 'app@yourdomain.com',\n      pass: process.env.EMAIL_APP_PASSWORD,\n      service: 'gmail' // or custom SMTP settings\n    });\n\n\n##  Generate and send the OTP:\n\n###  Step 3.\n\nTrigger this inside your `login/registration` route. The library automatically generates a secure crypto-random numeric code, maps it to the identifier (email/phone), and sends it out.\n\n\n\n    app.post(\"/api/auth/request-otp\", async (req, res) => {\n      const { email } = req.body;\n\n      try {\n        const success = await auth.otp.send(email);\n        if (!success) return res.status(500).json({ error: \"Failed to send OTP\" });\n\n        res.json({ message: \"OTP sent successfully!\" });\n      } catch (err) {\n        res.status(500).json({ error: err.message });\n      }\n    });\n\n\n##  Verify the user's input:\n\n###  Step 4.\n\nWhen the user submits the code from their screen, pass their identifier and the code to `.verify()`. The library automatically checks if the code matches, handles the expiration window, and destroys the OTP on success to prevent reuse.\n\n\n\n    app.post(\"/api/auth/verify-otp\", async (req, res) => {\n      const { email, code } = req.body;\n\n      try {\n        const isValid = await auth.otp.verify(email, code);\n\n        if (!isValid) {\n          return res.status(400).json({ error: \"Invalid or expired OTP\" });\n        }\n\n        // OTP is valid! Mint your JWT or log the user in here\n        res.json({ success: true, message: \"Authenticated!\" });\n      } catch (err) {\n        res.status(500).json({ error: err.message });\n      }\n    });\n\n\n##  💡 Production Gotchas to Keep in Mind\n\n**Rate Limiting:** `auth-verify` handles the generation and verification, but it won't stop a malicious actor from hitting your /request-otp endpoint 10,000 times to blow up your email/SMS billing. Always wrap your OTP routes in a rate-limiter middleware like express-rate-limit.\n\n**Replay Attacks:** The library automatically handles deleting the OTP token storage space upon a successful validation so that the exact same code cannot be used twice.",
  "title": "How to make production ready OTP handling system"
}