Micro-merchants on Instagram and WhatsApp lose sales every day because they can't reply fast enough and they can't afford a full e-commerce setup.
So I built Byte-to-Bite: a conversational food-ordering engine where customers browse a menu, build a cart, and pay securely — all inside a chat window. No login wall. No heavy frontend. Just architecture.
Here's the full breakdown using the STAR pattern.
Situation: The problem with micro-commerce
Small vendors operating on Instagram and WhatsApp are doing real business but they're bottlenecked by manual replies. A customer DMs asking for a menu, waits, builds an order in a back-and-forth thread, then sends payment to a personal account.
The goal was a headless, automated agent. Customers get a live menu, a real cart, a custom invoice, and a secure checkout, without ever touching a login form.
Task: Three hard architecture problems
- Stateless HTTP forgets you. The backend had to act as a robust Finite State Machine (FSM) to track exactly where each user was in the checkout pipeline across requests.
- Cross-layer identity. Data had to flow cleanly between Next.js, an Express API, and Paystack without dropping the tracking thread.
- Concurrent isolation. Multiple shoppers had to build independent carts simultaneously with zero data leakage between sessions.
Action: How the three layers talk to each other
1. The Frontend Passport
On first load, the browser generates a UUID via the Web Crypto API:
const deviceId = window.crypto.randomUUID();
localStorage.setItem('x-device-id', deviceId);
Every outgoing Axios request injects this as a custom header:
axios.defaults.headers.common['x-device-id'] = localStorage.getItem('x-device-id');
No cookies. No registration. The client is its own passport.
2. The Headless Brain (FSM in TypeScript)
The backend reads the incoming device ID, pulls the user's MongoDB session, and routes the request through an FSM using strict union types:
type UserState = 'IDLE' | 'CHOOSING_MENU' | 'AWAITING_PAYMENT';
This eliminates runtime string typos and makes invalid state transitions impossible at compile time. Each state maps to a dedicated controller function — clean, modular, testable.
3. The Payment Bridge (Paystack + EventBus)
When a user types PAY, the backend compiles a checkout payload using environment variable abstraction:
const callbackUrl = process.env.PAYSTACK_CALLBACK_URL;
The device ID gets tucked into Paystack's transaction metadata. When payment clears, Paystack fires a server-to-server webhook. The backend verifies the HMAC signature, then fires an internal eventBus emission:
eventBus.emit('payment:confirmed', { deviceId });
// → resets MongoDB user state to 'IDLE'
Result: What production looks like
- Concurrent isolation: stress-tested at 50 simultaneous sessions — zero data leakage
- Environment parity: zero manual code changes between local dev and production
I chose to explicitly extends Document on the cart interface rather than using InferSchemaType. It's more verbose, but the explicitness of knowing exactly what methods are available on each document was worth the tradeoff for me. Curious whether others have a strong opinion here.
👉 Test the Live Demo Here (Running in Paystack Test Mode — feel free to use the dummy test cards to complete an order!)
What's next
Implementing Cron jobs to clear pending transactions after 24 hours.
When you build payment loops, do you lean on webhook-driven event buses, short polling, or WebSockets? I'd genuinely like to know — there are real tradeoffs I'm still thinking through.
Top comments (0)