An e-commerce storefront
Catalog, cart and Stripe checkout, with the product data owned by your app instead of the payment processor.
The problem
The tempting shortcut is to let Stripe hold the products. It works until you need a category page, a search filter, stock, or a price that depends on who is looking — none of which the payment processor knows about. Now every page load is an API call, and the catalog you actually query is a cache you have to keep honest.
The other half is the storefront itself. A product grid, a cart and a checkout are not hard, but they are the surfaces customers judge you on, and they are usually where a project quietly stops using its design system.
What you are building
Live surfaces, not screenshots — every one composed from the same components you would install. The catalog lives in your database and syncs outward to Stripe. The storefront is composed from react-fancy, so it inherits the same tokens as the rest of the app.
Storefront
A product grid over your own catalog — prices live in your database, synced to Stripe.
Cart and checkout
The checkout session comes from the catalog facade, so no Stripe-specific code reaches your controllers.
Checkout session created from your own catalog
The code
// Products are local models, so the storefront filters, sorts and paginates in
// SQL -- no network call on page load, and no second copy of the prices.
public function index(Request $request): Response
{
return Inertia::render('Shop/Index', [
'products' => Product::query()
->where('active', true)
->when($request->string('q')->toString(), fn ($q, $term) =>
$q->where('name', 'like', "%{$term}%"))
->with('prices')
->paginate(24),
]);
}use LaravelCatalog\Facades\Catalog;
// The same facade covers both modes, so the cart carries no Stripe-specific
// branching into your controllers.
$checkout = Catalog::createCheckoutSession($request->user(), [
'price' => $price->stripe_price_id,
'mode' => $price->isRecurring() ? 'subscription' : 'payment',
'success_url' => route('checkout.success'),
'cancel_url' => route('shop.index'),
]);
return redirect($checkout->url);// fancy-seo server-renders the head and emits Product JSON-LD, so the listing
// arrives complete in the first byte rather than after hydration. A storefront
// that only assembles itself in the browser is one search engines see as empty.
<Seo
title={product.name}
description={product.description}
jsonLd={{
"@context": "https://schema.org",
"@type": "Product",
name: product.name,
offers: {
"@type": "Offer",
price: (product.price.amount / 100).toFixed(2),
priceCurrency: product.price.currency.toUpperCase(),
availability: product.in_stock
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock",
},
}}
/>How to solve it
Install the catalog
Products and prices become models in your own database, synced outward to Stripe rather than read back from it.
Run thisbash composer require particle-academy/laravel-catalogBuild the grid from your own query
Because the catalog is local, filtering and pagination are ordinary SQL. This is the decision that keeps category pages fast.
Send the customer to checkout
One facade call covers subscriptions and one-off payments, so the cart does not branch on payment mode.
Make product pages crawlable
Server-rendered head plus Product JSON-LD, with price and availability that match what checkout will actually charge.
