@cdsxyz/setu-storefront
@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:
- Paise pricing — all price fields on the wire are integer paise. Use
formatPaiseto display them. - 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 theXSRF-TOKENcookie Setu sets, sent under theX-CSRF-Tokenheader (the only header name Phoenix's CSRF plug checks). - Shared props — Inertia page props include
auth.customer,tenant,cart,wishlist,nav_itemsat the top level (set byStorefrontSharedPropsplug on the server). - 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: ... }. - Inertia —
@inertiajs/vue3must be configured in the host app. Composables useusePage()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-uifor 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
-
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 (plushttps://www.<domain>) to call the API. Without it, every cross-origin fetch fails with a CORS error. -
Required for production: CNAME
api.<domain>to Setu. Pointapi.acme.comat Setu's real production host —creativedestructionstudios.com(thePHX_HOSTfly.tomlsets 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'sTenantPlugalready resolvesapi.<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.comas a third-party site relative to youracme.comstorefront, so theSameSite=Nonesession cookie gets aggressively pruned — every visit looks like a new session, carts empty between page navigations, and customer logins don't persist. Settingapi.acme.computs 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_APIdirectly (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
dt-ui components for Vue 3 — generated from the same DTX source as the Angular a...
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 |