Skip to content

Repository files navigation

thavare

Production storefront for Thavare, an Indian Ayurvedic skincare brand. It is a headless Next.js front end over Shopify: Shopify holds the catalogue, customers and order records, while checkout, payment, shipping, GST invoicing and warehouse operations are handled by this application.

The reason for the split is that Shopify's own checkout does not fit the requirements here — Razorpay as the payment gateway, Shiprocket serviceability quoted before payment, and Indian GST invoices with per-financial-year numbering. Orders are therefore created in Shopify only after the Razorpay signature verifies, and a local Turso (libSQL) table owns the shipment lifecycle.

Features

  • Headless catalogue from the Shopify Storefront API, with product copy, badges, ingredients and usage instructions read from Shopify metafields and normalised in lib/shopify-mapper.ts.
  • Razorpay checkout with server-side HMAC-SHA256 signature verification, constant-time comparison, and two-layer idempotency (local shipments table first, recent Shopify orders as fallback).
  • Shiprocket integration: pincode serviceability and shipping cost quoted pre-payment, order creation, courier assignment and pickup scheduling, AWB tracking, label generation, cancellation.
  • Shipment state machine (lib/shipment-transitions.ts) mapping Shiprocket webhook statuses onto a forward-only transition whitelist with terminal states, so out-of-order webhooks cannot regress an order.
  • Daily Vercel cron that retries failed or stuck shipments up to a per-record maxRetries, with reassignment to another courier on pickup failure and email alerts to ops.
  • GST invoices rendered from real HSN codes and per-product rates, splitting CGST/SGST from IGST by whether the customer is in the origin state, with invoice numbers issued from an atomic per-financial-year counter.
  • Token-authenticated ops dashboard (/ops) for acknowledging, packing, labelling, cancelling and refunding orders, guarded by a dedicated JWT secret in middleware.
  • Customer accounts via Shopify customer access tokens: registration, login, password recovery and reset, saved addresses, order history, and a wishlist stored in a customer metafield.
  • Per-request CSP nonce with strict-dynamic, plus HSTS, frame denial and referrer policy, applied in middleware.ts. The nonce is passed to Razorpay's injected script rather than relaxing the policy to unsafe-inline.
  • Rate limiting on every public write route, backed by Upstash Redis in production and falling back to an in-memory store locally so development needs no Redis.

Architecture

Browser (React 19 / Next 16 App Router)
  │
  ├─ catalogue ──────────────► Shopify Storefront API (GraphQL, 2025-01)
  │                            products, variants, metafields, customer auth
  │
  └─ checkout
       │  POST /api/shipping/check   → Shiprocket serviceability + rate
       │  POST /api/discount         → Shopify discount code lookup
       │  POST /api/razorpay/order   → Razorpay order (amount recomputed server-side)
       │        Razorpay modal
       ▼
     POST /api/razorpay/verify
       ├─ verify HMAC signature (timing-safe)
       ├─ idempotency: shipments table → Shopify recent orders
       ├─ create Shopify order (Admin API)
       ├─ insert shipments row, create Shiprocket order, assign courier
       └─ emit Klaviyo event

  Shiprocket ──► POST /api/webhooks/shipping/[token]
                   ├─ map status, validate transition
                   └─ on "delivered" → fulfil Shopify order

  Vercel cron ──► GET /api/cron/retry-shipments  (daily, Bearer CRON_SECRET)
Path Contents
app/ App Router pages, storefront routes, /ops dashboard, /api route handlers
components/ Storefront UI — home sections, product, cart, shop, layout, shared primitives
lib/ Integration clients (shopify, shopify-admin, shiprocket, razorpay, klaviyo), Zustand stores (cart, wishlist, auth), GST logic, rate limiting, sanitiser, Sentry reporter
lib/db/ Drizzle schema (shipments, invoice_counters) and the libSQL client
drizzle/ Generated SQL migrations and snapshots
e2e/ Playwright specs, including a suite that runs against the live site
docs/ Design specs and implementation plans kept alongside the code

