Opening.theVurlo.

> Initializing Session...

INITIALIZING_WORKSPACE·0%
01.Vurlo
Active Session
01 . Project Overview

Vurlo

Built in 10 Days • React 19 • TanStack Start

vurlo/hero.webp
Hero view

Description

I built and shipped a complete e-commerce SaaS platform solo at vurlo.store. It integrates Firebase Auth, granular Firestore security rules, Razorpay checkout, and an interactive AI chatbot helper. Engineered for performance with lazy-loading, product quick-view modals, and server-side SEO & GEO optimization, all managed via a custom admin panel.

Timeline

10 days

Role

Founder & Full-Stack Engineer

Status

Production

Year

2026

Deployment

Production SaaS

Core Stack

React 19 / Firebase

Visit Live Site View Source Code
02 . Core Rationale

Why I Built It

Understanding the core problems, proposed solutions, and realized product achievements.

The problem

Firestore has no "hold this unit while someone pays" primitive — two people can buy the last lamp at the same second if stock isn't locked at the right moment.

What actually happens
T+0s

Customer A adds the last Saturn Lamp (stock: 1) to cart, hits checkout.

T+1s

Customer B does the exact same thing, same product, same second.

T+2s

Both clients read products/{id}.stock → both see 1. Both pass the "is there enough stock" check.

T+3s

Both writes land. Stock goes 1 → 0 → then to -1 if nothing stops the second write.

T+9s

Razorpay confirms both payments. Vurlo just sold a lamp it doesn't have.

The fix, step by step
1
Reserve inside a transaction, not before it
use-cart.tsx

placeOrder() opens one runTransaction per cart line item and re-reads stock at commit time — not whatever the client fetched on page load.

2
Throw before any write lands
use-cart.tsx

If stock < quantity inside the transaction body, it throws. Firestore retries the whole read/write pair on conflict — I'm not writing retry logic by hand.

3
Batch the order creation
use-cart.tsx

One writeBatch creates the order doc, clears the cart subcollection, and writes a notification — all committed together, so there's no half-created order.

4
Roll back on every failure exit
checkout-component.tsx

Signature mismatch, modal dismiss, SDK constructor throw, raw catch — all four delete the pending order and run a compensating runTransaction to add stock back.

Result

Zero oversells since launch — COD and Razorpay both run through the same reserve → commit → verify pipeline.

Stock leaks in prod
0
Failed checkout recoveries
100%
Lighthouse perf
98 / 100
Initial bundle size
-45%
03 . System Pipeline

Architecture Pipelines

Visual system flow diagrams representing data pipelines and structured checks.

Pipeline Data flow nodes (Glows on Hover)
01 // routes

src/routes/ → routeTree.gen.ts

automatic page routes matched directly to your project folders — uses @tanstack/router-plugin to watch the file tree at build-time and compile routeTree.gen.ts, removing any need to write manual routing configs.

02 // cart-hook

use-cart.tsx

keeps client cart state fully synced across open tabs and guest sessions — uses a live onSnapshot listener for accounts, a localStorage fallback for guests, and performanceMerge() to reconcile both when a user logs in.

03 // checkout-ui

checkout-component.tsx

manages shipping validations, real-time coupon calculations, and checkout branching — lazy-loaded to keep load times down, splitting execution flows cleanly between Cash-on-Delivery and Razorpay online payments.

04 // reserve

placeOrder()

reserves stock atomically inside a database transaction so two people never buy the same last item — reads current stock, updates it, and commits checkout details alongside cart clearing in a single atomic writeBatch.

05 // razorpay-order

api/create-razorpay-order.ts

a secure backend helper to initiate online credit/debit card orders — instantiates the Razorpay SDK server-side to generate order tokens securely without ever exposing keys to the client web browser.

06 // razorpay-verify

api/verify-razorpay-payment.ts

the check that stops someone faking a 'payment successful' callback and getting a free order — recomputes crypto.createHmac('sha256', RAZORPAY_KEY_SECRET) over `${order_id}|${payment_id}` and compares it against razorpay_signature before firebase-admin marks the order paid. no timing-safe compare, works fine for now but I'd swap in crypto.timingSafeEqual if I revisit this

07 // rules

firestore.rules

in plain terms: a customer can cancel their own pending order and change nothing else about it. Enforced by `diff(resource.data).affectedKeys().hasOnly(['status'])` on the top-level orders/{orderId} rule — users/{uid}/orders/{orderId} itself is hard-locked to `allow read, write: if false` so there's no back door in

08 // email

api/send-order-email.ts

sends confirmation emails asynchronously without slowing down checkout — fires a background notification request to Resend after checkout commits, ensuring slow mail servers never hold up the customer's success screen.

09 // ai-recommend

api/ai-recommend.ts

uses AI to suggest complementary products directly on the cart page — sends current cart catalog items to Gemini 2.0 Flash and returns matching product IDs, backed by an in-memory cache to save on API costs.

04 . Core Capabilities

Features & Capabilities

Key features and built-in components implemented inside the codebase.

