13 KiB
OTP Tables Analysis - What Your Code Actually Does
Summary: NO user_id in OTP Tables - This is CORRECT
Your code uses phone number only for OTP tables. Users are created AFTER OTP verification.
Database Tables
1. otp_requests Table (in init.sql lines 94-109)
Status: ❌ NOT USED BY YOUR CODE
CREATE TABLE IF NOT EXISTS otp_requests (
id UUID PRIMARY KEY,
phone_number VARCHAR(20) NOT NULL, -- NO user_id!
otp_hash VARCHAR(255) NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
consumed_at TIMESTAMPTZ, -- Extra field for tracking consumption
attempt_count INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Note: This table exists in your database schema but is never referenced in your code.
2. otp_codes Table (ACTUALLY USED)
Status: ✅ ACTIVELY USED BY YOUR CODE
Database Schema (init.sql lines 115-122):
CREATE TABLE IF NOT EXISTS otp_codes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
phone_number VARCHAR(20) NOT NULL, -- NO user_id!
otp_hash VARCHAR(255) NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
attempt_count INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Code Definition (src/services/otpService.js lines 34-41):
CREATE TABLE IF NOT EXISTS otp_codes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
phone_number VARCHAR(20) NOT NULL, // ← NO user_id!
otp_hash VARCHAR(255) NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
attempt_count INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Both schemas match - NO user_id column!
How Your Code Uses otp_codes
Step 1: Request OTP (/auth/request-otp)
Location: src/routes/authRoutes.js line 189
const { code } = await createOtp(normalizedPhone);
Function: src/services/otpService.js lines 82-117
async function createOtp(phoneNumber) {
await ensureOtpCodesTable();
const code = generateOtpCode();
const expiresAt = new Date(Date.now() + OTP_EXPIRY_MS);
const otpHash = await bcrypt.hash(code, 10);
// Encrypt phone number before storing
const encryptedPhone = encryptPhoneNumber(phoneNumber);
// Delete any existing OTPs for this phone number
await db.query(
'DELETE FROM otp_codes WHERE phone_number = $1 OR phone_number = $2',
[encryptedPhone, phoneNumber]
);
// ← INSERT: Only phone_number, NO user_id!
await db.query(
`INSERT INTO otp_codes (phone_number, otp_hash, expires_at, attempt_count)
VALUES ($1, $2, $3, 0)`,
[encryptedPhone, otpHash, expiresAt] // ← NO user_id here!
);
return { code };
}
What happens:
- ✅ User requests OTP with phone number only
- ✅ OTP stored in
otp_codeswith phone_number only - ❌ NO user_id because user doesn't exist yet!
Step 2: Verify OTP (/auth/verify-otp)
Part A: Verify OTP Code
Location: src/routes/authRoutes.js line 267
const result = await verifyOtp(normalizedPhone, code);
Function: src/services/otpService.js lines 131-220
async function verifyOtp(phoneNumber, code) {
await ensureOtpCodesTable();
const encryptedPhone = encryptPhoneNumber(phoneNumber);
// ← SELECT: Lookup by phone_number only!
const result = await db.query(
`SELECT id, otp_hash, expires_at, attempt_count, phone_number
FROM otp_codes
WHERE phone_number = $1 OR phone_number = $2 // ← NO user_id in WHERE clause!
ORDER BY created_at DESC
LIMIT 1`,
[encryptedPhone, phoneNumber]
);
// ... verification logic ...
// ← DELETE: Remove OTP after verification
await db.query('DELETE FROM otp_codes WHERE id = $1', [otpRecord.id]);
return { ok: true };
}
What happens:
- ✅ OTP verified using phone_number only
- ✅ OTP deleted from
otp_codestable - ❌ Still NO user_id at this point!
Part B: Create/Find User (AFTER OTP Verification)
Location: src/routes/authRoutes.js lines 310-335
// ← This happens AFTER OTP verification succeeds!
// find or create user
const encryptedPhone = encryptPhoneNumber(normalizedPhone);
const phoneSearchParams = preparePhoneSearchParams(normalizedPhone);
let user;
const found = await db.query(
`SELECT id, phone_number, name, role, NULL::user_type_enum as user_type,
COALESCE(token_version, 1) as token_version
FROM users
WHERE phone_number = $1 OR phone_number = $2`,
phoneSearchParams
);
if (found.rows.length === 0) {
// ← CREATE USER HERE (after OTP verified!)
const inserted = await db.query(
`INSERT INTO users (phone_number) // ← Only phone_number, user gets auto-generated UUID id
VALUES ($1)
RETURNING id, phone_number, name, role, NULL::user_type_enum as user_type,
COALESCE(token_version, 1) as token_version`,
[encryptedPhone]
);
user = inserted.rows[0]; // ← user.id is created HERE!
} else {
user = found.rows[0]; // ← Existing user found
}
// Now user.id exists!
What happens:
- ✅ AFTER OTP verification succeeds
- ✅ User is found or created based on phone_number
- ✅
user.id(UUID) is assigned at this point - ✅ This is when user_id first exists!
Complete Flow Diagram
┌─────────────────────────────────────────────────────────────┐
│ STEP 1: Request OTP │
│ POST /auth/request-otp │
│ Body: { phone_number: "+919876543210" } │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ INSERT INTO otp_codes │
│ (phone_number, otp_hash, expires_at, attempt_count) │
│ VALUES ('+919876543210', 'hash...', '2024-...', 0) │
│ │
│ ❌ NO user_id - User doesn't exist yet! │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ STEP 2: Verify OTP │
│ POST /auth/verify-otp │
│ Body: { phone_number: "+919876543210", code: "123456" } │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ SELECT FROM otp_codes │
│ WHERE phone_number = '+919876543210' │
│ │
│ ❌ NO user_id in query - phone_number only! │
└─────────────────────────────────────────────────────────────┘
│
▼ (if OTP valid)
┌─────────────────────────────────────────────────────────────┐
│ DELETE FROM otp_codes WHERE id = ... │
│ (OTP consumed) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ STEP 3: Create/Find User │
│ (AFTER OTP verification succeeds) │
│ │
│ SELECT FROM users WHERE phone_number = '+919876543210' │
│ │
│ If NOT found: │
│ INSERT INTO users (phone_number) │
│ VALUES ('+919876543210') │
│ RETURNING id ← user.id CREATED HERE! │
│ │
│ ✅ user.id EXISTS NOW! │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ STEP 4: Create Session │
│ │
│ INSERT INTO refresh_tokens │
│ (user_id, token_hash, device_id, ...) │
│ VALUES (user.id, ...) ← user_id used here! │
│ │
│ INSERT INTO user_devices │
│ (user_id, device_identifier, ...) │
│ VALUES (user.id, ...) ← user_id used here! │
└─────────────────────────────────────────────────────────────┘
Why NO user_id in OTP Tables?
✅ Current Design (CORRECT):
-
User doesn't exist when OTP is requested
- OTP can be requested for phone numbers that don't have accounts yet
- User is created after successful OTP verification
-
Phone number is the identifier
- Phone number is UNIQUE in
userstable - Phone number links OTP → User
- No need for user_id in OTP table
- Phone number is UNIQUE in
-
Supports both registration and login
- New users: OTP → Create user
- Existing users: OTP → Find user
- Same flow works for both!
❌ Alternative (Would Be Wrong):
If you added user_id to otp_codes:
- ❌ Can't request OTP for new users (user_id doesn't exist)
- ❌ Would need to create user before OTP (defeats purpose of verification)
- ❌ Complicates the flow unnecessarily
Summary Table
| Table | Has user_id? |
When Used | Purpose |
|---|---|---|---|
otp_codes |
❌ NO | Request/Verify OTP | Store OTP codes before user exists |
otp_requests |
❌ NO | ❌ NOT USED | Legacy table (ignore it) |
users |
✅ YES (primary key) | After OTP verification | Store user accounts |
refresh_tokens |
✅ YES | After OTP verification | Store sessions (needs user_id) |
user_devices |
✅ YES | After OTP verification | Track devices (needs user_id) |
Answer to Your Question
Q: Should otp_code and otp_req include user_id?
A: ❌ NO - Your current code is correct as-is:
otp_codes- Does NOT haveuser_id✅ (correct)otp_requests- Does NOT haveuser_id, but also NOT USED by your code
Q: Are we going to create a user ID before and assign it, or only use phone number?
A: Your code uses phone number only for OTP, then creates user_id AFTER OTP verification:
- OTP request/verify: Uses phone_number only
- User creation: Happens AFTER OTP verification succeeds
- User_id assignment: Happens when user is created/found in
userstable
This is the standard and secure pattern for phone-based authentication! ✅