Editorial content — journal articles and the ingredient glossary — lives in typed modules (lib/journal.ts, lib/ingredients.ts) rather than a CMS, since it changes rarely and benefits from being type-checked and statically rendered.

Quickstart

npm install
cp .env.example .env.local   # fill in real values
npx drizzle-kit push         # apply the Drizzle schema to Turso
npm run dev                  # http://localhost:3000

lib/shopify.ts throws at import time when the Shopify variables are missing, so the app will not boot without at least NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN and NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN.

Variable names required by .env.example (values omitted):

Group Variables
Shopify NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN, NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN, SHOPIFY_ADMIN_ACCESS_TOKEN
Payments NEXT_PUBLIC_RAZORPAY_KEY_ID, RAZORPAY_KEY_SECRET
Shipping SHIPROCKET_EMAIL, SHIPROCKET_PASSWORD, SHIPROCKET_CHANNEL_ID, SHIPROCKET_PICKUP_LOCATION, SHIPROCKET_WEBHOOK_SECRET
Database TURSO_DATABASE_URL, TURSO_AUTH_TOKEN
Ops OPS_JWT_SECRET, OPS_USERS, OPS_ALERT_EMAIL, CRON_SECRET
Marketing / analytics NEXT_PUBLIC_KLAVIYO_PUBLIC_KEY, KLAVIYO_PRIVATE_API_KEY, KLAVIYO_LIST_ID, NEXT_PUBLIC_GA_MEASUREMENT_ID
Optional UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN, SENTRY_DSN, NEXT_PUBLIC_SENTRY_DSN, NEXT_PUBLIC_BRAND_VIDEO_URL

The contact form additionally reads WORKSPACE_EMAIL and WORKSPACE_APP_PASSWORD for its Nodemailer transport; these are not listed in .env.example.

API

Method Route Purpose
POST /api/razorpay/order Create a Razorpay order; server recomputes the payable amount
POST /api/razorpay/verify Verify signature, create Shopify order and shipment record
POST /api/shipping/check Shiprocket serviceability and rate for a 6-digit pincode
POST /api/discount Validate a Shopify discount code
POST /api/stock-check Live availability for up to 20 variant GIDs
GET /api/search Flattened product list for the search modal
POST /api/track-order Order status by order number plus email
GET /api/invoice/[token] Invoice data; token is base64url(orderName:razorpayPaymentId)
POST /api/reviews/submit Submit a review (customer token required)
POST /api/subscribe, /api/contact Klaviyo subscription; support email
POST /api/account/* Orders, addresses, wishlist, cancel-order — Bearer customer token
POST /api/ops/* Dashboard actions — ops-token cookie required
POST /api/webhooks/shipping/[token] Shiprocket status webhook, token compared to the secret
GET /api/cron/retry-shipments Retry stalled shipments; Authorization: Bearer CRON_SECRET
GET /api/health Shopify, Shiprocket and Turso probes; always 200, status ok/degraded

/api/health deliberately returns 200 even when degraded, so an uptime check or load balancer does not pull the site out of rotation because one third-party dependency is slow.

Tech stack

Next.js 16 (App Router) · React 19 · TypeScript · Tailwind CSS v4 · Drizzle ORM on Turso/libSQL · Zustand · jose (JWT) · Razorpay · Shiprocket · Shopify Storefront and Admin APIs · Klaviyo · Upstash Redis · Nodemailer · deployed on Vercel.

Testing

Unit tests with Vitest (jsdom) cover the cart store, the Shopify product mapper, shipment transitions, box/weight packing and the Shiprocket client:

npm run test        # watch mode
npm run test:run    # single run
npm run lint

Playwright end-to-end specs live in e2e/ and are not wired to an npm script:

npx playwright test                     # against http://localhost:3000
npx playwright test e2e/live-site.spec.ts   # runs against https://thavare.com

License

Not currently licensed for reuse.

About

Headless Next.js storefront for an Ayurvedic skincare brand: Shopify catalogue, Razorpay checkout, Shiprocket shipping and GST invoicing

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages