← work / ca-world-service-shop

Nothing Is Ever Overwritten

A from-scratch Shopify Online Store 2.0 theme for a recovery fellowship's World Service shop — literature, keytags, medallions and cards, sold to members and groups around the world. The deploy process is built so a release can never quietly overwrite the live storefront, and a rollback is always exactly one command away.

LIVE In production at shop.ca.org.
The C.A. World Service Shop home page: a hero reading 'Literature, keytags, medallions and more from the World Service Office' above a grid of featured products including Specialty Medallions, Newcomer Keytags, English Pamphlets, and the Twelve-Step Companion Guide
Project type
Shopify Online Store 2.0 theme — built from scratch, not a purchased template
Role
Theme engineer — architecture, Liquid development, build tooling, deploy safety
Live store
shop.ca.org
Status
Live in production, actively maintained
Source
Public repository on GitHub
Foundation
Started from Shopify's Skeleton Theme; most of it has since been rebuilt

// 01 — The constraint that shaped everything

A theme is live the second it's published. The safety has to be built in, not practiced.

This is the storefront where C.A. groups and members order literature, keytags, and medallions — the physical materials a meeting hands someone on day one. There's no staging server standing between a theme change and the person placing that order; whatever gets published is what they see.

That reframes how a deploy has to work. A mistake can't just be unlikely — it has to be either structurally impossible or trivially reversible, because there's no dedicated operations team watching for it. And because the fellowship this shop serves is built on anonymity, that principle has to show up in the storefront itself: nothing requires an account to buy, and nothing tracks more than it needs to.

ConstraintWhat it forced
A theme change is live for every shopper the instant it's publishedThere's no staging environment unless the deploy process builds one in — a bad push doesn't wait for a review cycle to cause a problem
Hosted entirely inside Shopify's platformCheckout, payment processing, and PCI compliance are Shopify's responsibility by design — this project owns everything a shopper sees and clicks before that point, and nothing after it
Built for a fellowship whose core principle is anonymityBuying literature or a keytag shouldn't require an account or leave an unnecessary trail — whatever the storefront can render without identifying a shopper, it does
Volunteer-maintained, no dedicated operations teamA deploy mistake has to be either structurally impossible to make or trivial to undo — there's no on-call rotation to catch it at 2am

// 02 — What this work covers

  • → Built the theme from Shopify's Skeleton Theme starting point, replacing nearly all of it — only the base section-grid CSS and the overall layout shape remain from the original.
  • → Set up Tailwind CSS v4 and Vite as the build pipeline, with the compiled bundle committed to git, since Shopify can only ever serve flat files out of a theme's assets folder.
  • → Wrote a safety wrapper around the Shopify CLI that structurally refuses any command capable of publishing, overwriting, or deleting the live theme — checked against the store's actual theme list by id and by name, not by trusting whichever flag was typed.
  • → Designed the deploy model: every release ships as a new, unpublished theme, reviewed on a shareable preview link, and published only as a separate, deliberate step.
  • → Built the color and font theming system so a section's settings cascade to every snippet and block inside it through CSS custom properties, with no component needing to know which section it's rendered inside.
  • → Implemented quantity-stepper quick-order rows for multi-language product families — keytags, specialty medallions — replacing a single nested variant dropdown with an expandable per-language list a shopper can add from directly.
  • → Wired the cookie-consent banner to Shopify's Customer Privacy API so it only appears where consent is actually required, coordinated with the store's own privacy settings so there's never a second, duplicate banner.
  • → Set up theme-check:recommended linting and a one-way live-data pull workflow, so local development renders the real, live home page and settings without risking an accidental overwrite of uncommitted work.

// 03 — The approach

Six stages. Every one of them makes a mistake harder to make.

