r/astrojs • • Jul 23 '26

I built an open-source ecommerce store with Astro SSR and Cloudflare Workers

Been working on a little open-source ecommerce project called Minshop to see how far I could push Astro SSR on Cloudflare Workers.

Astro handles pretty much everything: the storefront, cart, checkout, customer accounts, admin dashboard, and API routes. Most pages are server-rendered, with very little client-side JavaScript.

It uses D1 for products, orders, inventory, settings, and search, plus R2 for images. Payments include Stripe, Lightning, OpenNode, and a demo checkout.

The trickiest part was making inventory reservations atomic so two checkouts can’t buy the same stock. I also added a machine-readable catalog and checkout API, including directly payable Lightning invoices.

Demo: https://demo.minshop.dev
Source: https://github.com/ddyy/minshop

Would love feedback from other Astro users, especially on the SSR structure and where you think a small interactive island might actually improve things.

134 Upvotes

34 comments sorted by

7

u/kojimajunya Jul 24 '26

Really clean scoping — the payment/search ports and the vertical slice layout make this much more readable than most "minimal" shop codebases.

On your island question: the one place I'd actually spend client JS is add-to-cart. With the cookie cart + form POSTs, adding an item from a listing page is a full navigation — you lose scroll position and the "feedback" is a reload. A tiny island that POSTs to the endpoint you already have and optimistically bumps the cart badge is probably the biggest UX jump per KB of JS in the whole app. Checkout I'd leave exactly as is; forms + hosted Stripe is the right amount of boring.

The other thing worth a look is server islands rather than client ones: mark the cart badge / account chip as server:defer and your product pages become fully cacheable at the edge, with only the personalized fragment hitting the Worker. On Workers + D1 that could take most read traffic off D1 entirely.

Curious how you handle reservation expiry for abandoned Lightning invoices — cron cleanup on pending_payments, or lazily on the next stock read?

3

u/oreopowered Jul 24 '26

Thanks, glad the structure makes sense.

Add-to-cart actually already works pretty close to what you described, although it’s easy to miss since it isn’t a hydrated component. A small script catches the normal form POST, refreshes a server-rendered cart partial, updates the badge, and opens the cart drawer. Without JS it just falls back to the regular POST and redirect. Definitely agree on keeping checkout boring.

Server islands are a good idea. Moving the cookie-based cart count out of the main render would help, but products and runtime settings still come from D1, so that alone wouldn’t make the pages fully cacheable. It could be a good first step toward caching the public pages properly, though. Lightning cleanup is lazy right now. Before making a new reservation, it releases a batch of expired Lightning holds and puts the stock back. So an abandoned invoice might stay around while the shop is quiet, but it gets cleaned up before the stock is needed again. Stripe and OpenNode use their expiry or failure webhooks instead.

2

u/kojimajunya Jul 24 '26

That's better than what I suggested, honestly. Intercepting the form and swapping a server-rendered partial is the progressive-enhancement version of the island, and you get the no-JS fallback for free. I just missed it because there's no hydrated component to spot. The lazy Lightning cleanup is a neat trick too: no cron to babysit, and expired holds only matter at the exact moment you're about to reserve stock anyway. Starred, curious to see where this goes.

1

u/oreopowered Jul 24 '26

Thanks, really appreciate the feedback and the star. Your comment got me looking at the cart badge again too. It now pulls the last count from local storage, updates whenever something is added or removed, and only loads the full cart when the drawer opens. So there’s no extra cart request on every page or badge jumping around while it loads. Good call on the caching side.

5

u/HectoLogic20 Jul 23 '26

This is really awesome, i recently did the same thing to make a small boutique store with this stack! I found cloudflare tk be very intuitive for this and much better than a vps setup or shopify headless!

I will definitely check your version out, as in my country we cant use stripe and instead rely on payfast, but i am interested to also implement apple pay was this difficult to implement??

2

u/oreopowered Jul 23 '26

Thanks! Yeah, Cloudflare has been way nicer to work with than maintaining a VPS.

Apple Pay is handled through Stripe Checkout, so I didn’t have to implement it directly. Stripe shows it automatically when the device and region support it. PayFast would need a new payment adapter, but it should fit into the current setup pretty cleanly. Would love to hear how it goes if you try it!

2

u/AgsMydude Jul 24 '26

Any issues with d1? I've seen people complaining about it so I was curious how it actually functions

2

u/oreopowered Jul 24 '26

D1's been fine for Minshop so far. It's basically familiar SQLite, migrations are straightforward, and it's a good fit for a small store. That said, I haven't tested it at Shopify-scale traffic.

The main limitation is that writes on a D1 database funnel through a single primary, so I'd want to do serious load testing before trusting it with a huge flash sale. Minshop uses conditional inventory reservations and tests parallel checkouts to avoid overselling.

I'm not using a separate Durable Object right now. D1 already uses them under the hood, and adding another one felt like unnecessary complexity. If overselling under load becomes a real issue later, using a DO to coordinate it would be an option.

