Why sessions weren't an option

When I rebuilt KisanX from a PHP/MySQL web app into a MERN stack and React Native app, authentication was the first thing that had to change, not the last. Session-based auth, which the original PHP version used, depends on a browser holding a cookie and a server tracking session state. A React Native app doesn't have that relationship with a server by default — it's a standalone client making API calls, and it needs to carry its own proof of identity on every request. That's exactly what JSON Web Tokens are built for, so JWT became the obvious replacement once the mobile app was on the roadmap. I touched on this decision more broadly in a comparison of PHP and MERN; this article is the deeper dive into how the auth flow actually works.

The sign-up and login flow, end to end

At a high level, here's what happens when someone signs up on KisanX's mobile app: they submit their name, phone number, and a password. The Node.js/Express backend validates the input, hashes the password, and creates a user record in MongoDB — but marks the account as unverified. Instead of immediately issuing a JWT, the backend triggers an OTP to the provided phone number. Only after the correct OTP is submitted back does the account get marked verified, at which point the backend issues a JWT that the app stores and uses for every subsequent authenticated request.

Login, once an account is verified, is simpler: phone number and password go to the backend, the password hash is checked, and a fresh JWT comes back if it matches. No OTP is required again at that point — OTP exists to confirm identity at account creation, not to gate every login, which would be a usability cost without a proportional security benefit for this kind of app.

Issuing and verifying JWTs on the backend

On the server side, the JWT is signed with a secret key and includes the user's ID and a reasonable expiry time as its payload — deliberately not anything sensitive, since JWT payloads are encoded, not encrypted, and shouldn't be treated as a secure place to store data. Every protected route in the Express API runs through a middleware function that pulls the token from the request's Authorization header, verifies its signature, checks that it hasn't expired, and attaches the decoded user info to the request object before the actual route handler runs.

This middleware pattern was one of the more satisfying things to get right, because once it exists, every new protected endpoint — cart, orders, wishlist, dashboard — just needs one line referencing the middleware, rather than re-implementing auth checks everywhere. It also made the API noticeably easier to reason about, since "is this request authenticated" became a single, consistent question answered in one place.

Why I added an OTP layer on top of JWT

JWT solves "is this request from someone holding a valid token," but it doesn't solve "is this phone number real and does this person actually control it." For a marketplace connecting farmers and customers, having confidence that an account maps to a real, reachable phone number matters — it cuts down on throwaway accounts and gives a basic layer of trust before someone can transact on the platform. OTP verification at sign-up addresses that gap directly: a short numeric code sent to the phone, with a limited validity window, that has to be correctly entered before the account is considered active.

This is also where the Python for Cyber Security coursework and the Deloitte Cybersecurity Job Simulation I'd completed actually showed up in code, not just in theory. I made sure OTP attempts were rate-limited, so the verification endpoint couldn't be brute-forced by trying every possible code in quick succession, and that OTPs expire after a short window rather than staying valid indefinitely.

Storing tokens on the device, carefully

Where you store a JWT on a mobile device matters as much as how you issue it. Storing it in plain async storage without any protection is a common shortcut, and one I avoided — KisanX's app stores the token using Expo's secure storage APIs, which keep it out of plain, easily-readable app storage. On every app launch, the stored token gets checked and, if it's still valid, the user goes straight into the app rather than having to log in again; if it's expired, they're routed back to the login screen.

Testing the flow with Postman before touching the app

Before wiring any of this into the React Native screens, I built out and tested every endpoint — sign-up, OTP submission, login, and a couple of protected routes — in Postman first. That separation mattered more than I expected. It meant that when something went wrong later, I could immediately tell whether the bug was in the API logic or in how the app was calling it, instead of debugging both layers at once. I kept a small Postman collection with saved requests for each step of the auth flow, including deliberately invalid cases — an expired token, a wrong OTP, a reused token after logout — so I could re-run the whole sequence quickly any time I changed something on the backend.

That habit came directly out of comfort built up from PHP and MySQL Training and general API documentation practice; it's a small thing, but treating Postman as part of the actual build process, not just a debugging afterthought, made the JWT and OTP implementation far more reliable once it reached the mobile app.

Mistakes I made the first time around

My first implementation didn't handle token expiry gracefully — an expired token just resulted in confusing failed requests rather than a clean redirect to the login screen. I fixed that by having the API consistently return a specific status code for invalid or expired tokens, which the app's request layer checks for centrally, so any expired-token response anywhere in the app triggers the same "please log in again" flow instead of each screen handling it differently, or not handling it at all.

I also initially made the OTP window too short for real-world conditions — SMS delivery isn't instant, and a strict expiry meant legitimate users sometimes missed the window through no fault of their own. Lengthening it slightly, while keeping rate limits in place, was a small change that meaningfully reduced sign-up friction.

A short checklist if you're building this yourself

If you want to see how this fits into the rest of KisanX's architecture — the MongoDB schema, the REST API structure, and the deployment setup on Render — the full project page has the complete picture.