All Case Studies
Natually's re-platform at a glance
The main facts of Natually's move from Shopify to Dcrayon's Laravel platform.
| Item | Detail |
|---|---|
| Client and industry | Natually, natural skincare and haircare brand selling direct to consumers |
| Market and audience | India; online shoppers buying skincare and haircare |
| Engagement period | Catalogue import Mar 2026; move to its own server May 2026; maintained on the platform since |
| Our scope | Theme, catalogue import, link handling, checkout and payment integrations, shipping and tracking, product feeds and hosting move |
| Platform and stack | Laravel 12, PostgreSQL, Redis, Blade, Tailwind CSS, Alpine.js, Vite; Razorpay; Shiprocket; AWS in the Mumbai region |
Challenge
Natually sells natural skincare and haircare products online in India. In 2026 Dcrayon moved its store from Shopify to a Laravel platform that Dcrayon builds and maintains.
Natually's range runs from face serums, day and night creams, cleansers and kojic soaps to hair colour, hair removal wax, lip care and facial kits, most of them priced for everyday buying. The store ran on Shopify, and Dcrayon had customised its Shopify theme since 2025 (see Natually's Shopify storefront work). In March 2026 the catalogue was imported into Dcrayon's own e-commerce platform, built on Laravel 12, and by May 2026 the store was running on its own server under the brand's domain.
What Natually's store had to keep doing after leaving Shopify
Shopping features were spread across separate Shopify apps.
The archived storefront from 6 March 2026 loads a third-party one-page checkout and a shoppable-video app, and a capture from February 2025 shows further apps for free gifts, volume discounts and an Instagram feed. Each app brought its own scripts, its own settings screen and its own subscription, and none of them shared rules with the others. On the new platform, offers, cart rules and the connections to payment and shipping providers had to be managed from one codebase.
About 170 products and their old links had to survive the move.
A platform change can break every product link that search engines, ads and saved bookmarks point to. Natually's old store used Shopify's /products/ and /collections/ addresses, and the domain had been live since at least 2022. Some products had been renamed over the years and some had been discontinued. The new store had to keep each address working, redirect it deliberately, or retire it in a way search engines understand.
Checkout had to match how Indian shoppers pay.
The store needed UPI, cards, net banking and wallets, a form of cash on delivery, and a way for customers to follow their parcel after dispatch. It also had to keep selling when an outside checkout window failed to load.
Offers change often and must show everywhere at once.
Natually runs standing offers: a discount on prepaid orders, free shipping, and free gifts above set order values (on 13 October 2026, a free cap above Rs 999 and a free facial kit above Rs 1,699). Shoppers need to see these in the announcement bar, on product pages and in the cart, along with how far they are from the next gift.
One codebase serving several brands must keep each brand's details apart.
Dcrayon's platform runs more than one brand from the same code. The team's own notes record the risk plainly: a default value written for one brand, such as a phone number, could surface in another brand's footer. Natually needed its own theme, settings, payment keys and data without inheriting anyone else's.
Solution
A multi-tenant Laravel platform with a theme per brand
The platform is multi-tenant: one Laravel 12 codebase serves several brands, each one a tenant with its own PostgreSQL database. While Natually ran on the shared platform, the stancl/tenancy package switched the database connection, prefixed cache keys, scoped file storage, tagged queued jobs and loaded tenant-specific settings, such as payment keys, on every request, based on the domain. With a database per tenant rather than shared tables, one brand's orders and customers cannot appear in another brand's query.
On the shared platform, Natually got its own storefront without a separate codebase. A ResolveTenantFrontend middleware resolves views in a fixed order: the tenant's own folder first (frontends/natually/views), then the shared storefront defaults (frontends/default/views), then the platform's original views. Natually's header, footer, home page and product page live in its own folder with its own CSS and JavaScript, while account pages, emails and the admin panel stay shared and receive every platform fix.
In May 2026 Natually moved from the shared platform to its own AWS server in the Mumbai region, running its own copy of the platform code, with the database, uploaded files and theme carried across. The brand's domain now points to that server.
Keeping old Shopify product links alive
Products, categories, blog posts and content pages were imported into the new database: the local import of 27 March 2026 holds 175 products, 29 categories and 75 blog posts. Addresses kept Shopify's pattern, products at /products/ and collections at /collections/, and product slugs stayed the same wherever the product itself was unchanged.
The product route then makes one decision per request. An active product with that slug is shown. A product that exists but has been soft-deleted (marked deleted with a timestamp rather than removed from the database) returns 410 Gone, which tells search engines the page was removed on purpose rather than lost. An old slug from the Shopify era that maps to a renamed product returns a permanent 301 redirect to the new address. A singular /product/ address redirects to the plural form or returns 410 for a deleted item. Category pages follow the same rule, returning 410 for an inactive collection. The results section shows how this holds up on a sample of old links, and our website redesign page covers migrations like this one.
Offers, free gifts and the cart drawer
The cart is a side drawer, not a separate page. It shows each item with quantity controls, a "You may also like" row, and a message that tells the shopper how much more to add to qualify for the next free gift. The same thresholds appear in the announcement bar at the top of every page and in a dismissible reminder, so the offer reads the same wherever the shopper meets it.
On the browser side, cart and wishlist state live in Alpine.js stores (Alpine.js is a small JavaScript library for interactive page parts). The shared _core front end provides stores for the cart, the wishlist, the sign-in modal, product pack selection and toast messages, and every Natually page reads from the same cart store, so the header count, the drawer and the product page never disagree. Collections for combos and free gifts are ordinary categories in the catalogue, so the Natually team can add or retire an offer collection from the admin panel without code.
A checkout that holds stock and survives a failed payment window
A Buy Now button opens Shiprocket's hosted one-page checkout. Between the moment a shopper starts that checkout and the moment the order comes back, a few minutes can pass with no stock reserved, so the StockHoldService places a soft hold on the item in Redis, set atomically so two shoppers cannot both take the last unit. Holds expire after ten minutes if the shopper abandons the checkout. If the Shiprocket window fails to load for any reason, the page adds the product to the store's own cart and sends the shopper to the store's own checkout instead, so the sale is not lost.
The store's own checkout re-checks every cart line against current product data before it accepts an order. If stock has fallen, the shopper is told how many are left; if the price has changed since the item was added, the cart is updated and the shopper is asked to review it. The order is then written inside a database transaction that locks each product or variant row, decrements its stock only if enough remains, and marks the item out of stock when it reaches zero.
Payments run through a PaymentGatewayInterface, introduced so that the rest of the code never talks to a payment provider directly. Each gateway implements the same methods (create a payment attempt, verify a payment, handle a webhook, issue a refund) and the GatewayRegistry holds four implementations: Razorpay, Cashfree, cash on delivery and partial cash on delivery. Partial cash on delivery asks for a small online advance, Rs 50 per the store's product FAQ, with the rest paid at the door. Amounts pass between gateways as a Money value object holding integer paise, never floating-point rupees, and results come back as typed objects such as PaymentAttemptResult, VerificationResult and RefundResult. This is the e-commerce side of our API development work.
Shipping, tracking and product feeds
Courier integrations for Delhivery and Blue Dart implement a ShippingProviderInterface with the same operations: check a pincode, calculate a cost, book a delivery, track a waybill, cancel, print a label and request a pickup. A ShippingManager picks the provider for each order, and Shiprocket is integrated separately for checkout and shipments: when an order is placed, a PushOrderToShiprocket listener sends it on.
The platform also publishes a Google Merchant Center feed and a Meta catalogue feed from the same product records, so shopping ads list the same products and prices as the store. On 13 October 2026 the Google Merchant feed carried 143 products.
Contracts, services and typed data in the platform
Each responsibility has one home in the codebase, which the table below lists. Requests pass through middleware for tenancy and theme resolution, security headers, response caching and authentication before reaching controllers. Controllers validate input and hand anything that touches money, stock or a courier to a service. Services that talk to outside providers depend on contracts, PaymentGatewayInterface and ShippingProviderInterface, rather than on a provider's own client, and they exchange typed data objects such as CheckoutSessionData and ShiprocketWebhookEventData instead of loose arrays. Order events trigger listeners such as SendOrderNotification, PushOrderToShiprocket, CheckOrderFraud and UpdateRecommendationData, each handling one follow-up task after an order is placed.
The database stores money in decimal columns with two places: an order carries its subtotal, discount, tax, shipping cost, total and paid amount, each as decimal(12,2), while the gateway layer converts to integer paise before any provider sees a figure. Order lifecycle times have their own columns, confirmed_at, shipped_at, delivered_at and cancelled_at, and every status change is also written to order_status_history, so the team can see who moved an order and when.
The storefront's Blade components, sections and Alpine stores
Natually's pages are assembled from shared Blade components rather than copied markup. product-card renders a product in every grid, product-schema writes its structured data, breadcrumb handles navigation, and recommended-products, similar-products and recently-viewed fill the product page. Smaller components cover trust-badges, faq-section, whatsapp-button, chatbot-widget, modal, alert and form input.
The platform's section library, used by the shared storefront, includes hero_banner, category_grid, product_carousel, countdown_timer, brand_logos, instagram_reels, newsletter, text_with_image, trust_badges and custom_html. Natually's own home page template draws its promotional product banners from homepage_sections, where each section has a position and an active flag, so the team can edit, reorder or switch off a banner from the admin panel without code. The Natually theme's settings file names the theme, its rose-gold accent and the settings and sections it supports. Our e-commerce solutions page describes this kind of storefront.
Git hooks, CI checks and guarded releases
Work on the platform moves through feature branches and pull requests. Local Git hooks refuse a commit with a PHP syntax or Blade error and refuse a direct push to the main or development branches. Every pull request to the main or development branch then runs a continuous integration workflow that checks the syntax of every PHP file, compiles every Blade template, validates routes and configuration, builds the front-end assets with Vite, runs the PHPUnit suite, and fails the build if it finds a brand-specific default hard-coded into shared code, the guard against one brand's phone number appearing in another's footer.
While Natually ran on the shared platform, releases went through a script that refuses to continue without a fresh database snapshot, verifies each uploaded file by checksum, runs smoke tests across every brand, watches site health for five minutes and rolls back automatically if either check fails.
Two-factor admin access, signed webhooks and security headers
The admin panel uses its own login, separate from customer accounts. Every admin route requires two-factor verification, re-checked every 12 hours, and passes through an audit middleware that logs admin actions. Admins have a role, and anyone other than a super admin sees only the sections their permission list allows; twelve admin areas are guarded this way. Repeated failed sign-in attempts are locked out for a period.
Payment and messaging webhooks are verified before they are trusted. A Razorpay webhook must carry a signature that matches an HMAC-SHA256 of its body, compared in constant time, and Meta webhooks have their own signature check. Card and UPI details are entered only on the payment providers' own pages, so they never reach Natually's server. Prices and stock are re-checked on the server at checkout, whatever the browser sends.
Response headers come from a SecurityHeaders middleware. On 13 October 2026 the live store redirected plain HTTP to HTTPS and sent Strict-Transport-Security for one year with subdomains and preload, X-Content-Type-Options set to nosniff, X-Frame-Options set to SAMEORIGIN, a strict-origin-when-cross-origin Referrer-Policy, a Permissions-Policy that turns off the camera and microphone, and a Cross-Origin-Opener-Policy. Its session cookie was marked Secure, HttpOnly and SameSite=Lax.
Product structured data and split sitemaps
Product pages carry structured data for the product and its brand, an offer with price and availability, shipping details with delivery time and region, a merchant return policy and a breadcrumb trail. The home page publishes Organization data with contact details, WebSite data with a site search action, and FAQ data. The XML sitemap is split into pages, products, categories, brands and blog posts, and the robots file keeps the cart, checkout and account pages out of search. On 13 October 2026 the sitemaps listed 166 product pages and 16 collections. This is the technical SEO layer of the migration.
A separate AWS server with Redis and response caching
Natually's server on AWS runs nginx, PHP 8.3 and PostgreSQL, with Redis holding the cache, sessions and the job queue; our AWS services page covers this kind of setup. Product listing pages use a response cache: for visitors who are not signed in, a listing is served from cache for five minutes before it is rebuilt. On 13 October 2026 the home page served its product images in WebP format and lazy-loaded 62 of them below the fold. The store also ships a web app manifest, so shoppers can add it to a phone's home screen like an app.
Natually platform layers
Where each responsibility sits in the platform code behind Natually's store.
| Application part | What it does in this project |
|---|---|
| Tenancy and theme resolution | ResolveTenantFrontend serves Natually's own header, footer, home and product views before the shared defaults |
| Middleware | SecurityHeaders, CacheResponse, admin two-factor and audit, webhook signature checks |
| Controllers | Validate input, re-check price and stock at checkout, hand money and courier work to services |
| Payment contract | PaymentGatewayInterface with Razorpay, Cashfree, COD and partial COD gateways in the GatewayRegistry |
| Shipping contract | ShippingProviderInterface and ShippingManager for pincode checks, costs, bookings, tracking and labels |
| Typed data | Money in integer paise, CheckoutSessionData, PaymentAttemptResult, VerificationResult, RefundResult, ShiprocketWebhookEventData |
| Stock protection | StockHoldService soft holds in Redis during hosted checkout; row locks when the order is written |
Natually's order and catalogue tables
Real table names from the platform database and what each one holds for Natually.
| Entity | Table | What it holds |
|---|---|---|
| Product and variant | products and product_variants | Catalogue items, price, MRP, stock quantity and stock status |
| Collection | categories | Collections shown at /collections/, including combos and free gifts |
| Cart | carts and cart_items | Items with the price captured when added, re-checked at checkout |
| Order | orders and order_items | Subtotal, discount, tax, shipping, total and paid amount as decimal(12,2), with item price snapshots |
| Order history | order_status_history | Every status change on an order |
| Payment and shipment | payments and order_shipments | Gateway payment records and courier shipments per order |
| Home page | homepage_sections | The sections the Natually team arranges on the home page |
How Natually's store is protected
The controls on Natually's checkout, admin panel and responses, and how each was checked.
| Control | Where it applies | How it was checked |
|---|---|---|
| Separate admin login with two-factor verification and audit log | Every admin route | Admin route middleware reviewed in the repository |
| Section permissions | Twelve admin areas | Section middleware and admin permission list reviewed |
| Webhook signature verification | Razorpay and Meta webhooks | HMAC-SHA256 check with constant-time comparison reviewed |
| Server-side price and stock re-check | Checkout | Checkout code reviewed: price comparison, transaction and row locks |
| Security headers | Every response | Live check: HSTS with preload, nosniff, SAMEORIGIN, Referrer-Policy, Permissions-Policy |
| Session cookie and HTTPS | Whole store | Live check: HTTP redirects to HTTPS; cookie Secure, HttpOnly, SameSite=Lax |
Results
What Natually's shoppers and team have today
Natually now runs on code that Dcrayon maintains. Shoppers get a category menu across the full range, search that matches product names, descriptions, SKUs, categories and brands, a wishlist, a cart drawer that shows free-gift progress, Shiprocket's one-page checkout with a fallback to the store's own checkout, Razorpay payments and partial cash on delivery, and a Track Order page. The Natually team arranges the home page from sections, adds offer collections from the admin panel, and lists the same catalogue on Google and Meta from one set of product records.
To test the migration, we took the 54 product links on the archived Shopify homepage from 6 March 2026 and requested each one on the live store on 13 October 2026. 40 open the same product at the same address, 5 redirect permanently to a current product page, and 9 return 410 Gone for discontinued items. None of the 54 returns a 404 error.
A companion mobile app for Natually, built with Expo (React Native) and its own API, is in development in a separate codebase. Commercial figures for the move are not published.
Old Shopify links on the new Natually store
How old Shopify product links behave on the Laravel store.
| Metric | Baseline | Final | Period | Source |
|---|---|---|---|---|
| Old product links still reaching a product | 54 product links on the archived homepage | 45 of 54: 40 at the same address, 5 redirected with 301 | Checked 13 Oct 2026 | Archive capture and live requests |
| Old product links for discontinued items | Same 54 links | 9 return 410 Gone; 0 return 404 | Checked 13 Oct 2026 | Live requests |
Key takeaways
What Natually's move shows about leaving Shopify
A platform move is judged by what does not break: links, payment options and offers. Shoppers notice a missing payment method or a dead product link long before a new design, so those are the first things to plan and the first things to test after cutover.
A deliberate 410 is better than a silent 404. Telling search engines that a discontinued product is gone on purpose keeps crawling tidy, while a redirect to an unrelated product can confuse the shopper who clicked. And shared code needs guardrails: running several brands on one codebase saves work only if a setting written for one brand can never surface on another, which is why that check runs on every pull request.
If you are weighing Shopify against a custom build, our decision framework for D2C Shopify architecture sets out the trade-offs, and UPI payment architecture for India covers the checkout side. Ayurvexa's store runs on the same platform; read the Ayurvexa case study. To plan a re-platform of your own, talk to our team.
More work













