User accounts and real-time chat on a car marketplace
A marketplace without user identity is just a catalog. The client needed buyers and sellers to register, verify themselves, and communicate — all within the platform, not through phone numbers pasted into listing descriptions. The goal was to keep conversations tied to specific advertisements so that every interaction had context, while also supporting a companion mobile app that needed push notifications for new messages.
jwt-based identity
We built the identity system on JWT tokens using the jose library, avoiding heavier auth frameworks that would have added dependencies we didn't need. Registration requires email verification — the user signs up, receives a code via Resend, and confirms before their account activates. Password reset follows the same pattern: a time-limited token sent to the registered email, a reset form, done. User profiles store basic information plus a list of the user's own advertisements, which they manage through a dedicated dashboard. The JWT approach keeps things stateless on the server side, which matters when requests can land on any edge node in the deployment.
conversations tied to ads
The messaging system is where things got interesting. Each conversation is linked to a specific advertisement — when a buyer clicks "message seller" on a listing, the system either finds an existing conversation between those two users for that ad or creates a new one. This means a single buyer can have separate conversation threads for different cars from the same seller, and the context is always clear. The data model is straightforward: a Conversation belongs to an Advertisement and has two participants, and Messages belong to a Conversation with sender reference and timestamps.
real-time feel without websockets
Making the chat feel real-time without WebSockets was a deliberate constraint. The platform runs on serverless infrastructure where persistent connections aren't practical. Instead, we implemented a polling strategy with adaptive intervals — when a conversation is open, the client polls every few seconds; when the app is backgrounded or the user navigates away, it drops to a slower heartbeat. On the mobile Expo app, push notification tokens are registered at login and updated on each app launch. When a new message arrives and the recipient isn't actively polling, a push notification fires. The combination of fast polling when active and push when idle covers the gap well enough that users perceive it as instant.
email deliverability
Email deliverability turned out to be a bigger headache than the chat system. Verification emails from a new domain, sent to mail providers popular in Central Asia, landed in spam at an alarming rate during the first weeks. We worked through SPF, DKIM, and DMARC configuration, warmed up the sending domain gradually, and simplified the email templates to avoid spam triggers — fewer images, shorter subject lines, plain-text alternatives. Resend's deliverability monitoring helped us track bounce rates and adjust. It took about three weeks of tuning before inbox placement stabilized above 95%.
ui separation and lessons
One subtle challenge was keeping the chat UI generic while the data model is ad-specific. The conversation list needs to show which car the conversation is about — thumbnail, title, price — but the chat component itself shouldn't care whether it's rendering a conversation about a new sedan or a used truck. We solved this by loading conversation metadata (including the linked advertisement summary) at the list level and passing only message data into the chat renderer. Clean separation, but it required careful thought about what loads when.
The lesson here: identity and messaging sound like solved problems, but the edge cases are all context-specific. Email deliverability in a specific region, real-time feel without real-time infrastructure, conversation threading that makes sense for a marketplace — none of these had off-the-shelf answers. The adaptive polling approach was a pragmatic call that saved significant infrastructure complexity, and six months in, not a single user has complained about message delays.
Stack
Frontend: Next.js 15, React 19, Tailwind CSS, TanStack Query
Auth: jose (JWT), Resend (transactional email)
Backend: Next.js Route Handlers, Prisma 6, PostgreSQL
Mobile: Expo (push notification tokens)
