I've been building frontends for a while now, and one thing that still surprises me is how much infrastructure we accept as a given for small e-commerce projects. A database. An admin panel. A CMS subscription. A backend to glue it all together.
For a curated catalog of 10 to 50 products, that's a lot of moving parts.
I wanted to see how far I could go in the other direction. The result is AURORA Commerce — a Nuxt 4 storefront where products live in YAML files, payments go through Stripe, and the infrastructure cost is zero.
Here's how I built it and why it might be the right approach for your next project.
The idea: your repo is your database
Instead of querying a database or calling a CMS API, product data lives directly in the repository:
content/
products/
heavyweight-crewneck-charcoal.yml
linen-midi-dress-terracotta.yml
oxford-button-down-shirt-white.yml
Each file is a complete product record:
productId: 6
title: "Heavyweight Crewneck - Charcoal"
titleFr: Sweat Crewneck Heavyweight - Charbon
slug: heavyweight-crewneck-charcoal
price: 109
category: sweats
badge: Core line
highlight: Brushed cotton 420 g/m2
description: "Oversize sweatshirt in ultra-soft brushed cotton. 420 g/m2 weight,"
reinforced collar, and premium finishes. The staple you never take off.
descriptionFr: Sweat oversize en coton brosse ultra-doux 420 g/m2. Col renforce,
finitions soignees. Le basique premium que tu portes tous les jours.
images:
- https://your-cdn.com/product-1.jpg
- https://your-cdn.com/product-2.jpg
- https://your-cdn.com/product-3.jpg
sizes: [S, M, L, XL]
fabricWeightGsm: 420
origin: Knit in France, made in Portugal
sizeChart:
- { size: S, chestCm: 90, waistCm: 74, lengthCm: 67 }
- { size: M, chestCm: 95, waistCm: 79, lengthCm: 68 }
- { size: L, chestCm: 100, waistCm: 84, lengthCm: 69 }
- { size: XL, chestCm: 106, waistCm: 90, lengthCm: 70 }
reviews:
- { author: Theo G., city: Nantes, rating: 5, date: '2026-03-07',
quote: The weight is perfect and the finish feels way above standard sweatshirts. }
Add a product by duplicating a file. Update a price by changing a number. Deploy on push. No dashboard, no migration, no API key to rotate.
It sounds almost too simple. It kind of is — and that's the point.
Querying with Nuxt Content v3
Nuxt Content v3 handles the YAML parsing and exposes a typed query API. Fetching the full catalog:
// pages/boutique.vue
const { data: products } = await useAsyncData('products', () =>
queryCollection('products')
.where('active', '=', true)
.order('productId', 'ASC')
.all()
)
A single product by slug:
// pages/produit/[slug].vue
const { data: product } = await useAsyncData(`product-${slug}`, () =>
queryCollection('products')
.where('slug', '=', slug)
.first()
)
Nuxt Content generates a typed collection from your YAML schema automatically. You get autocomplete on product.sizeChart, product.titleFr, product.fabricWeightGsm — the whole thing. TypeScript is happy. You are happy.
The Stripe integration
The checkout flow is straightforward:
- User builds a cart (Pinia)
- Frontend calls a server route with the cart items
- Server creates a Stripe Checkout session and returns the URL
- Frontend redirects
The server route is the only backend code in the project:
// server/api/checkout-session.post.ts
import Stripe from 'stripe'
export default defineEventHandler(async (event) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const body = await readBody(event)
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: body.items.map((item: CartItem) => ({
quantity: item.quantity,
price_data: {
currency: 'eur',
unit_amount: item.price * 100, // YAML stores euros, Stripe wants cents
product_data: {
name: item.title,
images: [item.thumbnail],
},
},
})),
success_url: `${process.env.NUXT_PUBLIC_APP_URL}/success`,
cancel_url: `${process.env.NUXT_PUBLIC_APP_URL}/panier`,
shipping_address_collection: {
allowed_countries: ['FR', 'BE', 'CH', 'DE', 'GB', 'SE'],
},
})
return { url: session.url }
})
One detail worth noting: I use price_data instead of a Stripe price ID. This means products don't need to exist in the Stripe dashboard at all — the source of truth stays in the YAML files. One less thing to keep in sync.
Bilingual without a translation library
The storefront supports English and French natively. No i18n library, no translation files. The strategy is simple: bilingual fields directly in the YAML (title / titleFr, description / descriptionFr), combined with Nuxt's built-in routing strategy (prefix_except_default):
/boutique → English
/fr/boutique → French
A small composable resolves the right field based on the active locale:
// composables/useLocaleField.ts
export function useLocaleField() {
const { locale } = useI18n()
function t(en: string, fr?: string): string {
return locale.value === 'fr' && fr ? fr : en
}
return { t }
}
Usage in templates:
<h1>{{ t(product.title, product.titleFr) }}</h1>
<p>{{ t(product.description, product.descriptionFr) }}</p>
The copy lives with the product. No keys to manage, no files to synchronize.
Design tokens in one place
The entire visual identity is controlled from tailwind.config.ts:
theme: {
extend: {
colors: {
brand: {
primary: '#0A0A0A',
accent: '#C9A96E',
surface: '#F8F6F2',
},
},
fontFamily: {
display: ['Cormorant Garamond', 'serif'],
body: ['DM Sans', 'sans-serif'],
},
},
}
Change brand.accent once and every button, badge, and highlight updates across the storefront. No component hunting.
When this makes sense — and when it doesn't
This approach works well when:
- Your catalog is small and curated (under ~200 products)
- You want zero ongoing infrastructure cost
- You're comfortable with a git-based workflow for content updates
- You want full ownership — no platform lock-in
It's not the right fit if you need real-time inventory management, a non-technical client who needs a visual CMS, or complex product variants across multiple axes.
For the CMS case specifically: the architecture supports swapping queryCollection for a Sanity or Contentful client behind a shared adapter interface. The rest of the storefront doesn't need to change.
The result
A full storefront — home, shop, product detail, cart, Stripe checkout, success and cancel pages, bilingual, dark mode, SEO-ready — deployable on Vercel in under an hour.
→ Live demo
→ AURORA Commerce on Gumroad — Starter €29 / Pro €59
If you have questions or want to discuss the architecture, drop a comment. Happy to talk about it.
Built with Nuxt 4, Vue 3, Nuxt Content v3, Pinia, Tailwind CSS, Stripe, and Bun.
Top comments (0)