None of this needed exotic tooling — Shopify's own CLI does the actual work. The engineering is in what the deploy process refuses to let happen by accident.

  1. 01

    Start from Skeleton, keep what earns its place.

    Shopify's own Skeleton Theme supplied the starting shape — the section grid, the overall layout. Almost everything on top of it was rebuilt from there: every section, block, and snippet, and the entire styling approach, swapped for Tailwind CSS v4 compiled through Vite.

  2. 02

    Compile client-side, ship flat files.

    Shopify can only serve static assets out of a theme's assets folder, so the Tailwind bundle and the JavaScript entry point are built locally with Vite, and the compiled theme.css / theme.js are committed to git. The actual source lives in src/, deliberately excluded from what gets pushed to the store.

  3. 03

    Push to a new theme. Never the live one.

    A release is never a bare push against whatever happens to be published. It goes up as a new, named, unpublished theme, gets previewed on a shareable link, and is only published as a separate, deliberate step — which means the theme that was live a minute earlier is still sitting in the theme library, unpublished and intact.

  4. 04

    A CLI wrapper that argues back.

    Every deploy command runs through a thin wrapper script instead of the bare Shopify CLI. It inspects the actual arguments, including short flags and the CLI's own environment variables, and refuses anything that would publish, overwrite, or delete whichever theme the store currently has live.

  5. 05

    Local development renders the real site, safely.

    A separate script pulls the live theme's JSON — home page layout, theme settings, section groups — into the working tree, so local development matches what shoppers actually see. It refuses to run over uncommitted local changes, so a pull can never quietly eat work that hasn't been committed yet.

  6. 06

    Content stays in the admin. Structure stays in git.

    Products, pages, blog posts, and navigation live in Shopify's admin and were never theme data to begin with. Only layout, section configuration, and settings live in this repository — which is also exactly what the one-way sync has to protect, since nothing else needs protecting.

A release, start to finish
main branch, reviewed and clean
      │
      ▼
npm run build ──► minified theme.css + theme.js, committed
      │
      ▼
deploy wrapper: push --unpublished --theme "CAWSO <date>"
      │           (refuses --live, --publish, or no explicit target)
      ▼
new UNPUBLISHED theme, previewed via a shareable link
      │
      ▼
settings & home page arranged in the theme editor ──► pulled back into git
      │
      ▼
shopify theme publish ──► now live; the old theme drops back into
      │                     the library, unpublished — not deleted
      ▼
rollback, if ever needed: publish that old theme — one command, seconds

A wrapper that refuses the dangerous half of the CLI

The Shopify CLI is perfectly capable of overwriting or deleting whatever's currently live — that's a feature, not a bug, for the one person who means to do it on purpose. Every deploy in this project runs through a wrapper that passes read-only commands straight through and refuses the rest outright.

RefusedWhy
push --live, push --allow-liveOverwrites the published storefront in place
push --publishPublishes whatever was just pushed, immediately
push with no --theme / --unpublished / --developmentLets the CLI pick a target instead of a human choosing one
push --theme <live>, delete --theme <live>Checked against the store's real theme list, by id and by name
publishPublishing is treated as a deliberate, admin-side act, not a script's to make

Short flags and the CLI's own environment variables are read the same way, so there's no spelling of the same intent that slips past. It's a guard rail, not a lock — the bare CLI run directly still does whatever it's told, which is the deliberate escape hatch for the one deploy script actually allowed to publish.

Content and code don't live in the same place, with one exception

A one-way sync from the live store to git only works if it's obvious what each side owns. Products, pages, and orders were never theme data, so they're never at risk from a pull. The one overlap — home page layout and theme settings — is also the one thing that has to be pulled back deliberately after anyone touches the theme editor.

Lives inHolds
Shopify adminProducts, collections, pages, blog posts, navigation, customer accounts and orders
This repositorySections, blocks, snippets, layout, the global settings schema, and the compiled CSS/JS bundle
Two JSON files, specificallyHome page layout and theme settings — the one place content and code overlap, since they're also editable live in the theme editor and have to be pulled back deliberately or editor changes vanish on the next push

The storefront, in practice

The full home page, the keytag catalog's language-aware quick-order rows, and the cart with its 7th Tradition donation prompt. Click any of them to see the full page.

// 04 — Technical insights

Color and font settings scoped to the section, not the page

A merchant's color settings become CSS custom properties on that section's own root element, and every snippet and block inside reads from there. One section can theme everything nested inside it — including reusable components it shares with every other section — without any of those components needing to know which section they happen to be rendered in. No prop drilling through Liquid includes, no JavaScript reading a global theme object: just the CSS cascade, used the way it was designed to be used.

An app block that survives its own cart re-rendering

The cart page re-renders part of itself in place whenever a quantity changes, without a full page reload. Merchant-installed app blocks are allowed to sit inside the cart, but they're placed deliberately outside the specific region that script re-renders — otherwise a third-party widget would get torn down and remounted every time a shopper clicked a quantity stepper, losing whatever state it was holding.

Quick-order rows instead of a nested variant dropdown

Keytags and specialty medallions come in parallel language editions — English, French, Spanish, Swedish, and more — which Shopify's default variant picker would collapse into a dropdown menu per product. Here, a product like Newcomer Keytags expands in place into a listed row per language, each with its own quantity stepper and its own Add button, so ordering nine languages at once is nine clicks on one page instead of nine separate product visits.

One cookie banner, decided by settings, not guesswork

