Frequently Asked Questions
Implement 2FA by first setting up a NestJS project, integrating Sinch for SMS, and then building the authentication flow with phone verification and OTP delivery. The Sinch SMS API is used for sending OTPs, while Prisma handles database interactions for secure storage of user data and OTPs. This guide provides step-by-step instructions for setting up the entire process.
Prisma is used as an ORM (Object-Relational Mapper) to simplify database operations. It provides a type-safe interface for interacting with the PostgreSQL database, managing database migrations, and ensuring data integrity. This makes it easier to manage user data, OTP records, and other information related to authentication securely.
NestJS provides a robust and modular architecture well-suited for building scalable server-side applications. Its TypeScript support enhances code quality and maintainability. The modular structure helps organize code into controllers, services, and modules, improving code reusability and maintainability.
Enable 2FA after a user has successfully registered and verified their phone number. This adds an extra layer of security to the login process, protecting against unauthorized access even if the user's password is compromised. It's usually presented as an optional feature for users to activate.
While this guide focuses on Sinch, you can adapt the principles to integrate other SMS API providers. You'll need to adjust the SinchService to interact with your chosen provider's API and ensure it's compatible with the rest of the architecture. Key adjustments would involve modifying the service to make requests to the new provider's API endpoints and handle their specific response format.
PostgreSQL serves as the persistent data store for user information, including hashed passwords, phone numbers, 2FA status, and OTP records. It is a robust and reliable database solution chosen for its stability and open-source nature.
Create a .env
file in the project root and add your Sinch Service Plan ID, API token, and optional Sinch phone number. The values for SINCH_SERVICE_PLAN_ID
and SINCH_API_TOKEN
are obtained from your Sinch account dashboard. The SINCH_PHONE_NUMBER
is the virtual number or shortcode you use to send SMS.
You need Node.js, npm or yarn, a running PostgreSQL server, a Sinch account with SMS API credentials, and a basic understanding of TypeScript, REST APIs, and async/await. The NestJS CLI should also be installed globally to manage the project effectively.
bcrypt is used to securely hash user passwords before they are stored in the database. This ensures that even if the database is compromised, the raw passwords are not exposed, making it significantly harder for attackers to gain access to user accounts.
Class-validator and class-transformer are used for validating and transforming request data using Data Transfer Objects (DTOs). This helps ensure that data conforms to expected formats and types, improving the security and robustness of the API endpoints.
JSON Web Tokens (JWTs) are used for managing stateless authentication sessions after a user successfully logs in. Once authenticated, a JWT is issued to the client, which can be used to access protected routes without needing to send credentials each time.
The architecture consists of a Client, NestJS API, Prisma Client, PostgreSQL Database and the Sinch SMS API. The client interacts with the NestJS API, which handles the logic and communication with other services, like the Sinch API for SMS and Prisma for database interactions.
Use the NestJS CLI to create a new project: nest new sinch-otp-nestjs-app
and navigate into the directory cd sinch-otp-nestjs-app
. This command sets up the basic structure of your NestJS project.
Last Updated: October 5, 2025
Build SMS OTP & 2FA with NestJS, Sinch, and Prisma
Secure your NestJS application with SMS-based one-time passwords (OTP) and two-factor authentication (2FA). This comprehensive guide shows you how to implement a complete authentication system using Sinch's SMS API and Prisma ORM – from user registration to OTP verification.
What you'll build:
Version Requirements:
Source: Sinch SDK Core npm package (npmjs.com/package/@sinch/sdk-core, verified October 2025)
Prerequisites:
OTP Security Best Practices:
Following industry security standards, this implementation includes:
Source: OWASP Authentication Cheat Sheet, NIST Digital Identity Guidelines (SP 800-63B)
Step 1: Set Up Your NestJS Project
Create a new NestJS project and install the required dependencies:
Package purposes:
Source: Sinch SDK Core documentation (developers.sinch.com/docs/sms, verified October 2025)
Step 2: Configure Prisma for Database Access
Initialize Prisma and define your database schema for users and OTP codes:
Update
prisma/schema.prisma
with your database models:This schema defines:
userId
Generate the Prisma client and run migrations:
Update your
.env
file with your database connection:Step 3: Set Up Sinch SMS Configuration
Configure your Sinch credentials in
.env
:Authentication Options:
Sinch supports two authentication methods in SDK v1.x:
projectId
,keyId
, andkeySecret
servicePlanId
andapiToken
Choose the authentication method that matches your Sinch account region. You'll find your credentials in the Sinch Dashboard.
Source: Sinch SDK Core authentication guide (developers.sinch.com/docs/sms/api-reference/authentication, verified October 2025)
Step 4: Create the Sinch SMS Service
Build a dedicated service to handle SMS sending through Sinch:
Key implementation details:
Source: Sinch SDK Core v1.x API reference (npmjs.com/package/@sinch/sdk-core, verified October 2025)
Create the Sinch module:
Step 5: Build the OTP Service
Create the core OTP service that generates, sends, and verifies one-time passwords:
OTP service features:
Create the OTP module:
Step 6: Implement User Authentication
Create the authentication service that handles user registration and login:
Create the DTOs for request validation:
Step 7: Build the Authentication Controller
Create REST API endpoints for your authentication flow:
Create the authentication module:
Step 8: Set Up Prisma Service
Create the Prisma service for database access:
Create the Prisma module:
Step 9: Update the Main Application Module
Wire everything together in your main application module:
Step 10: Test Your OTP & 2FA System
Start your application:
Test the complete authentication flow:
1. Register a new user
Expected response:
You'll receive an SMS with your 6-digit OTP code.
2. Verify your phone number with OTP
Expected response:
3. Login with verified credentials
Expected response:
4. Resend OTP if needed
Production Enhancements
Take your OTP system to production with these critical improvements:
1. Implement Rate Limiting
Prevent brute-force attacks and SMS abuse:
2. Add JWT Token Authentication
Replace simple user IDs with secure JWT tokens:
Update your login response to include JWT tokens for session management.
3. Implement OTP Cleanup Scheduler
Automatically remove expired OTPs:
4. Enhanced Error Handling
Add detailed error responses without exposing sensitive information:
5. Add Logging and Monitoring
Implement structured logging for debugging and auditing:
Configure Winston for production-grade logging with log rotation and remote transport.
6. Database Indexing
Optimize your database queries with indexes:
Apply the migration:
Security Best Practices
Protect your OTP system with these security measures:
1. Store Sinch credentials securely
Never commit
.env
files to version control. Use environment-specific configuration management (AWS Secrets Manager, HashiCorp Vault) in production.2. Implement HTTPS only
Force HTTPS in production to prevent man-in-the-middle attacks:
3. Validate phone numbers strictly
Use E.164 format validation to prevent invalid SMS destinations:
4. Add OTP attempt limits
Track failed verification attempts and lock accounts after 5 failed tries:
5. Monitor SMS delivery failures
Alert on high SMS failure rates to catch configuration issues early.
6. Use secure password hashing
The bcrypt implementation uses 10 salt rounds – consider increasing to 12 for higher security (with performance trade-offs).
Troubleshooting Common Issues
SMS not delivering
Symptoms: OTP codes don't arrive at the recipient's phone
Solutions:
.env
"Invalid or expired OTP" errors
Symptoms: Valid OTP codes are rejected
Solutions:
Database connection failures
Symptoms: Prisma queries fail with connection errors
Solutions:
DATABASE_URL
in.env
matches your PostgreSQL configurationpg_isready
Sinch authentication errors
Symptoms: "Missing Sinch credentials" or "Authentication failed"
Solutions:
SINCH_PROJECT_ID
,SINCH_KEY_ID
,SINCH_KEY_SECRET
SINCH_SERVICE_PLAN_ID
,SINCH_API_TOKEN
.env
valuesSource: Sinch SDK Core authentication troubleshooting (developers.sinch.com/docs/sms, verified October 2025)
Next Steps
Expand your authentication system with these enhancements:
1. Multi-channel verification
Add email OTP as an alternative to SMS for users without phone access.
2. Backup codes
Generate one-time backup codes during registration for account recovery.
3. Push notification OTP
Use push notifications instead of SMS for users with your mobile app installed.
4. Biometric authentication
Integrate WebAuthn for fingerprint/face recognition on supported devices.
5. Account recovery flow
Build secure password reset using OTP verification.
6. Admin dashboard
Create monitoring interface for OTP delivery rates and user verification status.
7. Multi-factor authentication
Combine OTP with other factors (security questions, email verification) for higher security requirements.
Frequently Asked Questions
How long should SMS OTP codes remain valid?
OTP codes should expire within 5–10 minutes following OWASP security guidelines and NIST Digital Identity Guidelines (SP 800-63B). This tutorial implements a configurable 10-minute expiration window, which balances security (preventing extended attack windows) with user experience (allowing time for SMS delivery delays). You can adjust the
OTP_EXPIRY_MINUTES
constant in theOtpService
to meet your security requirements.What's the difference between OTP and 2FA?
OTP (one-time password) is a temporary code used for single authentication events, while 2FA (two-factor authentication) combines multiple verification methods – typically "something you know" (password) and "something you have" (phone for SMS OTP). This tutorial implements both: OTP codes for phone number verification during registration, and 2FA by requiring both password authentication and phone verification for login access.
How do I prevent SMS OTP brute-force attacks?
Implement multiple security layers: rate limiting (this tutorial uses @nestjs/throttler to limit requests to 3 per minute), attempt tracking (store failed verification counts in your Otp model), account lockout after 5 failed attempts, and exponential backoff for repeated OTP requests. Additionally, use single-use OTP codes (marked as
verified: true
after successful validation) to prevent replay attacks.Which Sinch authentication method should I use?
Sinch SDK v1.x supports two authentication methods: OAuth2 (using projectId, keyId, and keySecret) works for US and EU regions, while API Token authentication (using servicePlanId and apiToken) is required for Brazil, Canada, Australia, and other regions. Check your Sinch Dashboard to determine your account region and use the corresponding credentials. This tutorial's
SinchService
automatically detects and uses the appropriate method based on your environment variables.Can I use Sinch SMS API for international phone numbers?
Yes, Sinch supports SMS delivery to over 190 countries with E.164 phone number format (+country_code + number). However, SMS pricing, delivery rates, and regulatory requirements vary by country. Always verify phone numbers using the E.164 standard, check Sinch pricing for your target countries in the Sinch Dashboard, and consider implementing country-specific validation rules for phone number formats.
How much does SMS OTP cost with Sinch?
Sinch SMS pricing varies by destination country and volume. For example, US SMS typically costs $0.0075–$0.01 per message, while international rates range from $0.005 to $0.50+ per SMS depending on the country. Review the Sinch SMS pricing page for current rates, and implement cost controls like daily sending limits and abuse detection to prevent unexpected charges from malicious usage.
Should I store OTP codes in plain text or hashed?
This tutorial stores OTP codes in plain text because they're temporary (5–10 minute lifespan), single-use (marked as verified after use), and automatically cleaned up after expiration. However, for applications with stricter security requirements, consider hashing OTP codes using bcrypt before database storage and comparing hashed values during verification. This adds processing overhead but prevents database breach exposure of active codes.
How do I test SMS OTP locally without sending real messages?
During development, implement a test mode that logs OTP codes to console instead of sending SMS. Add a
NODE_ENV
check inSinchService.sendSMS()
: ifNODE_ENV === 'development'
, log the OTP code and skip the Sinch API call. Alternatively, use Sinch's test credentials to send SMS to verified test numbers, or implement a mock SMS service that stores codes in memory for automated testing.What happens if SMS delivery fails?
The
SinchService.sendSMS()
method includes error handling that catches Sinch API failures and throws an error. Implement these production enhancements: retry logic with exponential backoff for transient failures, webhook callbacks from Sinch to track delivery status, fallback to email OTP if SMS fails after 3 attempts, and user notifications when SMS delivery experiences delays or failures.How do I implement OTP for password reset flows?
Use the same
OtpService
infrastructure with a modified flow: create a "forgot password" endpoint that generates and sends an OTP to the user's verified phone number, verify the OTP code through a password reset endpoint, issue a temporary password reset token after successful OTP verification, and allow password updates only with valid reset tokens. Add a separateOtpType
enum field to distinguish verification OTPs from password reset OTPs.Conclusion
You've built a production-ready SMS OTP and 2FA system using NestJS, Sinch, and Prisma. Your implementation includes secure password hashing, phone number verification, OTP generation with expiration, and complete error handling.
What you accomplished:
Key takeaways:
Related tutorials:
Start securing your NestJS applications with SMS-based OTP authentication today. Your users gain enhanced account protection, and your application meets modern security standards.
Resources: