@cdsxyz/setu-storefront

Headless composables, types, and utilities for storefronts built on the Setu commerce backend.
488
Install
npm install @cdsxyz/setu-storefront
Latest Version:0.2.4
Vue:3
License:MIT
Last Updated:Oct 6, 2026
Links: npm
Maintainer: deviprsd

@cdsxyz/setu-storefront

Headless composables, types, and utilities for Vue 3 storefronts built on the Setu commerce backend.

Install

npm install @cdsxyz/setu-storefront

Peer dependencies

npm install vue @inertiajs/vue3 @unhead/vue

What's included

Cross-origin config

Export Purpose
configureSetu({ apiUrl }) Point every composable's fetch calls at a different origin (a Netlify-hosted storefront calling Setu's Fly app). Defaults to relative URLs — same-origin consumers don't need to call this.
apiPath(path) Prefix a path with the configured apiUrl — for a component (e.g. SearchBar) that fetches outside the built-in composables.
withCsrf(init?) fetch options with credentials: 'include' and the CSRF header merged in — for the same case.

Composables

Export Purpose
useCart() Cart mutations (add, update qty, remove, apply/remove promo code) via fetch; updates Inertia shared props
useWishlist() Wishlist toggle (isWishlisted, toggleWishlist, removeItem)
useSharedProps() Reads auth.customer, tenant, cart, navItems, flash from Inertia
useUrlMultiParam(key) Reactive multi-value URL param (filters, facets) with toggle/add/remove/clear
useUrlParam(key) Reactive single-value URL param (sort, page, search)
useUrlReset() Clear all query params in one navigation
clearUrlParams({ keys, keyPrefixes }) Clear specific query params (by key and/or prefix) in one navigation
useSeoHead() Page title, meta, og:image, JSON-LD via @unhead/vue

Utilities

Export Purpose
formatPaise(n) 8000 → "₹80" — all Setu prices are integer paise
formatPaiseOrNull(n) Same as formatPaise, but null instead of "" for missing values
formatPaiseRange(min, max) "₹80 – ₹120" or "from ₹80"
cropStyles(media, slot) CSS object for object-fit: cover with focal-point zoom; falls back to objectPosition from focal_point when no crop rect exists
dominantColor(media) Hex color from media metadata for placeholder backgrounds
resolveCrop(media, slot) Raw crop rect (CropRect | null) — no fallback, see cropStyles for that

Types

Full TypeScript types for all Setu commerce resources: Product, Variant, Cart, CartLine, Order, OrderLine, Customer, Tenant, Media, and more.

Contracts

