Headless Commerce April 5, 2026 12 min read

From Template to Platform: The Architecture Behind Cartridge

How we went from one client project to a repeatable product. The refactor story, the decisions that mattered, and why "platform" is the right word.

Tyler Colby · Founder, Colby's Data Movers

In January, we built a headless Shopify storefront for a client. By February, a second client wanted the same thing. By March, we had three clients. Each deployment started by forking the first client's repository and doing a find-and-replace across 30 files.

We changed the store name in 7 places. The Shopify domain in 4 places. The brand colors in 3 CSS files. The navigation in 2 layout components. The AI personality in a prompt template. The shipping policy in a markdown file. The contact email in the footer and the contact form. The social links in the footer and the Open Graph tags. The SEO defaults in the layout metadata. The analytics IDs in a script tag.

That was 50+ hardcoded values scattered across 30 files. We missed one on the second deployment. The client noticed their storefront had another company's phone number in the footer. That was the moment we decided to extract a product.

The Audit

Before refactoring, we did a full audit. We searched every file in the repository for strings that were specific to the first client. The results were worse than expected:

# What we found
grep -r "Great Garage Gear" --include="*.tsx" --include="*.ts" -l
# 14 files

grep -r "myshopify.com" --include="*.tsx" --include="*.ts" -l
# 6 files

grep -r "#1a365d" --include="*.css" --include="*.tsx" -l
# 8 files (the brand color)

grep -r "gear" --include="*.ts" -l
# 22 files (some false positives, but many were client-specific)

The store name appeared in page titles, meta descriptions, structured data, the header component, the footer component, the about page, the AI system prompt, the email templates, the 404 page, and the error boundary. Fourteen files total.

The brand color appeared in the Tailwind config, two CSS files, three inline styles, the theme-color meta tag, and the AI chat widget. Eight references.

The Shopify domain was in the data fetching layer (expected), but also in the checkout URL builder, the image URL rewriter, the sitemap generator, the robots.txt template, and a hardcoded canonical tag. Six references when there should have been one.

The Refactor

The refactor had one rule: every client-specific value must come from a single import. If a component needs the store name, it imports the config. If a CSS file needs the brand color, it uses a CSS variable that is generated from the config. No exceptions.

We did it in three passes.

Pass 1: Extract the Config Type

We defined the CartridgeConfig TypeScript interface (documented in our config-driven storefront post). Every field we found in the audit became a property on this type. The config file for the first client was populated with their existing values.

// cartridge.config.ts (first client)
import type { CartridgeConfig } from './lib/types';

const config: CartridgeConfig = {
  store: {
    name: 'Great Garage Gear',
    tagline: 'Premium Gear for the Modern Workshop',
    domain: 'https://greatgaragegear.com',
    shopifyDomain: 'great-garage-gear.myshopify.com',
  },
  brand: {
    primaryColor: '#1a365d',
    accentColor: '#ed8936',
    backgroundColor: '#ffffff',
    textColor: '#1a202c',
    fontHeading: 'Inter',
    fontBody: 'Inter',
    borderRadius: 'md',
  },
  // ... 200 more lines
};

Pass 2: Replace All References

Every hardcoded string was replaced with a config import. This was tedious but mechanical. The TypeScript compiler helped. When we changed a component to read from config.store.name instead of a string literal, any type errors in the config file surfaced immediately.

The CSS was trickier. We generated CSS custom properties from the config at build time:

// app/layout.tsx
import config from '@/lib/config';

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <style dangerouslySetInnerHTML={{ __html: `
          :root {
            --color-primary: ${config.brand.primaryColor};
            --color-accent: ${config.brand.accentColor};
            --color-bg: ${config.brand.backgroundColor};
            --color-text: ${config.brand.textColor};
            --font-heading: '${config.brand.fontHeading}', sans-serif;
            --font-body: '${config.brand.fontBody}', sans-serif;
          }
        `}} />
      </head>
      <body style={{ fontFamily: 'var(--font-body)', color: 'var(--color-text)', backgroundColor: 'var(--color-bg)' }}>
        {children}
      </body>
    </html>
  );
}

Every component that used the brand color now referenced var(--color-primary) instead of #1a365d. The Tailwind config was updated to pull from CSS variables. The entire visual identity of the storefront became a function of five config values.

Pass 3: Validation and Testing

We added Zod validation that runs at build time. The config is parsed and validated before the first page renders. Missing fields, invalid colors, broken URLs, all caught before deployment.

Then we created a second config file for a fictional store and deployed it. Different name, different colors, different products, different AI personality. Same codebase. Zero code changes. It worked on the first try.

What is Opinionated vs What is Configurable

This is the most important architectural decision. What goes in the config and what stays in the code?

