Wocart

Quick start

This page builds the smallest complete storefront: one server-only client, a home page that lists the store's products, and a product page that answers a real 404. Every file is shown in full; together they type-check and build for Cloudflare and for Vercel.

What you need

Node 22.12 or newer, the store's API origin (COD_SERVER_URL) and a store API key (STORE_API_KEY). A self-hosted Wocart runs its API locally on http://localhost:8787.

1. Create the project

TerminalCode
npm create astro@latest my-store -- --template minimal --typescript strict --no-git cd my-store npm install --save-exact @wocart/storefront@3.1.0 npm install @astrojs/cloudflare @astrojs/vercel zod npm install -D wrangler @astrojs/check typescript

zod 4 is the SDK's peer dependency. A Wocart storefront runs on two hosts from the same code, Cloudflare Workers and Vercel, so it installs both adapters; wrangler runs the Cloudflare build locally. Then set the build scripts in package.json:

JSONCode
{ "scripts": { "dev": "astro dev", "build": "astro build", "build:vercel": "DEPLOY_TARGET=vercel astro build", "preview": "astro build && wrangler dev" } }

build is the Cloudflare build (dist/), build:vercel the Vercel build (.vercel/output).

2. Configure Astro

Javascriptastro.config.mjs
import { defineConfig, envField, passthroughImageService } from "astro/config"; const isVercel = process.env.DEPLOY_TARGET === "vercel"; const adapter = isVercel ? (await import("@astrojs/vercel")).default() : (await import("@astrojs/cloudflare")).default({ imageService: "passthrough" }); export default defineConfig({ output: "server", adapter, session: false, image: { service: passthroughImageService() }, env: { schema: { COD_SERVER_URL: envField.string({ context: "server", access: "secret", optional: true }), STORE_API_KEY: envField.string({ context: "server", access: "secret", optional: true }), }, }, });

Both values are server secrets: never PUBLIC_*, never rendered into a page, never passed to a browser script. They are optional so a build never needs a store; a request without them fails loudly instead (step 3).

DEPLOY_TARGET=vercel builds with the Vercel adapter; without it the storefront builds for Cloudflare. session: false keeps the Cloudflare Worker from requiring a SESSION KV namespace the storefront never uses. Images pass through: Astro's default sharp service would put native binaries for the build machine into the Vercel output. To resize store images on either host, use the media image service.

The Cloudflare build reads its runtime from wrangler.jsonc:

wrangler.jsonc
{ "name": "my-store", "main": "@astrojs/cloudflare/entrypoints/server", "compatibility_date": "2026-04-22", "compatibility_flags": ["nodejs_compat"], "assets": { "directory": "./dist", "binding": "ASSETS" } }

astro dev runs in Cloudflare's local runtime, which reads its values from .dev.vars:

Terminal.dev.vars
COD_SERVER_URL=http://localhost:8787 STORE_API_KEY=your-store-api-key

Never commit it: add .dev.vars, .wrangler/ and .vercel/ to .gitignore.

3. One module creates the client

TypeScriptsrc/lib/store.ts
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(`[store] degraded read: ${error.operation} (${error.code})`); }, }); }
  • One place. If the origin or key is wrong, this is the file to inspect. No page builds its own client.
  • A new client per call, so per request: one request's reads never wait on another's, and a client's caches never cross requests.
  • createStoreClient throws MISSING_CONFIGURATION when serverUrl or apiKey is empty — a loud setup error instead of a store that silently looks empty.
  • onError is the only place you see why a read degraded: reads never throw (step 5).
  • locale asks the server for a published language other than the primary. Store config and languages adds language routing.

4. List the products

src/pages/index.astro
--- import { formatMoney, storefrontPath } from "@wocart/storefront"; import { createStore } from "../lib/store"; const store = createStore(); const [config, products] = await Promise.all([ store.config.get(), store.products.list({ limit: 12 }), ]); const money = (amount: number) => formatMoney(amount, { symbol: config?.currencySymbol ?? "دج", locale: config?.locale ?? "ar" }); --- <html lang={config?.locale ?? "ar"} dir={config?.locales[0]?.direction ?? "rtl"}> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>{config?.name ?? "Store"}</title> </head> <body> <h1>{config?.name}</h1> {products.length === 0 ? ( <p>No products yet.</p> ) : ( <ul> {products.map((product) => ( <li> <a href={storefrontPath("product", { handle: product.handle })}>{product.name}</a> <span>{money(product.price)}</span> </li> ))} </ul> )} </body> </html>
  • config.get() and products.list() run in parallel: neither needs the other.
  • Empty is a state, not an error. A failed read and a store with no products both give []. Design the empty page; onError tells you which.
  • storefrontPath("product", { handle }) gives /products/<handle>, the platform's path for a product. Never write the string by hand.
  • formatMoney places the store's currency symbol for its language.

5. Render a product

src/pages/products/[handle].astro
--- import { formatMoney } from "@wocart/storefront"; import { notFound } from "@wocart/storefront/astro"; import { createStore } from "../../lib/store"; const store = createStore(); const [config, product] = await Promise.all([ store.config.get(), store.products.get(Astro.params.handle!), ]); if (product === null) return notFound(Astro); const price = formatMoney(product.price, { symbol: config?.currencySymbol ?? "دج", locale: config?.locale ?? "ar" }); --- <html lang={config?.locale ?? "ar"} dir={config?.locales[0]?.direction ?? "rtl"}> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>{product.name}</title> </head> <body> <h1>{product.name}</h1> <p>{price}</p> {product.description && (product.descriptionFormat === "html" ? <div set:html={product.description} /> : <p>{product.description}</p>)} </body> </html>
src/pages/404.astro
--- --- <html lang="ar" dir="rtl"> <head> <meta charset="utf-8" /> <title>404</title> </head> <body> <h1>404</h1> </body> </html>
  • The handle is the product's public address. Never look a product up by its internal id in a URL.
  • null is a real 404. notFound(Astro) renders your /404 page with status 404 and turns caching off for that response, so a product published a minute later is not hidden by a cached refusal.
  • A description is either text or HTML (descriptionFormat). HTML was sanitized by the server when the merchant saved it: render it with set:html, and never sanitize it again or print it as text. Text is always escaped.

6. Run it

TerminalCode
npx astro check npm run build npm run build:vercel npm run dev

Open http://localhost:4321: the store's name and products, each linking to its page. /products/nope answers 404.

Where next

NextPage
Serve every published language at its own URLStore config and languages
Add the order form and the same-origin API routesAstro actions and routes, Checkout and OTP
Add a basketBasket and quotes
Show delivery prices per wilayaDelivery and geography
Let merchants edit the design in Wocart StudioThe theme starter

The browser never gets the key

Browser scripts import only @wocart/storefront (its root entry) and @wocart/storefront/review-media. Anything that needs the store goes through a route of your own that calls the SDK on the server — createStorefrontRoutes builds them for you.