Secure OTP Generation & Management in a Modern Node.js Stack
In today's security landscape, reliable user verification mechanisms such as OTP-based email validation are not just an add-on but a necessity. This newsletter outlines a robust, production-ready approach to OTP generation using Node.js's native crypto module, scheduling cleanup of expired OTPs via cron jobs, and integrating the flow within an email verification use case. The methodology leverages TypeScript for type safety and PostgreSQL for data integrity, providing a framework that suits the needs of decision-makers and senior engineers. Project Setup: Laying the Foundation with TypeScript Start by initializing your Node.js project with TypeScript. This guarantees strict typing and scalable code maintenance essential in enterprise environments. mkdir my-otp-project cd my-otp-project npm init -y npm install typescript --save-dev npx tsc --init This configuration sets the stage for a robust codebase. Adding TypeScript early helps prevent runtime errors and improves overall developer efficiency. Installing Key Dependencies To implement our OTP and scheduling logic, install the following libraries: crypto: Node's built-in module for secure random number generation. node-cron: For scheduling recurring tasks. pg: PostgreSQL client for database operations. npm i crypto npm i node-cron npm i pg npm i -D @types/node-cron @types/pg In addition, configure your environment variables by creating an .env file with database connection parameters: DB_USER= DB_PASSWORD= DB_HOST= DB_PORT= DB_NAME= Database Configuration: Establishing a Secure Connection A stable connection to PostgreSQL is critical. Checkout my another blog on Database connection with PostgreSQL. Advanced Integration: Connecting PostgreSQL with Node.js in a TypeScript Ecosystem | by Yug Jadvani | Mar, 2025 | JavaScript in Plain English In today’s hyper-competitive digital landscape, robust and efficient data architectures are not just a technical requirement they’re a… javascript.plainenglish.io Schema Design: Storing OTP Codes For scalability and data integrity, create a dedicated table to store OTP codes along with their expiration metadata. Below is an example PostgreSQL schema: CREATE TABLE otp_codes ( id SERIAL PRIMARY KEY, user_id INT NOT NULL, otp VARCHAR(6) NOT NULL, otp_expiry TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); This schema enforces referential integrity and ensures that OTP codes are linked to valid users. OTP Generation Using Crypto A secure OTP generation function is the heart of the process. The function below uses Node.js's crypto module to ensure a cryptographically secure random number generation. Notice the inclusion of options to customize the character set this gives you flexibility based on your security policies. // otp-generator.ts import * as crypto from 'crypto'; const digits = '0123456789'; const lowerCaseAlphabets = 'abcdefghijklmnopqrstuvwxyz'; const upperCaseAlphabets = lowerCaseAlphabets.toUpperCase(); const specialChars = '#!&@'; export interface GenerateOptions { digits?: boolean; // Include digits lowerCaseAlphabets?: boolean; // Include lowercase alphabets upperCaseAlphabets?: boolean; // Include uppercase alphabets specialChars?: boolean; // Include special characters } /** * Generates an OTP or password string based on length and provided options. * @param length - Desired length of the OTP. Defaults to 10. * @param options - Customization options for character selection. * @returns A secure, randomly generated OTP. */ export function otpGenerator(length: number = 10, options: GenerateOptions = {}): string { const { digits: includeDigits = true, lowerCaseAlphabets: includeLowerCase = true, upperCaseAlphabets: includeUpperCase = true, specialChars: includeSpecialChars = true, } = options; const allowedChars = (includeDigits ? digits : '') + (includeLowerCase ? lowerCaseAlphabets : '') + (includeUpperCase ? upperCaseAlphabets : '') + (includeSpecialChars ? specialChars : ''); if (!allowedChars) { throw new Error('No characters available to generate OTP. Please adjust the options.'); } let password = ''; while (password.length < length) { const charIndex = crypto.randomInt(0, allowedChars.length); // Prevent OTP from starting with '0' if digits are included if (password.length === 0 && includeDigits && allowedChars[charIndex] === '0') { continue; } password += allowedChars[charIndex]; } return password; } /** * Utility function to add a specific number of minutes to a date. * @param date - The base date. * @param minutes - Minutes to add. * @returns A

