How we build a Shopify alternative
Shopify is genuinely good. The admin is polished, the ecosystem is huge, and for a standard catalog you can be live in a week. Most stores should use it.
This post is about the other ones — the ones where Shopify stops being a platform and starts being a tax. Configurable products that don't fit a SKU-per-variant model. Deposits and custom enquiries the checkout wasn't designed for. App stacks that quietly cross $1,000/month. Theme hacks layered on theme hacks because the design ambition outran the template.
For a recent build in that bucket we went custom — Cloudflare on the edge, Postgres behind a Hono API, an admin the client owns outright. Here's how it's put together, what it actually costs, and the parts that are genuinely hard. I'll start with where Shopify is still the right answer, because it usually is.
When Shopify is still the right call
Before any of the architecture stuff, the honest filter:
- You're starting from zero with no dev team. Shopify will get you to a real store this week. A custom build is a project measured in weeks-to-months.
- Your business depends on a third-party app. Klaviyo flows, Recharge subscriptions, Loox UGC, a tax compliance app for 30 jurisdictions. If the app is the business, stay where the apps live.
- Your catalog is huge and standard. 10,000 SKUs that fit a normal product/variant model — Shopify's tooling is hard to beat.
- You need point-of-sale and online inventory unified out of the box. Shopify POS is a real product. Replacing it is a separate project.
If two or more of those describe you, close this tab and go pick a Shopify theme.
If none of them do — and especially if your product is configurable, your operations are bespoke, or you've already hit the ceiling of "everything is an app" — keep reading.
What the site has to do
Strip away the buzzwords and the requirements were boring:
- A product catalog with collections, variants, and a pricing matrix (different materials × different sizes → different prices).
- A cart and checkout that takes either a full payment or a deposit, calculates shipping by postcode, and confirms the order on payment success.
- A custom-enquiry path for items that don't fit the standard catalog.
- An admin where the client can edit products, prices, content, and images, and watch orders come in.
- Transactional email for order confirmations, status updates, and enquiry receipts.
- Lighthouse 90+ on content pages, 80+ on interactive product pages, WCAG 2.1 AA, and SEO that doesn't fight the rest of the stack.
That list looks like Shopify. It's also somewhere in the order of a few thousand lines of code per concern when you build it directly — cart + checkout + tax + shipping + webhook reconciliation + admin + auth adds up fast. The win isn't that it's small. The win is that every line is yours.
The single most important thing in the build
Skip everything else and read this paragraph.
Catalog data and order data are not the same shape, and the moment a customer pays you commit to that distinction:
- The catalog is live, mutable, edited every week. Prices change, names get tweaked, SKUs get retired.
- An order is a snapshot. The price the customer paid, the size label they saw, the material name as it read on the day — all frozen at purchase time.
We model this with a product_configuration row (product × material × size → price, available, enquiry-only flag) that the cart references by id and the order copies by value. Future catalog edits never rewrite history. Refunds, disputes, and tax reports all keep working a year later.
This sounds obvious. It is the thing Shopify's metafield-and-app world makes structurally hard the moment your variants stop being one-dimensional, and it's the reason we keep ending up in custom builds.
The stack
| Layer | What we used |
|---|---|
| Frontend | Astro 6 + React 19 islands |
| API | Hono on Cloudflare Workers |
| Database | Neon PostgreSQL (serverless) via Drizzle ORM |
| Auth | Better Auth (email + sessions, same apex domain) |
| Payments | Stripe (Checkout Sessions + Webhooks) |
| Resend + React Email templates | |
| Image storage | Cloudflare R2 |
| Styling | Tailwind CSS v4, shadcn/ui |
| Hosting | Cloudflare Pages (frontend) + Workers (API) |
Two deployable units: a static-shell site on Pages, an API on Workers. They share types and a Drizzle schema through a small shared package, so there's no drift between what the API returns and what the frontend expects.
Note on this site vs the post. The OwnStack marketing site you're reading uses a slightly different shape — Astro 6 SSR on Cloudflare Workers with D1 (SQLite) instead of Neon. Same family, same edge runtime, smaller surface for a content site. We pick D1 vs Neon per project based on whether the workload is "mostly reads of slow-changing content" or "ecommerce with concurrent writes."
How the pieces fit together
The site is statically rendered for shell, dynamically hydrated for state. Specifically:
- Astro builds the storefront shell as static HTML — layouts, copy, images, SEO, the parts that don't change between requests. The shell ships pre-rendered with no JS for the page render itself.
- Stock and price come from the API at request time, not from the build. A small island on each product page hydrates the live
product_configurationrows. Build-time would mean a full rebuild on every stock change — fine for content, fatal for ecommerce. - React islands (
client:load/client:visible) handle the interactive bits — the pricing selector, cart, gallery, checkout form, enquiry form. State lives in a couple of Nanostores so islands on different parts of the page stay in sync. - The Hono API on a Worker handles everything dynamic: live catalog reads, checkout sessions, Stripe webhooks, enquiry submissions, shipping quotes, and the entire admin CRUD surface.
- Postgres on Neon holds the catalog, orders, pricing, content, and settings. Drizzle ORM gives us typed queries shared between the API and the build-time shell-data fetchers.
- Stripe owns payments. Checkout Sessions for the customer-facing flow, webhooks back to the Worker to actually create the order on payment success.
- R2 stores product imagery, with the public bucket served behind a CDN URL.
- Resend sends transactional emails using React Email templates that live alongside the API code.
- The admin is a React SPA mounted at
/adminon the same apex domain as the storefront, so session cookies work without cross-origin gymnastics. It talks to the same Hono API. When the client publishes a content change, the API hits a Cloudflare deploy hook and the static shell rebuilds.
Here's the full picture — actors on the left, build pipeline and Cloudflare Pages in the middle, the API worker and external services on the right:
The parts that are actually hard
Anyone who's shipped this kind of stack will tell you the architecture diagram is the easy part. The hard parts are below the line, and they're the reason Shopify earns its rent.
Stripe webhook reliability
The single most likely place this stack breaks is the gap between "Stripe says payment succeeded" and "we have an order row." Three things have to be true:
- Idempotency. Stripe retries webhooks aggressively. We key every order by
checkout_session_idwith a unique constraint, so a retried webhook is a no-op rather than a duplicate order. - Race with the success redirect. The customer hits
/order/successbefore the webhook lands maybe 10% of the time. The success page polls the API for the order; the API returns "pending" until the webhook commits. No "order confirmed" email until the webhook says so. - Cold Postgres. Neon's serverless tier sleeps. The first webhook after idle adds 200–800ms of cold-start. Stripe's retry budget absorbs this fine, but you have to know it's there. For higher-volume stores we run Neon's non-sleeping compute and the cold-start disappears.
Auth across two services
Better Auth on the Worker, sessions in a cookie, admin SPA on the same apex. We deploy admin under /admin on the storefront domain, not a separate admin. subdomain, so the session cookie is first-party and SameSite=Lax just works. If you split the domains you're suddenly debating SameSite=None, secure-context cookies, and CORS preflights for every admin call. Don't.
Lighthouse on actual product pages
The "near-100 Lighthouse" thing you see on stack-marketing posts is a content-page number. A real product page with a gallery, pricing selector, cart drawer, and an analytics script lands more like 88–94 on a mid-tier Android over 4G. We chase the 90 floor on every page and screenshot it on every release; we don't chase 100 on interactive pages because the cost (no images, no analytics, no Stripe.js) isn't worth the score.
Stale-stock window
Even with live API hydration, there's still a few-hundred-millisecond window where a customer can add the last unit to their cart while another customer is checking out. Stripe Checkout's inventory hold and a final stock check inside the webhook close most of it; a backorder flow handles the rest. Anyone telling you they've eliminated this window is lying or single-tenant.
Observability
Wrangler tail, Cloudflare Logs, Sentry on both the Worker and the SPA, and a synthetic checkout that runs every 15 minutes hitting Stripe test mode. This is a real line item — see the cost section.
The admin is the boring part — and that's the point
The admin CMS gives the client:
- Products, collections, materials, sizes, pricing matrix — full CRUD.
- Order list with status updates that trigger customer emails.
- Enquiries with a status pipeline (new → in review → quoted → converted).
- Content editing for the homepage, about page, and custom-build process page.
- Image upload to R2 with automatic resizing.
- A "publish" button that triggers a redeploy of the static shell.
What it doesn't have: a plugin marketplace, a theme editor, a drag-and-drop page builder, an app store. None of those are missing features — they're absences by design. Every one of those would be a future maintenance bill the client doesn't need.
What this stack actually costs
The honest breakdown — including the bit most stack-marketing posts skip, which is what happens once you have actual traffic and someone has to keep the lights on.
At low traffic: $0/month.
That's not a marketing line, it's just what the bill looks like. At a few hundred orders a month with a small catalog, every component sits inside its free tier:
| Line | Monthly at low traffic |
|---|---|
| Cloudflare Pages | $0 |
| Cloudflare Workers (100k req/day free) | $0 |
| Neon Postgres (free branch) | $0 |
| R2 storage + egress (10 GB / 1M ops free) | $0 |
| Resend (3k emails/mo free) | $0 |
| Domain (amortised) | ~$1 |
| Platform subtotal | ~$0–1 |
That's the line item Shopify can't match. There is no entry-level price floor — the platform cost only starts mattering once the store does.
At small-to-mid traffic (a few thousand orders a month, image catalog growing, monitoring you actually look at):
| Line | Monthly |
|---|---|
| Cloudflare Workers (paid plan) | $5 |
| Neon Postgres (production tier, non-sleeping) | $25–69 |
| R2 storage + egress | $1–5 |
| Resend | $0–20 |
| Sentry / error monitoring | $0–26 |
| Uptime + synthetic checkout monitoring | ~$10–20 |
| Domain | ~$2 |
| Platform + ops subtotal | ~$45–150 |
Stripe processing fees sit on top, same as they would on any platform.
Now the comparison that actually matters — Shopify with the apps a real store uses:
| Line | Monthly |
|---|---|
| Shopify Basic / Shopify | $39–105 |
| Klaviyo (email + SMS, ~10k contacts) | ~$150 |
| Recharge (subscriptions) | $99 + 1.25% |
| Loox or Yotpo (reviews / UGC) | $35–100 |
| A bespoke-products app (Bold, Infinite, etc.) | $30–80 |
| Shipping rules / postcode app | $20–40 |
| Realistic Shopify stack | ~$370–600/mo |
Plus Shopify's transaction fee on top of Stripe if you don't use Shopify Payments, plus theme dev hours when something needs to change. And Shopify charges from day one — there's no $0 floor while you're finding your feet.
So: roughly $0/mo at launch, $45–150/mo at real traffic, vs. $370–600/mo Shopify-with-apps from the moment you switch the lights on. The savings are real, but they aren't where the build pays for itself.
A custom build like this used to land somewhere between $30–80k depending on scope — which is why most stores stayed on Shopify and paid the app tax instead. With AI-accelerated development, the same build now starts from $1,500 and scales with scope. At $400/mo of platform savings, even the upper end pays itself back inside a year on platform cost alone — but that's still not where the real return comes from:
- Apps you no longer need to bend the business around — variant models, deposit flows, postcode-shipping logic that just gets coded.
- Revenue from things you couldn't ship on Shopify — the configurable product page that converts because it's not a hacked theme, the enquiry-to-quote pipeline that captures the long tail.
- Design and CRO velocity — a new landing page is a sprint, not a re-platform.
If a custom build moves conversion 0.5–1%, payback drops to weeks. That's the comparison to run, not the platform line.
What "owning the stack" actually buys you
The line we keep coming back to: the client owns the code, the database, the bucket, the domain, and the deploy pipeline. No vendor can deprecate a feature, raise prices, or hold the store hostage. Adding a new payment flow is a feature, not an app subscription. Redesigning is a sprint, not a re-platform.
A note on the "lift and shift to AWS in an afternoon" claim that stack posts love to make — it's not true. Cloudflare Workers, R2, Pages, and Neon all have specific runtime and API shapes. Migrating to AWS or GCP is a real sprint, possibly two, and you'd want a reason. The actual portability story is more honest and more valuable: it's standard Postgres, standard Node-compatible JavaScript, standard SQL, standard Stripe. No proprietary BaaS, no platform-locked schema, no ecosystem you can't leave. You can move it. You probably won't need to.
That ownership compounds. Every site we ship on this stack stays maintainable for years, because there's nothing in it that we didn't put there on purpose.
The bottom line
Most stores hitting Shopify's ceiling could ship a custom build in a few weeks for the cost of a few months of their current app stack — and walk away with a product they own, a conversion ceiling they set, and an operational footprint they understand line by line.
If that's you, get in touch. The first conversation is free; we'll tell you honestly if you're a Shopify store after all.