Feature Checklist
  • ✓Complete Firebase Authentication system — secure email/password login/signup workflows with instant client profile instantiation.
  • ✓Granular Firestore security rules — locks down user data, admin portals, and orders, preventing back-doors or cross-account access.
  • ✓Interactive customer AI chatbot assistant — answers catalog questions, recommends items, and handles pre-sale queries in real time.
  • ✓Dynamic product 'Quick View' preview overlays — lets customers check product specs, variants, and galleries without leaving the main collection feed.
  • ✓Optimized client-side lazy-loading bundles — routes, modals, and admin panels are loaded on-demand to keep initial JS bundles minuscule.
  • ✓Complete SEO & GEO optimization — server-side generated tags, geographic-aware configuration, and automated dynamic sitemap updates to rank perfectly on search engines.
  • ✓Secure stock reservation before payment — reserves items using a database transaction before the payment gateway modal opens, preventing race conditions.
  • ✓Automatic stock recovery on aborted payments — a compensating rollback deletes pending orders and restores stock on signature mismatches, modal dismissals, or API constructor throws.
  • ✓Seamless guest-to-account cart merging — preserves local items in browser storage and combines them into the customer database on sign-in without duplicating quantities.
  • ✓Flexible promo coupons — handles percentage/fixed discount rules, expiry dates, minimum spending thresholds, and usage caps.
  • ✓Tamper-proof payment integrations — runs online card payments verified via server-side HMAC-SHA256 checks, with a COD alternative.
  • ✓Secure merchant dashboard — role-gated access control for admins to manage product listings, coupon campaigns, and real-time inventory alerts.
03 . Technical Barriers

Solving Complex Bottlenecks

A deep dive into security rule architectures, race conditions, and verification solutions.

Problem

Between 'add to cart' and Razorpay's async confirmation, nothing stopped the same lamp being bought twice.

Why it was difficult

Two customers could both end up thinking they'd bought the last lamp. Firestore has no built-in way to say 'only one request touches this stock count at a time' (what databases call row-locking) — two placeOrder() calls reading the same field at the same instant can both pass the check and both decrement. That's overselling by construction, not a bug.

The fix
1
Wrap each cart line item in its own runTransaction
use-cart.tsx
2
Re-read stock inside the transaction body, not before it
3
Throw before any write — Firestore retries the whole pair on conflict
What I learned

"Firestore transactions are scoped per-document, not per-request. Protecting a multi-item order means N separate transactions, not one transaction wrapped around the whole cart — I got that wrong on the first pass."

06 . Build Log Timeline

Development Journey

A chronological day-by-day log detailing core milestones and features added.

Milestone timeline
Day 1

Scaffold & File-based Routing

TanStack Start scaffold, Tailwind/shadcn config, src/routes/ tree established.

Day 2

Firebase Wiring

firebase.json set up; Auth + Firestore SDK wired into use-auth.tsx and lib/firebase.ts.

Day 3

Cart & Guest Persistence

use-cart.tsx built out — onSnapshot cart, localStorage guest fallback, merge-on-login batch.

Day 5

Firestore Security Rules

firestore.rules written — admin role gating, field-diff protections, nested orders lockdown.

Day 6

Checkout & Coupons

checkout-component.tsx multi-step flow shipped, coupon validation against the coupons collection.

Day 7

Razorpay Integration

create-razorpay-order.ts and verify-razorpay-payment.ts built, HMAC verification wired into every exit path.

Day 8

Admin Console

admin.tsx role-gated layout shipped with products, orders, coupons, stock-request panels.

Day 10

AI, Email & Performance Pass

Gemini recommendations, Resend emails, image pipeline, manualChunks split, sitemap gen before deploy.

07 . Technical Audits

Performance & Audits

Audited system lighthouse stats for performance, accessibility, best practices, and SEO.

Lighthouse Audit (Active SVG Gauges)
0
Performance
0
Accessibility
0
Best Practices
0
SEO
05 . Architecture Decisions

Trade-off Explanations

Concise rationales for technology selections and future roadmaps.

Design Choices

File-based Routes, Compiled at Build Time

TanStack Start / Router

Routes are plain files under src/routes/; @tanstack/router-plugin compiles them into routeTree.gen.ts so I'm not hand-maintaining a route config object anywhere.

Suspense-Driven Code Splitting

React 19

checkout-component.tsx (906 lines) and the admin dashboard are both React.lazy imports behind Suspense fallbacks — keeps checkout/admin code out of the anonymous storefront bundle entirely.

Per-Document Atomic Stock Decrements

Firestore Transactions

runTransaction runs once per product doc in both placeOrder() and reserveStockAndGetTotal() (use-cart.tsx), so two simultaneous checkouts on the same item can't both win the stock check.

Deliberate Vendor Splitting

Vite manualChunks

rollupOptions.output.manualChunks explicitly separates vendor, firebase, router, query, and ui — a Firestore SDK bump doesn't blow away the React vendor chunk's cache.

Server-Only Payment Verification

Razorpay + firebase-admin

verify-razorpay-payment.ts is the only place RAZORPAY_KEY_SECRET recomputes the HMAC signature, and the only code holding firebase-admin credentials to flip an order to paid. The browser never sees either.

If I built V2...

"Evaluating scaling adjustments and cache controls shows true engineering maturity."

  • ✓Extract the four copy-pasted rollback blocks in checkout-component.tsx into one shared function — right now a fix to one means remembering to fix it in three other places
  • ✓Batch the per-item runTransaction calls in placeOrder() into a single transaction that reads all product refs up front, instead of N sequential round-trips for an N-item cart
  • ✓Add a scheduled job to expire orders stuck at paymentStatus: 'upi_pending' when someone abandons the Razorpay modal in a way that doesn't fire ondismiss
  • ✓Move ai-recommend.ts's in-memory Map cache into Firestore or Redis — it resets on every serverless cold start, so the 30-minute TTL almost never actually survives long enough to matter
  • ✓Wrap coupon usageCount increments in the same transaction as the usageLimit check — right now two concurrent redemptions can both read usageCount before either write lands
06 . Screenshots

Visual Gallery

Scrollable preview tracks showcasing panel responsive structures.

Horizontal Screenshot Scroll
Homepage Preview
Homepage Preview
Product Collections
Product Collections
Product Quick View
Product Quick View
Order Tracking
Order Tracking
Inventory Console
Inventory Console
admin panel
Secondary screenshot

Finished exploring this session?

End the workspace session to release resources and return back to the primary portfolio index.