In today's security landscape, reliable user verification mechanisms such as OTP-based email validation are not just an add-on but a necessity. This newsletter outlines a robust, production-ready approach to OTP generation using Node.js's native crypto module, scheduling cleanup of expired OTPs via cron jobs, and integrating the flow within an email verification use case. The methodology leverages TypeScript for type safety and PostgreSQL for data integrity, providing a framework that suits the needs of decision-makers and senior engineers.
Project Setup: Laying the Foundation with TypeScript
Start by initializing your Node.js project with TypeScript. This guarantees strict typing and scalable code maintenance essential in enterprise environments.
mkdir my-otp-project
cd my-otp-project
npm init -y
npm install typescript --save-dev
npx tsc --init
This configuration sets the stage for a robust codebase. Adding TypeScript early helps prevent runtime errors and improves overall developer efficiency.
Installing Key Dependencies
To implement our OTP and scheduling logic, install the following libraries:
- crypto: Node's built-in module for secure random number generation.
- node-cron: For scheduling recurring tasks.
- pg: PostgreSQL client for database operations.
npm i crypto
npm i node-cron
npm i pg
npm i -D @types/node-cron @types/pg
In addition, configure your environment variables by creating an .env
file with database connection parameters:
DB_USER=
DB_PASSWORD=
DB_HOST=
DB_PORT=
DB_NAME=
Database Configuration: Establishing a Secure Connection
A stable connection to PostgreSQL is critical. Checkout my another blog on Database connection with PostgreSQL.
Schema Design: Storing OTP Codes
For scalability and data integrity, create a dedicated table to store OTP codes along with their expiration metadata. Below is an example PostgreSQL schema:
CREATE TABLE otp_codes (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
otp VARCHAR(6) NOT NULL,
otp_expiry TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
This schema enforces referential integrity and ensures that OTP codes are linked to valid users.
OTP Generation Using Crypto
A secure OTP generation function is the heart of the process. The function below uses Node.js's crypto module to ensure a cryptographically secure random number generation. Notice the inclusion of options to customize the character set this gives you flexibility based on your security policies.
// otp-generator.ts
import * as crypto from 'crypto';
const digits = '0123456789';
const lowerCaseAlphabets = 'abcdefghijklmnopqrstuvwxyz';
const upperCaseAlphabets = lowerCaseAlphabets.toUpperCase();
const specialChars = '#!&@';
export interface GenerateOptions {
digits?: boolean; // Include digits
lowerCaseAlphabets?: boolean; // Include lowercase alphabets
upperCaseAlphabets?: boolean; // Include uppercase alphabets
specialChars?: boolean; // Include special characters
}
/**
* Generates an OTP or password string based on length and provided options.
* @param length - Desired length of the OTP. Defaults to 10.
* @param options - Customization options for character selection.
* @returns A secure, randomly generated OTP.
*/
export function otpGenerator(length: number = 10, options: GenerateOptions = {}): string {
const {
digits: includeDigits = true,
lowerCaseAlphabets: includeLowerCase = true,
upperCaseAlphabets: includeUpperCase = true,
specialChars: includeSpecialChars = true,
} = options;
const allowedChars =
(includeDigits ? digits : '') +
(includeLowerCase ? lowerCaseAlphabets : '') +
(includeUpperCase ? upperCaseAlphabets : '') +
(includeSpecialChars ? specialChars : '');
if (!allowedChars) {
throw new Error('No characters available to generate OTP. Please adjust the options.');
}
let password = '';
while (password.length < length) {
const charIndex = crypto.randomInt(0, allowedChars.length);
// Prevent OTP from starting with '0' if digits are included
if (password.length === 0 && includeDigits && allowedChars[charIndex] === '0') {
continue;
}
password += allowedChars[charIndex];
}
return password;
}
/**
* Utility function to add a specific number of minutes to a date.
* @param date - The base date.
* @param minutes - Minutes to add.
* @returns A new Date instance with the added minutes.
*/
export function addMinutesToDate(date: Date, minutes: number): Date {
return new Date(date.getTime() + minutes * 60000);
}
This modular approach allows for easy testing and potential reuse in other parts of your system.
Integrating OTP with Email Verification
The next piece of the puzzle is the integration into an email verification workflow. The function below verifies a user's email by:
- Validating request data.
- Checking if the user is already verified.
- Generating an OTP.
- Storing the OTP with an expiry timestamp.
- Sending the OTP via email.
// verifyEmail.ts
import { Request, Response } from 'express';
import pool from './DB';
import { otpGenerator, addMinutesToDate } from './otp-generator';
// Example options for OTP generation tailored to security requirements
const options = {
digits: true,
lowerCaseAlphabets: true,
upperCaseAlphabets: false,
specialChars: true,
};
export const verifyEmail = async (req: Request, res: Response): Promise => {
try {
// Extract email from request payload
const { email } = req.body;
// Validate required fields (assume validateRequiredFields is implemented)
const validation = validateRequiredFields({ email });
if (!validation.isValid) {
sendResponse(res, 400, {}, validation.error || 'All fields are required');
return;
}
// Check if user exists and retrieve user data (assume checkUserExists is implemented)
const { userData } = await checkUserExists(email);
if (userData.is_verified) {
sendResponse(res, 400, {}, 'User is already verified');
return;
}
// Generate a 6-digit OTP with the predefined options
const otp = otpGenerator(6, options);
// Set OTP expiration (e.g., 10 minutes from now)
const otpExpiry = addMinutesToDate(new Date(), 10);
// Persist the OTP in the database
await pool.query(
'INSERT INTO otp_codes (user_id, otp, otp_expiry) VALUES ($1, $2, $3)',
[userData.id, otp, otpExpiry]
);
// Dispatch verification email (assume sendEmailVerificationEmail is implemented)
await sendEmailVerificationEmail({
name: `${userData.firstname} ${userData.lastname}`,
email: userData.email,
}, otp);
sendResponse(res, 200, {}, 'OTP sent successfully for email verification');
} catch (error) {
handleError(res, error, 'An error occurred during email verification');
}
};
Comments within the code highlight critical checkpoints, such as data validation and OTP expiry logic, ensuring that the system adheres to security best practices.
Automated Cleanup with Cron Jobs
As OTPs have a short validity period, it is crucial to regularly purge expired entries. The following cron job runs at the top of every hour, ensuring your database remains free of obsolete OTP data:
// In your DB connection file or a dedicated scheduler module
import cron from 'node-cron';
cron.schedule('0 * * * *', async () => {
try {
const result = await pool.query('DELETE FROM otp_codes WHERE otp_expiry < NOW()');
console.log(`Cleaned up ${result.rowCount} expired OTP(s) at ${new Date()}`);
} catch (err) {
console.error('Error cleaning up expired OTPs:', err);
}
});
This scheduled task not only helps maintain database performance but also reinforces data hygiene a crucial aspect for compliance and security in enterprise systems.
Conclusion
By integrating secure OTP generation using the crypto module, leveraging TypeScript for reliability, and scheduling periodic cleanups with node-cron, this architecture stands as a model for scalable, secure user verification systems. For both C-suite executives and senior developers, the combination of modern technology stacks and well-thought-out modular design offers an effective solution to meet today's security and performance demands.
Implementing these patterns in your organization can enhance security, improve user experience, and streamline system maintenance critical factors that drive success in high-performing companies.
This newsletter provides a comprehensive walkthrough that not only outlines best practices but also equips your technical teams with a proven blueprint for secure OTP management in a production environment.
Feel free to share your thoughts and ask any questions in the comments below!