Overview
Opinix Trade uses a secure phone-based authentication system powered by Twilio SMS OTP and NextAuth session management. This ensures only verified users can access the platform while maintaining a seamless user experience.
All authentication is handled server-side with industry-standard security practices including JWT tokens and secure session cookies.
Authentication flow
Here’s how the complete authentication process works:
User enters phone number
You provide your phone number in the format +91-XXXXXXXXXX on the sign-in page.
OTP generation
The server generates a secure 6-digit OTP and stores it in the database: const OTP = Math . floor ( 100000 + Math . random () * 9000 ). toString ();
await prisma . oTP . create ({
data: {
otpID: phoneNumber ,
otp: OTP ,
expiresAt: new Date ( Date . now () + 10 * 60 * 1000 ) // 10 minutes
}
});
SMS delivery
Twilio sends the OTP to your phone: await twilioClient . messages . create ({
body: `Your OTP for OpiniX is ${ OTP } , do not share it with anyone!` ,
from: process . env . TWILIO_NUMBER ,
to: phoneNumber
});
OTP verification
You enter the OTP within 10 minutes. The server validates: const otpData = await prisma . oTP . findUnique ({
where: { otpID: phoneNumber , otp }
});
if ( ! otpData ) return { verified: false , message: "OTP not found" };
if ( otpData . otp !== otp ) return { verified: false , message: "Invalid OTP" };
if ( otpData . expiresAt <= new Date ()) {
return { verified: false , message: "OTP expired" };
}
Account creation/lookup
After successful OTP verification:
If user exists: Retrieve account details
If new user: Create account automatically with initial balance of ₹0
const user = await prisma . user . create ({
data: {
phoneNumber: credentials ?. phoneNumber ,
role: "USER" ,
balance: 0.0
}
});
Session creation
NextAuth creates a secure session with JWT token containing:
User ID
Phone number
Verification status
Current balance
NextAuth configuration
Opinix uses NextAuth with a custom Credentials provider for phone authentication.
Provider setup
import CredentialsProvider from "next-auth/providers/credentials" ;
import { PrismaAdapter } from "@next-auth/prisma-adapter" ;
export const authOptions = {
adapter: PrismaAdapter ( prisma ),
providers: [
CredentialsProvider ({
name: "Credentials" ,
credentials: {
phoneNumber: {
label: "Phone" ,
type: "text" ,
placeholder: "+91-98765*****"
}
},
async authorize ( credentials ) {
const isUserExists = await prisma . user . findFirst ({
where: { phoneNumber: credentials ?. phoneNumber },
include: {
OTP: {
select: { isVerified: true }
}
}
});
const isUserVerified = isUserExists ?. OTP [ 0 ]. isVerified ;
if ( isUserExists ) {
return {
id: isUserExists ?. id ,
phoneNumber: isUserExists ?. phoneNumber ,
balance: isUserExists ?. balance ,
role: isUserExists . role ,
isVerified: isUserVerified
};
}
// Create new user if verified
const user = await prisma . user . create ({
data: {
phoneNumber: credentials ?. phoneNumber ,
role: "USER" ,
balance: 0.0
}
});
return { ... user , isVerified: true };
}
})
],
session: {
strategy: "jwt"
},
secret: process . env . NEXTAUTH_SECRET
};
Session callbacks
NextAuth extends the session with custom user data:
JWT callback
Session callback
Sign-in callback
async jwt ({ token , user }) {
if ( user ) {
token . phoneNumber = user . phoneNumber ;
token . isVerified = user . isVerified ;
token . id = user . id ;
token . balance = user . balance ;
}
return token ;
}
async session ({ session , token }) {
if ( token ) {
session . user . phoneNumber = token . phoneNumber ;
session . user . isVerified = token . isVerified ;
session . user . id = token . id ;
session . user . balance = token . balance ;
}
return session ;
}
async signIn ({ user , credentials }) {
if ( ! user . isVerified ) {
return false ; // Block unverified users
}
const isUserExists = await prisma . user . findUnique ({
where: { phoneNumber: credentials ?. phoneNumber }
});
return !! isUserExists ;
}
OTP management
Sending OTP
The OTP service handles generation, storage, and delivery:
export const sendSMSOTP = async ( phoneNumber : string ) => {
if ( phoneNumber . length <= 10 ) {
return { success: false , message: "Invalid phone number" };
}
try {
const OTP = Math . floor ( 100000 + Math . random () * 9000 ). toString ();
// Check if OTP already exists
const isOtpDataExists = await prisma . oTP . findUnique ({
where: { otpID: phoneNumber }
});
if ( isOtpDataExists ) {
// Update existing OTP
await prisma . oTP . update ({
where: { otpID: phoneNumber },
data: {
otp: OTP ,
expiresAt: new Date ( Date . now () + 10 * 60 * 1000 )
}
});
} else {
// Create new OTP
await prisma . oTP . create ({
data: {
otpID: phoneNumber ,
otp: OTP ,
expiresAt: new Date ( Date . now () + 10 * 60 * 1000 )
}
});
}
// Send via Twilio
await twilioClient . messages . create ({
body: `Your OTP for OpiniX is ${ OTP } , do not share it with anyone!` ,
from: process . env . TWILIO_NUMBER ,
to: phoneNumber
});
return { success: true , message: "OTP sent" };
} catch ( error ) {
console . error ( "Error in sendSMSOTP:" , error );
return { success: false , message: "Failed to send OTP" };
}
};
Validating OTP
export const verifySMSOTPAction = async ( otp : string , phoneNumber : string ) => {
try {
const otpData = await prisma . oTP . findUnique ({
where: { otpID: phoneNumber , otp }
});
// Check if already verified
if ( otpData ?. isVerified ) {
return { verified: true , message: "User already exists" };
}
// Validate OTP
if ( ! otpData ) {
return { verified: false , message: "OTP not found" };
}
if ( otpData . otp !== otp ) {
return { verified: false , message: "Invalid OTP" };
}
// Check expiration
if ( otpData . expiresAt <= new Date ()) {
await prisma . oTP . delete ({
where: { otpID: phoneNumber , otp }
});
return { verified: false , message: "OTP expired" };
}
// Mark as verified
await prisma . oTP . update ({
where: { otpID: phoneNumber , otp },
data: { isVerified: true }
});
return { verified: true , message: "OTP verified successfully" };
} catch ( error ) {
console . error ( "Error verifying OTP:" , error );
return { verified: false , message: "An error occurred" };
}
};
Session management
Accessing session data
In client components, use the useSession hook:
import { useSession } from 'next-auth/react' ;
function MyComponent () {
const { data : session , status } = useSession ();
if ( status === "loading" ) {
return < div > Loading ...</ div > ;
}
if ( status === "unauthenticated" ) {
return < div > Please sign in </ div > ;
}
return (
< div >
< p > User ID : { session . user . id }</ p >
< p > Phone : { session . user . phoneNumber }</ p >
< p > Balance : ₹{ session . user . balance }</ p >
</ div >
);
}
In server components and API routes:
import { getServerSession } from "next-auth" ;
import { authOptions } from "@/lib/auth" ;
export async function GET ( req : Request ) {
const session = await getServerSession ( authOptions );
if ( ! session ) {
return new Response ( "Unauthorized" , { status: 401 });
}
// Access user data
const userId = session . user . id ;
const balance = session . user . balance ;
// ...
}
Session expiration
Sessions use JWT with the following behavior:
Default expiration : 30 days
Automatic refresh : Sessions refresh on each request
Logout : Clear session cookie
import { signOut } from 'next-auth/react' ;
// Sign out user
await signOut ({ callbackUrl: '/' });
Security best practices
OTPs are single-use (marked verified after use)
10-minute expiration window
Stored securely in database, never logged
Sent only via encrypted SMS channel
Never included in client-side code
JWT tokens signed with NEXTAUTH_SECRET
HttpOnly cookies prevent XSS attacks
Secure flag enabled in production
SameSite=Lax prevents CSRF
Sessions invalidated on password change
Phone numbers validated before OTP generation
Rate limiting on OTP requests (implementation recommended)
Database indexes prevent duplicate accounts
OTP attempts limited (implementation recommended)
Required secrets in .env: NEXTAUTH_SECRET = your-secret-key
NEXTAUTH_URL = https://yourdomain.com
TWILIO_ACCOUNT_SID = your-twilio-sid
TWILIO_AUTH_TOKEN = your-twilio-token
TWILIO_NUMBER = +1234567890
Never commit these to version control!
Type definitions
Opinix extends NextAuth types for TypeScript support:
declare module "next-auth" {
interface User {
id ?: string ;
phoneNumber ?: string ;
isVerified ?: boolean ;
balance ?: number ;
}
interface Session {
user : DefaultSession [ "user" ] & {
id ?: string ;
phoneNumber ?: string ;
isVerified ?: boolean ;
balance ?: number ;
};
}
}
declare module "next-auth/jwt" {
interface JWT {
id ?: string ;
phoneNumber ?: string ;
isVerified ?: boolean ;
balance ?: number ;
}
}
Protected routes
Protect pages that require authentication:
import { redirect } from 'next/navigation' ;
import { getServerSession } from 'next-auth' ;
import { authOptions } from '@/lib/auth' ;
export default async function ProtectedPage () {
const session = await getServerSession ( authOptions );
if ( ! session ) {
redirect ( '/api/auth/signin' );
}
return < div > Protected content </ div > ;
}
For client-side protection:
import { useSession } from 'next-auth/react' ;
import { useRouter } from 'next/navigation' ;
import { useEffect } from 'react' ;
function ProtectedComponent () {
const { data : session , status } = useSession ();
const router = useRouter ();
useEffect (() => {
if ( status === 'unauthenticated' ) {
router . push ( '/api/auth/signin' );
}
}, [ status , router ]);
if ( status === 'loading' ) {
return < div > Loading ...</ div > ;
}
return < div > Protected content </ div > ;
}
API route protection
Protect API routes from unauthorized access:
import { NextRequest , NextResponse } from "next/server" ;
import { getServerSession } from "next-auth" ;
import { authOptions } from "@/lib/auth" ;
export async function POST ( req : NextRequest ) {
const session = await getServerSession ( authOptions );
if ( ! session ?. user ?. id ) {
return NextResponse . json (
{ error: "Unauthorized" },
{ status: 401 }
);
}
// Process authenticated request
const userId = session . user . id ;
// ...
}
Always verify sessions server-side. Client-side session checks can be bypassed and should only be used for UI purposes.
Troubleshooting
Common causes:
Phone number format incorrect (must include +91)
Twilio credentials invalid
Phone number not verified in Twilio (test mode)
SMS delivery delay (wait up to 2 minutes)
Check server logs for Twilio errors.
Verify:
NEXTAUTH_SECRET is set in environment
NEXTAUTH_URL matches your domain
Cookies are enabled in browser
Not using incognito/private mode
No conflicting middleware blocking cookies
Reasons:
OTP expired (10 minute limit)
Typo in entered OTP
OTP already used (single-use)
Database sync issue
Request a new OTP to resolve.
Next steps