The consent banner runs on Shopify's own Customer Privacy API and only shows itself where consent is actually required and the store's own privacy settings ask for it. Shopify ships its own cookie banner too, so getting this wrong doesn't fail safe — it fails as two banners stacked on top of each other, arguing about the same thing.

// 05 — Results

A deploy that can't quietly break the thing people rely on.

MeasureResult
Theme lintPasses theme-check:recommended with zero offenses
Deploy modelEvery release is a new, unpublished theme; publishing is a separate, deliberate step
Rollback timeOne command — republish the previous theme, still sitting unpublished in the theme library
Accidental live overwriteStructurally blocked — the deploy wrapper checks the store's real theme list before allowing push, delete, or publish, not just the flags typed
Checkout & paymentsEntirely Shopify's — PCI compliance, tax, and payment processing sit outside this theme's scope by design
Account required to buyNo — guest checkout is a first-class path, not an afterthought

// 06 — What this project reinforced

A safe deploy is a property of the tooling, not a habit of the person running it.

A deploy safety net should check reality, not the flag someone typed.

It would have been easy to block a short list of dangerous-looking flags and call it done. Instead the wrapper asks the store itself which theme is live — by id and by name — before deciding whether a command is safe, which also catches the CLI's own environment variables and any short-flag spelling of the same intent.

Reversible beats careful.

Trying to get every release perfect before it ships is a losing bet. Shipping every release as a new, unpublished theme means the bet doesn't matter — the previous theme is still sitting in the library, unpublished, and rolling back is one command instead of a recovery plan.

If a tool can only read, build it so that's structurally true.

The live-data pull script only ever calls the Shopify CLI's read command. The safety isn't a warning in a README asking a developer to be careful — it's that the script has no code path capable of writing to the store, full stop.

A one-way sync needs a dirty-tree check, or it isn't safe.

Pulling live theme settings into a working tree that already has uncommitted local changes is exactly how a pull quietly eats work nobody meant to lose. The pull refuses to run until that tree is clean, with an explicit override for the rare time that's actually the point.

Scope color and font settings to the section, not the page.

Merchant color settings become CSS custom properties on a section's own root element, so one section can theme every snippet and block inside it without any of them needing to know which section they're sitting in. No prop drilling, no JavaScript context — just the cascade, used on purpose.

Pick one font for one job and never negotiate it.

The primary typeface is merchant-configurable through Shopify's font picker. Every price on the site is not: it renders in a single self-hosted serif regardless of what the merchant picks for everything else, because money is the one piece of text on a storefront that benefits from never changing its voice.

A second cookie banner is worse than none.

The consent banner reads Shopify's own Customer Privacy API and the store's privacy settings before deciding whether to show itself at all. Getting that coordination wrong doesn't mean no banner — it means two different banners arguing with each other, which is a worse outcome than either one alone.

Commit the build output when the platform can't build it for you.

Shopify has no build step of its own — it serves whatever flat files are in the theme's assets folder. That makes the compiled CSS and JS a tracked, reviewable part of the repository rather than a generated artifact, and it makes shipping the unminified development bundle by mistake the single easiest way to ship something wrong.

// Technologies used

LayerTechnologyWhy it was chosen
TemplatingShopify Liquid — Online Store 2.0 sections, blocks, and JSON templatesSections and blocks give merchants a configurable storefront without touching code; JSON templates keep page composition declarative
StylingTailwind CSS v4Utility classes in the markup for everything expressible that way; a small per-file stylesheet block for the rest — a ::backdrop, a :has() selector, rich-text link styling
Build toolingVite, with compiled output committed to gitShopify only serves static files from a theme's assets folder, so the bundle has to be built locally and tracked, not generated at request time
Deploy safetyCustom Node scripts wrapping the Shopify CLIStructural refusal of any command that could publish, overwrite, or delete the live theme, rather than a documented warning asking developers to be careful
LintingShopify Theme Check — theme-check:recommendedCatches Liquid and theme-architecture mistakes before they reach a push, not after
PrivacyShopify Customer Privacy APIDrives the cookie-consent banner so it only appears where required and never duplicates the platform's own banner
FontsSelf-hosted Newsreader for prices and serif display, merchant-configurable primary typeface otherwiseEvery monetary figure on the site renders in one deliberate typeface, independent of whatever font a merchant picks for the rest of the theme
Hosting, checkout & paymentsShopifyPCI compliance, tax, and payment processing are the platform's problem by design — out of scope for the theme entirely
Visit the shop ↗ Source code ↗ More work →
Next case study → Ask the Book, Not the Internet A book-agnostic RAG system with hybrid retrieval and citation-level grounding.