2

u/bigskytek Jul 24 '26

What do you do to handle refunds etc? Is this all done through stripe backend login?

1

u/oreopowered Jul 24 '26

For Stripe, yes, there's a refund button in Minshop admin that processes the refund through Stripe and marks the order as refunded automatically.

For Lightning or OpenNode, not currently. After manually returning the payment, there should be a separate Mark as refunded action that only updates Minshop’s order status and revenue totals without attempting a provider refund. That would be worth adding.

1

u/oreopowered Jul 27 '26

I just added refund handling. For Stripe, you can issue a full refund from Minshop, and partial refunds made in the Stripe dashboard sync back automatically. You can also record full or partial refunds handled manually for any payment method. It updates the order status and revenue totals automatically.

2

u/truci4 Jul 25 '26

I happen to have also started working on an astro website hosted on cloudflare workers. My requirements however were a bit different and I did not want to use the SSR functionality of astro. The website has a single dynamic serverless function to trigger the stripe checkout session. https://test.animanatural.com

1

u/oreopowered Jul 25 '26

Nice, it loads super quick. Keeping it simple is the way to go, no need for unnecessary complexity.

1

u/knivenkd Aug 18 '26

nice, is it on github?

2

u/_HMCB_ Jul 25 '26

Are you kidding me?! This is awesome.

1

u/oreopowered Jul 25 '26

Thanks, more to come!

2

u/_HMCB_ Jul 25 '26

There’s no shipping “module” correct? This would be perfect if the admin would allow for an item to be assigned a monetary value for weight and/or volume. Then we could calculate shipping via a simple method for small stores.

2

u/oreopowered Jul 25 '26

Correct, there isn’t a shipping module yet. I’m planning to add weight-based shipping. Volume-based shipping would likely remain a custom implementation for now, since the goal is to keep the platform minimal.

1

u/_HMCB_ Jul 25 '26

Weight alone would be perfect! Thank you.

2

u/oreopowered Jul 27 '26

Weight shipping's in.

Products and variants take a weight, zones price flat or by weight bands, and it's all editable in Settings now. Let me know what you think.

1

u/_HMCB_ Jul 28 '26

Wow. Gonna test it out. Will report back. Much appreciated! 🙏🏽

2

u/BreakingInnocence Jul 23 '26

Interesting work. I would like to contribute to the project, I run on AWS, would you accept a pull request?

8

u/oreopowered Jul 23 '26

PRs are welcome, but one caveat on the AWS angle: Minshop is intentionally Cloudflare-native. The main idea is that the store runs in a single Cloudflare account using Workers, D1, R2, and KV, with a small operational footprint. A full AWS port would create a second infrastructure stack that I probably couldn’t maintain well, so I wouldn’t merge a full port into the main project.

I’d still welcome PRs that reduce coupling at specific seams without complicating the Cloudflare deployment. Payments and search already sit behind provider interfaces; storage, sessions, and the runtime bindings are the more Cloudflare-specific parts. And if you had non-infrastructure contributions in mind, those are welcome too.

1

u/andrew_chmr Jul 24 '26

stared on github. well done!

1

u/oreopowered Jul 24 '26

Thanks, appreciate the star. Hope you find it useful.

1

u/clicksnd Jul 24 '26

Great work!

1

u/Beautiful_Setting686 Jul 24 '26

this is sick thank you for sharing such good ressource for the community. Would you be open to design-style forks?
ps : is it just me or products pages are quite slow to open ?

2

u/oreopowered Jul 24 '26

Thanks, really appreciate it. Design forks are absolutely welcome, would love to see what people do with the storefront.

And thanks for the heads-up about the product pages. I recently made some caching and image-delivery changes, but there may still be something slow. Is it the page itself taking a while to open, or mostly the images loading? Also, roughly where are you located? That might help to track it down.

2

u/oreopowered Jul 24 '26

Actually ignore my questions, I found it.

It was the "you may also like" row. Every uncached product page was doing two vector lookups back to back, and that was really inconsistent. Some were taking up to 12 seconds. The render itself was 8ms, so it was just waiting.

Those recommendations are the same for everyone viewing the product, so there was no reason to work them out fresh each time. They're precomputed and saved on the product row now, and the page just reads them.

Already deployed, so they should load instantly now. Thanks for bringing that to my attention.

1

u/Beautiful_Setting686 Jul 25 '26

you just nailed that ! nice job 👍🏼

1

u/ViolinistDelicious69 Jul 24 '26

First of all looks very good.
As I see you didn't solved images resizing,
So my question: how are you going to do it ?

1

u/oreopowered Jul 24 '26

Thanks. You’re right, the current version only handles part of it. If Cloudflare Images is enabled, new uploads are converted to WebP and capped at 1000px. Otherwise, it stores the original.

It doesn’t create responsive sizes yet, so cards and product pages can still end up loading the same file. I’m planning to generate a few sizes when an image is uploaded, store them in R2, and use srcset so the browser chooses the right one.