Storefront Architecture

Nuxt 4 + Blökkli visual site builder, file-based routing, BFF pattern

This page is for frontend developers about to start a storefront project — usually at a partner agency or on a customer's product team. It explains the technical stack you'll be working with, why each piece is there, and how the storefront fits into the rest of the platform.

If you already know Nuxt 4 well, most of this will look familiar. The revenexx-specific parts are the BFF pattern between the browser and the platform APIs (the part you write that sits between user-facing pages and the catalog/orders/etc. APIs), how the storefront's data is automatically scoped to its tenant, and the Blökkli integration for visual page composition (currently in preview as part of PO-46 — treat the Blökkli specifics as planned rather than shipped).

What you're building on

The default storefront template is a Nuxt 4 application, and five pieces do most of the work. Each one is there for a reason — together they're what lets the same codebase serve many customers without a rewrite per project.

  • Nuxt 4 — the Vue 3 meta-framework the whole storefront runs on, server-rendered by default. It gives you file-based routing, server routes for the BFF layer, and per-route rendering modes (static, dynamic, ISR), so you can serve a product page from cache and a cart page live without two different stacks.
  • Nuxt UI — the accessible, unstyled component library the standard template builds its product cards, forms, and navigation from. It's a starting point, not a cage: if a customer's design system is fundamentally different, you can swap it out.
  • Blökkli (preview, PO-46) — the visual page builder. Marketing teams compose Hero, ProductGrid, FAQ, and custom blocks in Cockpit, and the storefront renders them by mapping each block's type to a Vue component — so content people change pages without touching code.
  • Formily — schema-driven forms, leaned on heavily in checkout. Because a form is defined by a JSON schema rather than hand-written markup, you can change which fields a customer's checkout collects by editing config instead of rebuilding components.
  • Platform APIs — the REST and GraphQL endpoints the platform generates automatically from your app's tables, with every response automatically scoped to the tenant. This is where your data comes from; you consume it rather than building it.

Architecture Overview

text
┌─────────────────────────────────────────┐
│        Customer Browser                 │
│  (Desktop / Mobile / PWA)               │
└────────────────┬────────────────────────┘
                 │
         HTTP/HTTPS (SSR/CSR)
                 │
┌─────────────────▼────────────────────────┐
│      Nuxt 4 Storefront (BFF)            │
│  ┌──────────────────────────────────┐   │
│  │  File-based Routing              │   │
│  │  /products  /checkout  /account  │   │
│  └──────────────────────────────────┘   │
│  ┌──────────────────────────────────┐   │
│  │  Blökkli Pages (Editable)        │   │
│  │  Homepage / Category / CMS       │   │
│  └──────────────────────────────────┘   │
│  ┌──────────────────────────────────┐   │
│  │  BFF Layer                       │   │
│  │  - Cart aggregation              │   │
│  │  - Checkout orchestration        │   │
│  │  - Authentication                │   │
│  └──────────────────────────────────┘   │
└─────────────────┬────────────────────────┘
                  │
    GraphQL / REST (tenant-scoped)
                  │
    ┌─────────────┼─────────────┐
    │             │             │
    ▼             ▼             ▼
┌────────┐  ┌────────┐  ┌────────────┐
│ Cockpit│  │ Catalog│  │ Orders App │
│(Content)│ │ (SKUs) │  │(tenant-    │
│        │  │        │  │ scoped)    │
└────────┘  └────────┘  └────────────┘
    │             │             │
    └─────────────┼─────────────┘
                  │
       Platform data (tenant-scoped)

File-Based Routing

Nuxt's file-based routing automatically creates routes from your pages/ directory structure.

Directory → Route Mapping:

text
pages/
  └─ index.vue                      → /
  └─ products/
     └─ index.vue                   → /products
     └─ [id].vue                    → /products/:id
  └─ checkout/
     └─ index.vue                   → /checkout
     └─ [step].vue                  → /checkout/:step
  └─ account/
     └─ index.vue                   → /account
     └─ orders.vue                  → /account/orders
     └─ settings.vue                → /account/settings

Dynamic Routes:

Pages with [id].vue create a catch-all route that passes the segment as a parameter:

Vue
<script setup>
const route = useRoute()
const productId = route.params.id
const product = await fetchProduct(productId)
</script>

Blökkli: Visual Page Composition

Blökkli is a headless page builder that lets non-developers compose pages by dragging and dropping pre-built blocks. The reason it matters architecturally is the division of labour it creates: marketing owns the content and layout of a page, while you own the blocks it's assembled from. A page is never code a developer has to ship — it's data the marketing team edits.

The standard library covers the blocks most storefronts need — a Hero banner with image, headline, and CTA; a ProductGrid that renders the catalog with filters and sorting; Testimonials, Newsletter signup, FAQ, and Navigation blocks — and you can extend it with your own Vue components, which then appear in the editor as block types like any other.

The flow has three stages. First, each page lives as a JSON tree of blocks stored in Cockpit content — a title, a slug, and an ordered list of blocks with their props:

JSON
{
  "title": "Clearance Sale",
  "slug": "clearance-sale",
  "blocks": [
    {
      "type": "hero",
      "props": {
        "title": "Up to 70% Off",
        "image": "url/to/banner.jpg",
        "cta_url": "/products?sale=true"
      }
    },
    {
      "type": "productGrid",
      "props": {
        "category": "clearance",
        "per_page": 12,
        "sort": "price-asc"
      }
    }
  ]
}

Second, the marketing team edits that tree visually in Cockpit — they never see the JSON. Third, the storefront fetches the page and renders it by walking the block list and mapping each type to a registered component:

Vue
<template>
  <div class="page">
    <component
      v-for="block in page.blocks"
      :key="block.id"
      :is="`Block${block.type}`"
      :props="block.props"
    />
  </div>
