All Case Studies
Challenge
Keratine Professional sells keratin shampoos, hair masks, serums and hair spa treatments in India. In April 2026 Dcrayon moved its keratine.in store from Shopify to Dcrayon's multi-tenant Laravel commerce platform.
The range on keratine.in is built around smoothing and repair: repair and smoothening shampoos, sulphate-free shampoos with rosemary or Moroccan argan oil, keratin and nano-smoothing hair masks, Brazilian hair spa, a protein repair treatment, shine serums and a long list of shampoo, mask and serum combos. Until April 2026 the store ran on Shopify, with a blog of hair care guides published since at least December 2024. The brand also sells on Amazon, which is covered in the Keratine Professional Amazon case study; the website is the subject here.
Dcrayon's scope was the whole move: bringing the catalogue and the articles across, keeping the search titles Google already showed, building a Keratine home page on the shared platform, and getting the domain serving the new store. Laravel is a PHP framework for building web applications. Dcrayon's platform uses it to run several brand stores from one codebase, each brand with its own database. In that setup each brand is a tenant, and Keratine became one.
Why keratine.in could not simply be copied across
Shopify's product export does not match the platform's catalogue model.
Shopify exports a product as several CSV rows: the first carries the title, description, vendor and price, and the rows below carry extra images and variants. Categories arrive as a single text path, and the vendor is just a name. The Laravel platform stores products, categories, brands and images in separate related tables, and every product belongs to a seller. Pasting the export in by hand would have meant re-keying the range and losing the image order and SEO fields that came with it.
The blog and its search titles existed only on Shopify.
The guides at keratine.in/blogs/keratine-blogs covered keratin treatments, argan oil hair spa, red onion hair masks and serums for frizz. Each had its own meta title and description that Google was already showing. A platform move that left those behind would have cut the store off from the searches its content already answered.
Every section of the first Keratine home page was hard-coded.
The first version of the new home page assembled brand-specific blocks straight into the template: a serum feature, a timeless hair care panel, promotional tiles, a botox and keratin block, a hair mask block and a combos banner. Changing an offer, a banner or a product row meant editing code. For a brand that sells mostly through combos and seasonal offers, that tied everyday marketing to developer time.
The home page artwork weighed about 19 MB.
The section images arrived as PNG files, a format that keeps every pixel and produces large files. The commit that later converted them records their combined weight at about 19 MB. A shopper on mobile data would have downloaded that on a first visit to the home page.
What stood in the way of moving keratine.in
The four problems the move had to solve, and who each one affected.
| Issue | Who it affected | Business impact |
|---|---|---|
| Shopify export did not match the catalogue model | Brand team and Dcrayon developers | Re-keying products would lose image order and SEO fields |
| Blog and search titles only on Shopify | Readers arriving from search | Guides and their titles would drop out of search |
| Home page sections hard-coded | Brand marketing team | Every offer or banner change needed a developer |
| About 19 MB of PNG artwork | Shoppers on mobile data | Heavy first visit to the home page |
Solution
Choosing a tenant over a separate single-brand application
The first decision was where Keratine would live. A standalone Laravel application for one brand would have meant building checkout, accounts, order handling and an admin again, and maintaining them separately afterwards. Adding Keratine as a tenant on the existing platform gave the brand those features on day one and limited the brand-specific code to what made Keratine look like Keratine.
That limit was written into the build. Only the home page template was created for the brand. Collections, product pages, the cart, checkout, account and policy pages all come from the platform's shared default views. The Keratine look uses a teal accent (#5ac6c0) and the Lato typeface, set in the tenant's own stylesheet. The store was staged on a Dcrayon subdomain before keratine.in was pointed at the new platform.
How tenant lookup and view fallback serve each page
The platform uses the stancl/tenancy package with one database per tenant. When a request arrives on keratine.in, the package identifies the tenant from the domain and its bootstrappers switch the database connection, the file storage location and the queue context to Keratine's. Products, orders, customers and blog posts therefore sit in Keratine's own database, apart from every other brand on the platform.
Views are resolved by a middleware class called ResolveTenantFrontend. It looks for a folder named after the tenant and builds the view search order as frontends/keratine/views, then frontends/default/views, then the shared resources/views. Keratine's home page therefore comes from its own folder, while every other page falls back to the shared templates. The same middleware tells the layout which stylesheet and script bundle to load, and Vite builds Keratine's three entry files: frontends/keratine/css/app.css, frontends/keratine/css/home.css and frontends/keratine/js/app.js.
The rest of the stack is Laravel 12 on PHP 8.2 or later, Alpine.js for small interactive pieces in the browser, Tailwind CSS 4 for styling, Vite 6 to build the assets, spatie/laravel-permission for roles, Laravel Sanctum for API tokens, and Laravel Scout with Meilisearch for product search. The live store runs on AWS behind nginx.
Mapping Shopify's export onto the Laravel catalogue
Dcrayon wrote an import script for the move, scripts/import-keratine.php. It initialises the Keratine tenant first, so every record it writes goes into Keratine's database. It then reads the Shopify product export column by column: Handle, Title, Body (HTML), Vendor, Product Category, Type, Tags, Variant Price, Variant Compare At Price, Variant SKU, Variant Grams, Status, Published, SEO Title and SEO Description. Rows that carry only an Image Src are attached to the product above them, with their alt text and position kept, so each product arrives with its gallery in the original order.
Shopify's single category path is split into a parent and child category tree, and each product is linked to the deepest matching category. Anything Shopify left uncategorised goes into a hair care category rather than nowhere. Each vendor name becomes a Brand record, created once and reused by slug. Because the platform's products belong to a seller, the script also creates a seller record for Keratine Professional with a commission rate of zero, which keeps the brand's own products clear of any marketplace commission logic.
On the platform side, the products table stores mrp and price as decimal values with two places, a unique sku, a stock status of instock, outofstock or backorder, a tax rate defaulting to 18.00 percent, an hsncode for GST, and a seodata field for search titles and descriptions. Shopify's compare-at price maps to mrp, its price to price, and its SEO fields to seodata.
Bringing the blog across with its metadata
For the articles, Dcrayon added an Artisan command, import:keratine-blogs. Shopify's JSON feed for the blog was blocked, so the command reads the public HTML pages instead. It works through the blog's listing pages, opens each article, and saves the title, body, featured image, author, publish date, meta title, meta description and tags into the blog_posts table. It downloads each featured image into storage, skips any article whose slug already exists, and waits 500 milliseconds between requests. Options include --dry-run to preview what it would save, --skip-images, --pages to limit how many listing pages it reads, and --tenant, which defaults to keratine.
Because the platform's other brands did not need a blog table at the time, a tenant migration creates blogposts only in a tenant database where it is missing, guarded by a table check. The blogposts table holds title, a unique slug, excerpt, content, featuredimage, category, tags as JSON, authorid, ispublished, publishedat, seodata as JSON and viewcount. The imported articles are served at keratine.in/blog with the same slugs they had on Shopify.
Matching the Shopify-era titles and share cards
The search work was done as part of the move. The new home page title was set to match the existing keratine.in title word for word, and the meta description was expanded to 131 characters. Open Graph tags were added, the title, image and site name that WhatsApp, Facebook and other apps show when a link is shared, with a 1200 by 630 pixel share image, along with Twitter card tags. Section headings kept the wording shoppers already knew, such as the "by Keratine Professionals" line.
On 13 October 2026 the store's structured data, the code that tells search engines what a page describes, included an Organization record with the legal name, address, area served, contact point, logo and social profiles; a WebSite record with a site search action; and an FAQPage record on the home page. Product pages carry Product, BreadcrumbList and FAQPage records. Every page sets a canonical URL. The sitemap is an index split into products, categories, blog posts and pages, and the robots file keeps account, cart and checkout paths out of search results. This is the technical SEO side of the project.
Home page sections the brand team controls
On 27 April the hard-coded blocks were replaced with the platform's configurable sections. Each section is a row in the homepagesections table, read through the HomepageSection model, with a key, title, subtitle, type, content stored as JSON, background and text colours, an image, button text and link, a position and an isactive switch. The Keratine template checks each key before rendering it: categoriesgrid, featured, videotestimonials, bestsellers, productbanner1, benefits, deals, productbanner2, promobanner, testimonials and newarrivals. Turning a section off in the admin removes it from the page; changing its title, image or button changes the page without a deployment.
The page is assembled from shared Blade components, Laravel's reusable template pieces: x-layouts.app for the frame, x-product-card for every product tile, x-instagram-reels, x-faq-section, x-trust-badges and x-recently-viewed. Interactive behaviour comes from Alpine stores shared by every tenant, kept in frontends/_core/js/stores: cart.js, wishlist.js, pdp-pack.js for pack choices on product pages, auth-modal.js and toast.js. A flash-sale pop-up with a product count and a "No thanks" option, and a contained hero banner with rounded corners, followed on 27 and 28 April. The platform uses Blade components and shared stores rather than a formal design-system library.
The section artwork was converted from PNG to WebP, a newer format that compresses far better, and resized to 672 by 672 pixels. The page's viewport setting was changed to allow zooming up to five times, so a shopper with low vision can pinch to enlarge text. This is the custom web application work Dcrayon does when a store needs more than a theme.
Pull requests, CI checks and checksum deploys
Each piece of work went in as a small, named commit on its own branch and was merged through a numbered pull request: 28 for the tenant set-up, 74 for the home page rebuild and 80 for the banner fix. The commit records credit an AI coding assistant as co-author, so the code was AI-assisted and then merged through those pull requests. The repository's instructions for AI sessions forbid direct pushes to the main branch, hand edits on the production server and brand-specific defaults hard-coded into shared code, and a pre-push hook in the repository blocks pushes to protected branches.
Every pull request runs a continuous integration workflow with three jobs. The first checks the syntax of every PHP file, compiles every Blade template, validates the routes and the configuration cache, checks that Blade directives are balanced, and fails the build if a brand-specific default is hard-coded into shared code, which is what stops one tenant's phone number from appearing in another tenant's footer. The second runs the Vite production build. The third checks that no environment file or hard-coded secret has been committed. All three jobs passed on the pull requests for the home page rebuild and the banner fix, and again on their merges to the main branch on 27 and 28 April 2026. Deployments then run through a script that compares a checksum of every uploaded file with the copy on the server and stops if they differ. A checksum is a short fingerprint of a file's contents, so a mismatch means a file did not arrive intact.
Access, payments and headers on the store
Accounts are separated by role through spatie/laravel-permission: customer, admin, staff, seller, super admin, affiliate and delivery partner. The admin area sits behind middleware that confirms an admin session and then checks section by section whether that user may open the page, returning a 403 response when they may not. Admins can switch on two-factor sign-in, after which the admin area asks for a fresh code at set intervals; login attempts are throttled, and admin actions are written to an audit log. Every tenant's data stays in its own database.
On the payment side, by the 28 April release that carried Keratine's last home page change, the platform handled money through a Money value object that stores amounts as whole paise, never as floating-point rupees, and rejects negative values. Payment providers plug in behind a PaymentGatewayInterface with four operations: create a payment attempt, verify a payment, handle a webhook and refund. Checkout details travel in a CheckoutSessionData object, a DTO (data transfer object) that carries the cart, totals and contact details as typed, read-only values. Incoming Razorpay webhooks must pass a signature check before they are processed. Request validation sets firm limits, for example a cart quantity must be a whole number from 1 to 99, a phone number is required at checkout, and an email must be a valid address.
A check of keratine.in's public response headers on 13 October 2026 showed HTTPS with HSTS (Strict-Transport-Security) set for a year and including subdomains, X-Content-Type-Options set to nosniff, X-Frame-Options set to SAMEORIGIN, a Referrer-Policy of strict-origin-when-cross-origin, a Permissions-Policy that switches off camera and microphone access, and a Cross-Origin-Opener-Policy.
Lighter assets and AWS hosting
Vite produces content-hashed CSS and JavaScript files, and the page preloads the files it needs first. On the home page checked on 13 October 2026, 36 images were set to load lazily, only when a shopper scrolls near them. The store also ships a web app manifest, so shoppers can add Keratine to a phone's home screen and open it full screen like an app. The site is hosted on AWS.
Keratine data as the platform stores it
Real table, model and file names from the April 2026 release that holds Keratine's records.
| Entity | Table or file | What it holds |
|---|---|---|
| Product | products | Name, slug, sku, mrp and price (decimal), stock status, tax rate, hsn_code, seo_data |
| Category and Brand | categories, brands | Parent and child categories from the Shopify path; one brand per vendor |
| Seller | sellers | Keratine Professional as the selling business, commission rate zero |
| Blog post | blog_posts | Title, unique slug, content, featured image, tags, published_at, seo_data |
| Home page section | homepage_sections (HomepageSection) | Key, title, content JSON, image, button, position, is_active |
| Catalogue import | scripts/import-keratine.php | Maps Shopify CSV columns into the tables above |
| Blog import | import:keratine-blogs command | Reads Shopify article pages into blog_posts |
Keratine page parts from small to large
How the Keratine pages are put together, from shared settings up to full page templates.
| Component level | Examples from this build | Used on |
|---|---|---|
| Brand settings | Teal accent #5ac6c0, Lato typeface in the tenant stylesheet | Every Keratine page |
| Browser stores | cart.js, wishlist.js, pdp-pack.js, auth-modal.js, toast.js | Cart drawer, wishlist, product pages, sign-in |
| Blade components | x-product-card, x-faq-section, x-trust-badges, x-instagram-reels, x-recently-viewed | Home, collection and product pages |
| Configurable sections | categories_grid, bestsellers, product_banner_1, benefits, deals, promo_banner, new_arrivals | Keratine home page |
| Page templates | frontends/keratine/views/home.blade.php; shared frontends/default views | Home page; all other pages |
Controls protecting keratine.in orders and admin
The security controls in the platform release of 28 April 2026, and how each was confirmed for this case study.
| Control | Where it applies | How it was checked |
|---|---|---|
| Database per tenant | All Keratine records | stancl/tenancy configuration in source |
| Role and section permissions | Admin, staff, seller and other accounts | Permission middleware in source |
| Optional two-factor sign-in, login throttling, audit log | Admin accounts | Middleware in source |
| Signed Razorpay webhooks; amounts in paise | Checkout and payment callbacks | Webhook middleware and Money class in source |
| HSTS, nosniff, frame and referrer policies | Every public page | Response headers, 13 October 2026 |
Results
What keratine.in does now
keratine.in runs on the Laravel platform with its own Keratine home page and the shared store features behind it. Shoppers browse collections for shampoo, pre-shampoo, conditioner, hair mask, hair serum, hair spa, hair treatment and combos; open a side cart that suggests other products and shows how close the order is to a free gift; keep a wishlist; see recently viewed items; pay by UPI or cash on delivery; and reach the brand on WhatsApp.
The Shopify-era articles are readable at their original slugs under /blog, with their titles, descriptions and featured images, alongside newer guides on hair fall shampoo, frizz serums and hair spa creams. For the brand team, offers, banners and product rows are now changed in the admin rather than in code, and each section can be switched on or off.
The image conversion cut the home page section artwork from about 19 MB of PNG to about 300 KB of WebP, as recorded in the commit notes. Keratine's sales and traffic figures are not published.
The recommended next step is to point each old article address under /blogs/keratine-blogs/ at its new article, so readers and search engines land on the exact page they asked for rather than the blog index.
keratine.in before and after the move
The main changes the move made, with the record behind each.
| Area | Before | After | Evidence |
|---|---|---|---|
| Platform | Shopify store | Tenant on Dcrayon's Laravel platform, hosted on AWS | Live site and response headers |
| Blog | Shopify blog | Same articles and slugs on the new blog | Import command; live check of three articles |
| Home page content | Hard-coded brand blocks | Admin-managed sections with on and off switches | Pull request 74, 27 April 2026 |
| Section artwork | About 19 MB of PNG | About 300 KB of WebP at 672 by 672 pixels | Commit notes, 23 April 2026 |
| Release checks | Not applicable | Syntax, template, route, build and secrets checks on every pull request | CI runs, 27 and 28 April 2026 |
Key takeaways
What the Keratine move shows about changing platforms
A platform migration is also a content migration. The products were the easy part. The articles, page titles and share tags carried the store's history in search, and they needed their own import, their own metadata mapping and their own checks. Writing the blog import as a repeatable command, with a dry run and a skip for duplicates, made it safe to run more than once.
The tenant model kept the brand-specific code to one home page, three asset files and two import tools. Everything else came from shared views that other stores already used, which is also why a fix to the shared cart or checkout reaches Keratine without a separate release. Moving the home page to configurable sections took one more round of work and gave the brand team control of its own campaigns.
If you are weighing a similar move, our guide to building a high-converting e-commerce website with Laravel covers the store features worth planning for, and technical SEO architecture for D2C brands explains what to protect when URLs change. For the marketplace side of the same brand, read the Keratine Professional Amazon case study.
Planning to move your store off Shopify, or onto a platform you control? Talk to Dcrayon about your catalogue, content and URLs before you start.
More work