Opinionated (in the code, not configurable):

  • The page layout structure (header, hero, collection grid, product detail, footer)
  • The animation system (Framer Motion, specific transitions, scroll-triggered animations)
  • The component architecture (React Server Components for data, Client Components for interaction)
  • The responsive breakpoints (mobile-first, 768px tablet, 1024px desktop)
  • The image optimization strategy (Shopify CDN with srcset)
  • The accessibility patterns (ARIA labels, keyboard navigation, focus management)
  • The performance budget (180KB JS maximum, sub-second LCP target)

Configurable (in the config file):

  • Brand identity (colors, fonts, logo, name)
  • Content (headlines, descriptions, policies, about page)
  • Features (which of the 11 features are enabled)
  • AI behavior (personality, tools, rate limits)
  • Navigation structure (links, dropdowns, CTA)
  • SEO defaults (title template, description, OG image)
  • Third-party integrations (analytics, marketing platform)

The line is drawn at design decisions vs content decisions. The grid layout is a design decision. It looks good. It is tested. It performs well. Making it configurable would let people break it. The headline text is a content decision. It should be whatever the merchant wants.

This is the difference between a platform and a template. A template gives you everything and says "change whatever you want." A platform gives you decisions and says "change what matters to you." The opinionated parts are a feature. They are why the storefront looks professional without a designer.

The CLI

Deploying a new storefront from the config needed to be fast. Not "fast for a developer" fast. Actually fast. We built a CLI that initializes a new project, validates the config, and deploys to Vercel in one command sequence.

$ npx create-cartridge my-store

  Creating Cartridge storefront: my-store
  Scaffolding project... done
  Installing dependencies... done

  Next steps:
  1. Edit cartridge.config.ts with your store details
  2. Run `npm run dev` to preview locally
  3. Run `npm run deploy` to deploy to Vercel

The create-cartridge command scaffolds a new project with the template code, a starter config file, and instructions. The merchant (or their developer) edits the config file. One file. Then they deploy.

The deploy process:

$ npm run deploy

  Validating config... ok
  Connecting to Shopify (great-garage-gear.myshopify.com)... ok
  Fetching product catalog (92 products)... ok
  Building storefront... ok (12.4s)
  Deploying to Vercel... ok

  Live at: https://my-store.vercel.app
  Custom domain: Configure at https://vercel.com/dashboard

Validation runs first. If the config is invalid, the deploy fails with a clear error message before any resources are consumed. Then it connects to Shopify to verify the domain is reachable and the public JSON endpoints are accessible. Then it builds. Then it deploys.

Total time from config edit to live site: under 2 minutes.

The Admin Dashboard

The config file is the source of truth. But not everyone wants to edit a TypeScript file. The admin dashboard is a web interface that reads the config, presents it as a form, and writes changes back to the config file.

It is not a CMS. It does not have a database. It is a UI layer on top of the file. When you change the store name in the dashboard, it writes config.store.name = "New Name" to the config file, commits the change to git, and triggers a redeploy. The git history shows exactly what changed, who changed it, and when.

The dashboard has four sections:

  • Brand: Colors, fonts, logo upload, border radius preview
  • Content: Hero, about page, shipping policy, return policy
  • Features: Toggle switches for all 11 features with previews
  • AI: Personality editor, greeting, suggested questions, tool configuration

Each section shows a live preview alongside the form. Change the primary color and the preview updates instantly. Toggle a feature off and the preview shows the component disappearing. The merchant sees the effect before they deploy.

Why "Platform" and Not "Template"

Templates are starting points. You fork them, customize them, and maintain them. When the template author releases an update, you manually merge it into your fork. Conflicts are inevitable. After six months, your fork has diverged so far from the template that updates are impossible.

Cartridge is not a template. The storefront code lives in a package. Your config file lives in your project. When we release an update (new feature, bug fix, performance improvement), you run npm update @cartridge/storefront and redeploy. Your config does not change. Your customizations do not conflict. The update just works.

// package.json
{
  "dependencies": {
    "@cartridge/storefront": "^1.0.0"
  }
}

// Your only file (besides config)
// app/layout.tsx
export { default } from '@cartridge/storefront/layout';
export { generateMetadata } from '@cartridge/storefront/metadata';

The separation is clean. We own the components, the animations, the data layer, the AI integration, and the performance optimizations. You own the config. Updates to our code do not touch your config. Changes to your config do not require our code to change.

This is the architecture of a platform. Not a theme. Not a template. Not a boilerplate. A platform that accepts a configuration and produces a storefront.

The Numbers

Since extracting the platform from the first client project:

  • Time to deploy a new store: Dropped from 2 weeks (fork, customize, test, deploy) to under 1 hour (edit config, deploy)
  • Config file size: 200-400 lines depending on content length
  • Codebase files the merchant needs to touch: 1 (the config)
  • Lighthouse Performance score: 96-100 across all deployments
  • Total platform code: ~18,000 lines of TypeScript across 140 files
  • Config surface area: 47 configurable fields

We went from "let me fork the repo and find-and-replace 50 values across 30 files" to "let me fill out one config file." That is the refactor. That is the product.

Next week, we are making it available to everyone.