Wocart

How it works

A Wocart storefront has three layers:

  1. Your Astro pages own the routes, layout, copy and interaction.
  2. @wocart/storefront reads the store and calls its server. The root entry is browser-safe; the client and the key live on the server.
  3. cod-server owns prices, stock, coupons, delivery, OTP, order acceptance and every other rule.

The important consequence: a page renders what the server returned. It does not calculate a price, decide that a coupon applies, or mark an order as accepted.

What a source file imports

Your fileImport from
Page frontmatter, middleware, action, API route@wocart/storefront/server and @wocart/storefront/astro
Browser script or island@wocart/storefront
Review photo or voice script@wocart/storefront/review-media

The root entry has no API key and no client factory. That is deliberate, not a convention: the package's contract test fails if the key implementation leaks into the root artifact.

One client per request

Create the client where the request is served, pass it to the page, and let it go with the response.

TypeScriptCode
// src/lib/store.ts — server only import { createStoreClient, type StoreClient } from "@wocart/storefront/server"; import { COD_SERVER_URL, STORE_API_KEY } from "astro:env/server"; export function createStore(locale?: string): StoreClient { return createStoreClient({ serverUrl: COD_SERVER_URL ?? "", apiKey: STORE_API_KEY ?? "", ...(locale === undefined ? {} : { locale }), onError: (error) => console.warn(error.operation, error.code), }); }

A client holds one store's key and one language's caches. Do not keep one as a module-global and do not share it across stores.

Page types and their paths

The platform owns these paths. A storefront renders each page it supports at the path the platform uses, and builds links with storefrontPath.

Page typePathMain reads
Home/config.get, products.list, collections.get
Catalog/productsproducts.page
Category/category/:slugcategories.list, products.list({ categoryId })
Collection/collections/:handlecollections.get, products.list({ collection })
Product/products/:handleproducts.get
Store page/pages/:slugpages.get
Landing page/lp/:slugpages.landing
Checkout/checkoutBrowser basket, then cart.validate
Thank you/thank-youorders.tracking
Track order/track-orderlookupOrder, over orders.status (order number and phone); ?order=<number> fills the number in
Search/searchproducts.search; ?q=<words> is the query
Contact/contactconfig.contact and whatsAppContact
TypeScriptCode
import { storefrontPath } from "@wocart/storefront"; storefrontPath("product", { handle: "linen-shirt" }); storefrontPath("category", { slug: "women" }); storefrontPath("checkout");

STOREFRONT_PAGE_PATHS is the list; @wocart/theme's SurfacePageType names the same page types for a theme's surfaces. Other paths (/about, /offers/summer) belong to your Astro project. The SDK does not constrain them, but menus, the WhatsApp launcher's switches and Studio know only the platform's page types.

A read degrades; it does not throw

Every read resolves to an empty value when the server cannot be reached, a response no longer matches the contract, or the resource is not public:

ReadEmpty value
config.get()null
products.get()null
products.list()[]
products.page() / products.search(){ items: [], hasNext: false, total: 0 }
categories.list()[]
collections.get()null
menus.get()null
pages.get() / pages.landing()null
shipping.rates(){}
geo.communes()[]
cart.validate()a failure result
orders.tracking()null

A design decision follows: an empty state is part of the page, not an exception handler. “No products yet” and “the API is briefly down” look the same to the reader. onError is how the operator tells them apart.

TypeScriptCode
const products = await createStore().products.list(); if (products.length === 0) { return renderEmptyCatalog(); }

A catalogue can also be empty by design. Do not put a red error banner on a new store with no products.

Null on a detail page is a real 404

For products.get, collections.get, menus.get, pages.get and pages.landing, null means “not public or not found”. Render your 404, not a 200 page with blank fields.

A write returns a result

Writes never throw for a refusal. They answer with a discriminated result:

TypeScriptCode
const result = await store.orders.create(body); if (!result.success) { if (result.domainError.category === "validation") { // Show the field issues. } else if (result.domainError.category === "business") { // Show the store's refusal. } return; } console.log(result.data.orderNumber);

result.domainError always exists on an SDK failure. Read its category, code, retryable, sideEffect and fieldIssues rather than parsing an HTTP status.

The categories are:

CategoryMeaningUsual response
validationA field or shape is wrongMark the field and keep the form
authenticationThe store key or setup is wrongLog it; do not retry in the browser
businessThe store refused the requestShow the server's message
transportNetwork, timeout or cancellationOffer a retry when retryable
systemThe server failedKeep the shopper's input and offer a retry
unknownAn unclassified gateway failureKeep the input; do not claim success

Retries and caching

The SDK already applies the safe policy:

  • Safe reads are retried after a transport failure or a 5xx.
  • Writes are never retried. A dropped connection after an order may mean the order exists. Check before asking the shopper to submit again.
  • config.get(), categories.list() and geo.communes(id) cache a successful result for the client's lifetime. Failures are never cached.
  • Products, pages, menus, collections, rates and page data are fetched fresh. Your route cache is what decides how often a page is rebuilt.

readTimeoutMs defaults to 10 seconds for reads. Writes have no default timeout: a write cut off by a timer may already have happened.

The security boundary

The store key is a server secret. Browser code never sees it.

Code
Browser └─ POST /api/cart/validate (your same-origin route) └─ createStoreClient(...) (server only, key from env) └─ cod-server (prices and rules)

This is also why the SDK's browser helpers are not a second API. formatMoney, the basket, the OTP machine and image URL builders run locally; anything that needs the key goes through your route.

What the server still decides

Even if your page displays it, the server re-checks:

  • the price of every basket line;
  • stock and sellability;
  • whether a coupon applies;
  • the delivery fee for the chosen destination;
  • whether OTP or Turnstile is required;
  • whether an order is accepted at all.

The browser may be optimistic. The order is not.