The build order, roughly:
Express+DB โ Auth โ Orders โ Payments โ Hardening (rate limits, validation, logging) โ Menu customization (sizes/toppings with live pricing)
Every step tested end-to-end before moving to the next.
Payments: Stripe PaymentIntents + signed webhooks.
Card data never touches my server โ Stripe handles that directly. My backend only finds out a payment succeeded when Stripe sends a cryptographically signed webhook confirming it. No "trust the frontend" shortcuts.
Payments: Stripe PaymentIntents + signed webhooks.
Card data never touches my server โ Stripe handles that directly. My backend only finds out a payment succeeded when Stripe sends a cryptographically signed webhook confirming it. No "trust the frontend" shortcuts.
Orders run inside real database transactions.
If one item in a multi-item order fails to insert, the entire order rolls back no half-created orders sitting in the database. Prices are always recalculated server-side too, never trusted from the client.
Orders run inside real database transactions.
If one item in a multi-item order fails to insert, the entire order rolls back no half-created orders sitting in the database. Prices are always recalculated server-side too, never trusted from the client.
The part that took the most care: BOLA protection (Broken Object-Level Authorization).
Every "get my order" / "delete my address" query is scoped by the authenticated user's ID server-side never trusted from the URL.
Tested it by literally trying to steal another account's data.
The part that took the most care: BOLA protection (Broken Object-Level Authorization).
Every "get my order" / "delete my address" query is scoped by the authenticated user's ID server-side never trusted from the URL.
Tested it by literally trying to steal another account's data.
Auth was the first real challenge:
โข bcrypt password hashing (never plain text)
โข short-lived JWT access tokens (15 min)
โข long-lived refresh tokens, stored server-side so they can actually be revoked
โข soft-delete accounts that instantly kill every active session
Auth was the first real challenge:
โข bcrypt password hashing (never plain text)
โข short-lived JWT access tokens (15 min)
โข long-lived refresh tokens, stored server-side so they can actually be revoked
โข soft-delete accounts that instantly kill every active session
Here's the actual request flow. Every request passes through Helmet, CORS, rate limiting, JWT verification, and ownership checks before it ever touches the database.
Nothing gets trusted by default not the client, not the price, not the user ID in the URL.
Here's the actual request flow. Every request passes through Helmet, CORS, rate limiting, JWT verification, and ownership checks before it ever touches the database.
Nothing gets trusted by default not the client, not the price, not the user ID in the URL.
Started with the absolute basics: npm init, a single Express route, app.listen().
Then layered in PostgreSQL, proper folder structure (routes/controllers/models), and never looked back.
By the end: 8 database tables, 25+ endpoints.
Started with the absolute basics: npm init, a single Express route, app.listen().
Then layered in PostgreSQL, proper folder structure (routes/controllers/models), and never looked back.
By the end: 8 database tables, 25+ endpoints.
Also learned the hard way
if you change a value in .env, you MUST restart the server. Node doesn't hot-reload env vars. Chased the same "secret mismatch" bug three separate times because I kept forgetting this.
Got the whole pipeline working end-to-end on a live call. Then I killed it.
The voice sounded too generic once I actually heard it in a real conversation not worth the extra latency it added to every sentence. Reverted to a standard, faster voice instead.
Got the whole pipeline working end-to-end on a live call. Then I killed it.
The voice sounded too generic once I actually heard it in a real conversation not worth the extra latency it added to every sentence. Reverted to a standard, faster voice instead.
Real bug I hit: ffmpeg reading from a pipe (not a file) can't auto-detect the input format from a file extension โ it has to guess. It guessed wrong. Output was garbled gibberish.
Fix: one flag. -f mp3 before the input, telling ffmpeg explicitly what it's receiving.
Real bug I hit: ffmpeg reading from a pipe (not a file) can't auto-detect the input format from a file extension โ it has to guess. It guessed wrong. Output was garbled gibberish.
Fix: one flag. -f mp3 before the input, telling ffmpeg explicitly what it's receiving.
Real bug I hit: ffmpeg reading from a pipe (not a file) can't auto-detect the input format from a file extension โ it has to guess. It guessed wrong. Output was garbled gibberish.
Fix: one flag. -f mp3 before the input, telling ffmpeg explicitly what it's receiving.
The hardest part: custom Nigerian-accent voice.
Vapi's custom voice API wants raw 16-bit PCM audio, no headers, exact sample rate. My TTS provider (YarnGPT) returns mp3.
So I built a Node service: text โ YarnGPT โ mp3 โ ffmpeg conversion โ exact PCM Vapi needs streamed live.
The hardest part: custom Nigerian-accent voice.
Vapi's custom voice API wants raw 16-bit PCM audio, no headers, exact sample rate. My TTS provider (YarnGPT) returns mp3.
So I built a Node service: text โ YarnGPT โ mp3 โ ffmpeg conversion โ exact PCM Vapi needs streamed live.
Then I went adversarial on my own system before calling it done deliberately tried to break it with misheard speech, interruptions, unavailable items, incomplete payloads.
Found a real gap (allergen enforcement was prompt-only) and fixed it before moving forward.
Then I went adversarial on my own system before calling it done deliberately tried to break it with misheard speech, interruptions, unavailable items, incomplete payloads.
Found a real gap (allergen enforcement was prompt-only) and fixed it before moving forward.
Allergy safety was the part I cared about most. It's not enough to tell the model "warn about allergies" in a prompt models can slip.
So I made the backend itself reject adding a conflicting item unless the model explicitly passes an acknowledgment flag after warning the user.