This package assumes the following about the Setu backend:

  1. Paise pricing — all price fields on the wire are integer paise. Use formatPaise to display them.
  2. CSRF — same-origin, a <meta name="csrf-token"> element must be present; composables read it on every mutation. Cross-origin (see "Deploying a storefront to Netlify" below), there is no such tag — composables fall back to the XSRF-TOKEN cookie Setu sets, sent under the X-CSRF-Token header (the only header name Phoenix's CSRF plug checks).
  3. Shared props — Inertia page props include auth.customer, tenant, cart, wishlist, nav_items at the top level (set by StorefrontSharedProps plug on the server).
  4. API routes — cart mutations at /cart/add, /cart/update/:id, /cart/remove/:id, /cart/apply-code, /cart/remove-code/:id; wishlist at /account/wishlist/add, /account/wishlist/remove/:id. All return { ok: boolean, cart/wishlist: ... }.
  5. Inertia — @inertiajs/vue3 must be configured in the host app. Composables use usePage() to read shared props reactively.

Usage

import { useCart, useWishlist, formatPaise } from '@cdsxyz/setu-storefront'
import type { Product, Cart } from '@cdsxyz/setu-storefront'

// In a Vue 3 component
const { addToCart, loading } = useCart()
const { isWishlisted, toggleWishlist } = useWishlist()

addToCart('variant-uuid', 1)
toggleWishlist('variant-uuid')
formatPaise(8000) // "₹80"

What's NOT included

  • Vue components (see @cdsxyz/setu-storefront-ui for brand-agnostic starting points — most fully-designed tenants write their own instead)
  • Admin panel logic
  • Domain-specific features (textile customization, serialized inventory)
  • CSS or Tailwind configuration

Deploying a storefront to Netlify

Setu treats Netlify-hosted storefronts as cross-origin Inertia clients. Each tenant operates their own repo and deploys it to their own Netlify site. cds-tech/bastrika.in is the first (and, until a generic one is harvested from it, the reference) implementation of this pattern — fork it rather than rebuilding from this README alone. The browser talks to Setu directly for both initial HTML (via SSR) and subsequent navigation / mutations.

One-time tenant setup

  1. Register your domain in Setu — your tenant record needs domain: "acme.com". Setu's CORS layer reads this on every request and only allows your origin (plus https://www.<domain>) to call the API. Without it, every cross-origin fetch fails with a CORS error.

  2. Required for production: CNAME api.<domain> to Setu. Point api.acme.com at Setu's real production host — creativedestructionstudios.com (the PHX_HOST fly.toml sets on the Fly app; use that, not the generic <app>.fly.dev, for identity/branding) — and serve your storefront from there in env config (SETU_API_URL=https://api.acme.com). Phoenix's TenantPlug already resolves api.<domain> to your tenant via the standard domain-resolution path.

    What breaks without it: the storefront still works on Chrome (with default settings) but falls over on Safari (~20% of typical Indian e-commerce traffic) and Chrome with enterprise privacy mode. Safari's ITP treats creativedestructionstudios.com as a third-party site relative to your acme.com storefront, so the SameSite=None session cookie gets aggressively pruned — every visit looks like a new session, carts empty between page navigations, and customer logins don't persist. Setting api.acme.com puts the API on the same eTLD+1 as the storefront, making the cookie first-party from the browser's perspective. Without this, you will get user complaints within hours of launch.

    For staging / preview environments, the recommended setup is optional but you should expect Safari to be broken. Do not skip for production.

Repo layout

Use @cdsxyz/setu-storefront from npm (@cdsxyz/setu-storefront-ui is brand-agnostic scaffolding, not a fit for a fully-designed tenant — most tenants, bastrika.in included, write their own components against it). Inertia's @inertiajs/vue3 SSR-mode createInertiaApp (the main entry, not /server — that export is a Node daemon wrapper for a different deploy shape) handles the render.

your-storefront/
├── package.json          # depends on @cdsxyz/setu-storefront, vue, @inertiajs/vue3, @unhead/vue
├── vite.config.ts        # client + SSR builds
├── netlify.toml          # SSR Function for cacheable GETs, static shell for cart/checkout/account
├── netlify/functions/ssr.ts
└── src/
    ├── app.ts            # client entry — createInertiaApp({ resolve: name => import(./Pages/${name}.vue) })
    └── Pages/            # your Vue page components

SSR Function (Netlify)

// netlify/functions/ssr.ts
import { createSSRApp, h } from 'vue'
import { renderToString } from 'vue/server-renderer'
import { createInertiaApp } from '@inertiajs/vue3'
import { createHead, renderSSRHead } from '@unhead/vue/server'

const SETU_API = process.env.SETU_API_URL!   // e.g. https://creativedestructionstudios.com  or  https://api.acme.com

export default async (req: Request) => {
  // Anonymous only — never forward the request's cookie upstream and never
  // relay Set-Cookie back. This response gets CDN-cached; caching a
  // session cookie would serve one visitor's session to the next. The
  // session is established by the browser's own first client-side call to
  // SETU_API, not by this function.
  const upstream = await fetch(`${SETU_API}${new URL(req.url).pathname}`, {
    headers: { 'X-Inertia': 'true', 'X-Inertia-Version': '1', 'Accept': 'application/json' },
  })

  const page = await upstream.json()
  const head = createHead()

  const { body } = await createInertiaApp({
    page,
    render: renderToString,
    resolve: name => import(`../../src/Pages/${name}.vue`),
    setup: ({ App, props }) => createSSRApp({ render: () => h(App, props) }),
    // wire `head` into useHead()/useSeoHead() calls per your unhead setup
  })

  const { headTags } = await renderSSRHead(head)

  return new Response(htmlShell({ headTags, body }), {
    headers: { 'content-type': 'text/html' },
  })
}

The SSR function only handles GETs on cacheable routes (catalog/product/ collection/weaver pages) — enforce that split in netlify.toml, not just in code, so "cacheable" and "cookieless" can't drift apart. Cart/checkout/ account pages skip the function entirely: a static shell, CSR only. All mutations (POST/PUT/DELETE) go browser → Setu directly via Inertia's client-side router — the browser holds the cross-origin session cookie and sends the XSRF-TOKEN cookie's value back under the X-CSRF-Token header, and Setu's TenantCors accepts the request because the Origin matches.

Required env vars

Variable Example Purpose
SETU_API_URL https://creativedestructionstudios.com or https://api.acme.com Base URL the SSR function and the client both fetch from

Client-side, call configureSetu({ apiUrl: import.meta.env.SETU_API_URL }) once at startup so useCart/useWishlist target Setu instead of Netlify's own origin.

What you do NOT need

  • A CDN — Netlify serves your client bundle from its edge already.
  • A separate API key in the browser — customer authentication is via the Setu session cookie, set the first time the browser calls SETU_API directly (login, or any client-side mutation) — never by the SSR function, which is anonymous-only and CDN-cached.
  • A custom Phoenix deploy — Setu's Phoenix app stays as-is; you're just another cross-origin client.

Related Packages

@mawjod/nuxt

Nuxt 4 module for the Mawjod storefront API.

1,919 0
@merq/store

The Vue frontend package for merq.

674
@zenky/storefront-vue

Zenky Storefront SDK for Vue 3.

3,606
ecomplus-render

Vue.js app to render E-Com Plus store template

2,484 0
@1ecomm/dt-ui-vue

dt-ui components for Vue 3 — generated from the same DTX source as the Angular a...

371

Version History

Version Released Vue License
0.2.4 ^3.0.0 MIT
0.2.3 ^3.0.0 MIT
0.1.0 ^3.0.0 MIT