← Back to all articles

No database, no problem: e-commerce with Nuxt Content and Stripe

Published on March 31, 2026 · 4 min read

nuxttailwindcssstripebunjs

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:

  1. User builds a cart (Pinia)
  2. Frontend calls a server route with the cart items
  3. Server creates a Stripe Checkout session and returns the URL
  4. 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 demoAURORA 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.