</template>

<script setup>
const route = useRoute()
const page = await fetchBlökkliPage(route.params.slug)
</script>

Backend-for-Frontend (BFF) Pattern

This is the most important revenexx-specific idea on the page. The storefront doesn't talk to the platform's core APIs straight from the browser; it has a server layer of its own — the backend-for-frontend — that sits in between. That layer exists because the browser and the platform APIs want different things: the browser wants one tidy response shaped for the page it's rendering, while the platform exposes several granular, normalized APIs. The BFF is where you bridge that gap, and it's also where anything secret or sensitive stays, safely server-side. Four jobs land here on most projects.

Cart aggregation is the clearest example. A cart on screen needs product details from the Catalog API and live stock from the Orders API, combined into a single object the page can render, with the cart's own state synced between localStorage and the server. Rather than make the browser fire several calls and stitch the results together, a server route does it once:

TypeScript
// Storefront server-side (Nuxt server route)
export default defineEventHandler(async (event) => {
  const cartItems = getCookie(event, 'cart') || []
  const products = await cockpit.catalog.getProducts(cartItems.map(i => i.id))
  const inventory = await cockpit.orders.checkInventory(cartItems)
  
  return {
    items: cartItems.map(item => ({
      ...item,
      product: products.find(p => p.id === item.id),
      in_stock: inventory[item.id]?.available > 0
    })),
    total: calculateTotal(cartItems, products)
  }
})

Checkout orchestration is the second job, and the most delicate: the BFF runs the multi-step validation across address, payment, and confirmation, coordinates between the payment gateway and the orders service, and rolls the transaction back if any step fails — logic that has no business running in the browser. Authentication and session is the third: login and logout flows, issuing and validating session tokens, and keeping each customer pinned to the right tenant. And search is the fourth, where the BFF handles pagination, filtering, faceted search across category, price, and brand, and integration with the suggest API. In every case the pattern is the same — the browser asks the BFF a page-shaped question, and the BFF does the fan-out to the platform.

Security: automatic tenant scoping

All platform API calls from the storefront carry the current tenant. The platform automatically scopes every response to that tenant — your data is scoped to your tenant without any app-level filtering.

Example:

Customer A logs into their storefront:

TypeScript
// Set tenant context
const headers = {
  'X-Revenexx-Tenant': 'customer-a',
  'Authorization': `Bearer ${session.token}`
}

// The API returns only this tenant's data
const products = await fetch('/graphql', {
  headers,
  body: { query: 'products { id sku price }' }
})
// Returns only products visible to customer-a

Tenant scoping ensures:

  • Customer A never sees Customer B's products
  • Each tenant's data is completely isolated
  • No cross-tenant leakage possible

Styling: Nuxt UI + Custom Themes

Theming runs entirely on CSS custom properties: the palette, typography, and spacing are defined as variables, the template ships a set of built-in themes (Default, Dark, Ocean, Forest, Sunset, Monochrome), and because components read the variables rather than hard-coded values, a customer can switch theme at runtime with no reload. This is the same mechanism the dedicated Theming page covers in depth; the short version is that swapping the variable values reskins the whole storefront.

Vue
<script setup>
const theme = ref('default')

const setTheme = (newTheme) => {
  document.documentElement.setAttribute('data-theme', newTheme)
  theme.value = newTheme
}
</script>

<template>
  <select @change="e => setTheme(e.target.value)">
    <option value="default">Default</option>
    <option value="dark">Dark</option>
    <option value="ocean">Ocean</option>
  </select>
</template>

CSS Custom Properties

CSS
:root {
  --color-primary: #4f46e5;
  --color-secondary: #10b981;
  --color-text: #1f2937;
  --color-bg: #ffffff;
  --spacing-unit: 0.5rem;
  --font-primary: 'Inter', sans-serif;
}

/* Dark theme */
[data-theme="dark"] {
  --color-text: #f3f4f6;
  --color-bg: #111827;
}

Server Routes (API Routes)

Nuxt allows server-side logic via server routes (in server/api/ or server/routes/):

TypeScript
// server/api/cart.post.ts
export default defineEventHandler(async (event) => {
  const { productId, quantity } = await readBody(event)
  const tenantId = event.context.user?.tenant_id
  
  await cockpit.cart.addItem(tenantId, productId, quantity)
  return { success: true }
})

Running this logic in a server route rather than the browser pays off three ways: your API secrets stay server-side and never reach the client, you avoid an extra network hop so latency drops, and error handling and logging all happen in one place you control.

Performance: Nuxt 4 Optimizations

Nuxt 4 gives you several performance levers, and they matter most on the high-traffic pages of a storefront. Hybrid rendering lets each page be static, dynamic, or cached as appropriate, and route caching lets you set a per-route TTL — a product page might cache for an hour while a cart page never does. On top of that, images are optimized and lazy-loaded automatically, code splitting ensures a visitor only downloads the JavaScript for the page they're on, and the underlying Nitro server keeps the server routes fast and lightweight. The net effect is that the common pages stay cheap to serve under load.

Deployment: Edge + Serverless

A storefront is usually deployed to a serverless-plus-edge host — Vercel and Netlify are the common choices, both pairing serverless functions with an edge CDN, though a self-hosted Docker and Kubernetes setup works too. Whatever the target, the deployment auto-scales with traffic, serves static assets from the edge close to the customer, caches API responses (in-process or a shared cache), and supports A/B testing and feature flags. The goal in every case is the same: keep the storefront fast and resilient as traffic spikes, without hand-tuning capacity.

Next Steps

  • Checkout — Multi-step checkout flow with Formily
  • Authentication — Login, register, password reset
  • Theming — Customizing colors, fonts, and layouts
Was this page helpful?