Lumiere
A luxury handcrafted-candle storefront with a queue-driven order pipeline — payments, branded emails, PDF invoices, fulfillment and loyalty run end-to-end with no manual intervention.
- Role
- Solo Full-Stack Developer
- Year
- 2026
- Category
- E-Commerce
- Platform
- Web App
- Stack
- ReactViteTypeScriptFastifyPrismaPostgreSQLRedisBullMQRazorpay

Why I built this
Selling premium candles meant the customer experience had to feel luxury-grade — branded order emails with PDF invoices, SMS and WhatsApp updates, loyalty points — while the operator needed resilience: retries, idempotency and alerting, 'so there is no manual intervention once payments land.' Lumiere's whole backend is designed around that promise, with a design target of scaling to high user volume with proper infrastructure.
How it works
Storefront catalog and discovery
Real-time search modal, category and collection filters synced to the URL, product cards with a waitlist CTA when sold out, and Framer Motion page transitions across a 13-product catalog.
Self-built auth
JWT access tokens (15m) with hashed, rotating refresh tokens (30d), per-route rate limits, Google Identity Services login, and auth-gated cart, wishlist and checkout flows.
Webhook-driven order automation
The Razorpay payment.captured webhook is the source of truth. It fans out to queues that send branded email/SMS/WhatsApp confirmations with a generated PDF invoice, create Shiprocket shipments with AWB tracking, deduct stock atomically with low-stock alerts, and update the live order status.
Super-admin panel and loyalty
A dashboard with product CRUD and a draft workflow, a review queue, and an automation timeline — plus a loyalty engine (1 point per ₹10 on delivery, birthday bonuses, win-back rewards).
Key decisions
Payment and order state transitions are driven by verified provider webhooks with dedupe — which survived the UPI delayed-capture edge case (authorized wait state, then captured confirmation).
Eight BullMQ queues with 3-attempt exponential backoff and idempotent job IDs (stage + orderId); terminal failures alert via Slack/email. Nothing is retried blindly.
Automation stages promote sequentially (1+2 → 3 → remaining) via an AUTOMATION_RELEASE_STAGES gate, so the pipeline proves itself incrementally in production.
Backend architecture
Modular Fastify monolith
Modules isolate auth, account, admin, orders, payments, products, cart, wishlist, reviews, automation, notifications, fulfillment, refunds, reports, webhooks and health, with centralized env validation and security plugins.
Eight BullMQ queues on shared Redis
automation-stage, notifications, fulfillment, inventory, post-delivery, reports, refunds and admin-alert — every stage with retries, persisted automation logs and alert paths.
20 Prisma models with webhook dedupe
Orders, payments, shipments, automation logs, rewards and returns, with a WebhookEvent(provider, eventId) uniqueness constraint so duplicate deliveries are harmless.
JWT + refresh rotation
Access tokens expire in 15 minutes; refresh tokens are stored hashed, rotated on use, and rate-limited per route (signup 3/min, login 5/min, refresh 10/min).
Challenges
OAuth security debt
An empty GOOGLE_CLIENT_ID after secret rotation, no PKCE, no MFA or email verification, and refresh tokens refreshable indefinitely within 30 days — documented and flagged for production hardening.
Launch-readiness gaps
The auth modal overlapped the cart drawer for guests, cart/wishlist were localStorage-only with backend persistence still TODO, and high-volume DB indexes on Order and AutomationLog were missing.
Docker/Render debugging friction
Four consecutive commits debugged the container: OpenSSL missing for Prisma, a bad health-check path, and a temporarily removed migrate command before landing on a working Render Dockerfile with migrate split from startup.
What I learned
Verified webhooks with dedupe are the only safe source of truth for money-driven state.
Idempotency plus staged rollout gates beat big-bang releases.
A blueprint-first workflow — execution.md → progress.md → prelaunch.md → launch-readiness.md — kept a solo build on track.
Code snippets
export const automationQueue = new Queue('automation-stage', { connection });
export const notificationQueue = new Queue('notifications', { connection });
export const fulfillmentQueue = new Queue('fulfillment', { connection });
export const inventoryQueue = new Queue('inventory', { connection });
export const postDeliveryQueue = new Queue('post-delivery', { connection });
export const reportsQueue = new Queue('reports', { connection });
export const refundsQueue = new Queue('refunds', { connection });
export const adminAlertQueue = new Queue('admin-alert', { connection });