Data & APIs
Backend ERP API contract, apiFetch, ISR cache tags, internal route handlers, contexts, and data-layer hooks.
CloudPos Ecom fetches all storefront data from a tenant-owned ERP backend over a versioned REST API. Server components call the backend directly (ISR-cached); browser-side mutations route through a locked-down Next.js API proxy that manages auth cookies. This page covers the full data layer: the fetch primitive, endpoint catalog, settings resolution, internal route handlers, React contexts, and client hooks.
apiFetch
src/lib/api.js exports apiFetch(route, options) as the single entry point for every server-side ERP call.
Target Resolution
apiFetch resolves the base URL and tenant key from either the options.tenant argument (multi-tenant server components pass the resolved tenant object) or the build-time environment variables:
NEXT_PUBLIC_API_BASE_URL → base origin, e.g. https://erp.cloudposai.in
NEXT_PUBLIC_X_TENANT → fallback X-Tenant header valueThe actual fetch target is always ${apiBaseUrl}/api/v1${route}.
Request Headers
Every apiFetch call sets:
Header | Value | Notes |
|---|---|---|
|
| Always |
| tenant key from config | Identifies the merchant on the ERP |
| locale code | Present when |
The ?locale= query parameter is intentionally omitted: the current backend 500s on unknown query params. Locale is signaled via Accept-Language only. Without the query param, en and ar share the same Next Data-Cache key per URL — acceptable because the backend does not localize list content at this time.
Timeout
Default timeout is 10 000 ms, configurable via API_TIMEOUT_MS environment variable. Uses AbortSignal.timeout() when available, falling back to AbortController.
ISR Cache Tags
Every server fetch is tagged for targeted on-demand revalidation:
export function tenantTags(tenant, category) {
const id = tenant?.id;
const tags = [`tenant:${id}`];
if (category) tags.push(`tenant:${id}:${category}`);
return tags;
}When a locale is also provided, apiFetch appends tenant:<id>:<locale> so individual locale caches can be purged independently. The three valid category values are:
Category | Data covered |
|---|---|
| website-settings, general, company-props (branding, feature toggles) |
| product lists, detail, featured, offers, recent, best-selling |
| banners, categories, FAQs, testimonials, services, SEO content, stores |
In development, apiFetch always uses cache: "no-store" so changes are reflected immediately.
Endpoint Catalog
All functions below are exported from src/lib/api.js. Each wraps apiFetch with pre-set revalidate windows and category tags.
Function | ERP route | Revalidate | Category |
|---|---|---|---|
|
| 3600 s | settings |
|
| 3600 s | settings |
|
| 3600 s | settings |
|
| 3600 s | content |
|
| 3600 s | content |
|
| 3600 s | content |
|
| 3600 s | content |
|
| 3600 s | content |
|
| 3600 s | content |
|
| 3600 s | content |
|
| 3600 s | content |
|
| 86400 s | content |
|
| 300 s | products |
|
| 300 s | products |
|
| 300 s | products |
|
| 300 s | products |
|
| 300 s | products |
| alias of | 1800 s | products |
|
| 1800 s | products |
|
| no-store | — |
|
| 3600 s | content |
|
| — (mutation) | — |
fetchProducts fetches pages in parallel batches of 5 (configurable via PRODUCTS_FETCH_CONCURRENCY) and stops when a page returns fewer than 20 items (the PRODUCTS_PAGE_SIZE). When a limit is given, only the exact number of pages needed is fetched.
fetchCategories and fetchCategoriesWithParent normalize missing slug fields by deriving a slug from the category name so every category node is linkable.
Settings Resolution
src/lib/settings.js exports resolveStoreSettings(settingsRes, propsRes), which merges the two settings endpoints (/website-settings and /company/prop-names) into one normalized object. company-props wins on code collisions.
const settings = resolveStoreSettings(websiteSettingsRes, companyPropsRes);The returned object shape:
{
currencyCode, // "INR" | "AED" | … (CURRENCY code)
showAddToCart, // SHOW_ADD_TO_CART_BUTTON flag
showProductPrice, // SHOW_PRODUCT_PRICE flag
showPricePublic, // SHOW_PRICE_PUBLIC flag (B2B gating)
showShareButton,
showNewBadge,
showStockInfo,
sections: {
banners, topCategories, featured, bestSelling, bestOffers, recent, contact
},
layout: { topBar, menu, bottomMenu, accountIcons },
auth: { emailLogin, phoneLogin, phoneOrEmailLogin, b2b, forcePasswordChange },
checkout: { whatsapp, askDeliveryDate, loginRequired, otpEnabled, optionalLogin, autoAccount },
}Boolean flags use a tri-state logic: absent code → fallback (caller-specified, fail-open by default for display toggles, fail-closed for opt-in features). Routes call resolveStoreSettings once per render and pass the result as props so no page re-implements toggle parsing. See Settings Reference for the full code catalog.
src/lib/formatters.js exports formatCompanyProps(propsRes, settingsRes) for the branding/contact data subset (company name, phone, email, logo URL, social links, delivery thresholds, etc.).
Internal Route Handlers
These Next.js API routes live under src/app/api/ and run in the Cloudflare Worker alongside the page renderer.
POST /api/proxy and GET /api/proxy
src/app/api/proxy/route.js is the authenticated backend proxy for all browser-side mutations. It enforces an explicit allowlist:
- Public endpoints (no cookie required): login, signup, B2B signup, password reset, enquiries, master-data-values.
- Temp-token endpoints:
verify-otp,forgot-password-change— use thetempTokenfield from the request body as aBearertoken. - Session endpoints: cart, orders, profile, addresses, coupons, payment — require a valid
auth_tokencookie. - Address endpoints matching
/customer/(update|delete)-address/<id>: allowed forPOSTandDELETEwith an active session.
On login and OTP-verify responses, the proxy strips the raw token / access_token from the JSON before returning it to the browser and sets three httpOnly cookies instead: auth_token, customer_id, auth_user. Tokens never appear in client-side JavaScript.
Rate limiting (20 req/min per IP, per-isolate best-effort) is applied to authentication endpoints via src/lib/rateLimit.js.
The proxy resolves the tenant target from the x-tenant-id header (set by middleware) or falls back to the request Host.
GET /api/session, POST /api/session, DELETE /api/session
src/app/api/session/route.js manages the auth cookie snapshot:
GET— returns{ authenticated, user }from theauth_token/auth_usercookies. Used byAuthContextfor the fast initial paint.POST— sets the three auth cookies from a{ token, customer_id, phone }payload. Called when a temp-token flow (OTP, password change) needs to upgrade to a full session.DELETE— clears all auth cookies. Called on logout.
Cookies are httpOnly, secure in production, sameSite: "lax", 7-day maxAge.
GET /api/revalidate (see also Revalidation)
POST /api/revalidate is the ERP-triggered ISR purge endpoint. See the Revalidation page for details.
GET /api/review, POST /api/review
src/app/api/review/route.js proxies review reads (unauthenticated) and review submissions (requires auth_token cookie). The POST passes FormData directly to the ERP so image uploads work without re-encoding. No Content-Type header is set on the outgoing request — the browser-set multipart/form-data boundary is preserved.
GET /api/invoice/[orderId]
src/app/api/invoice/[orderId]/route.js streams invoice PDFs from <apiBaseUrl>/download-invoice/<orderId> to the browser as an attachment. Requires auth_token cookie. The orderId param is validated to be numeric before hitting the ERP.
POST /api/log
src/app/api/log/route.js is a client-side error reporting endpoint that forwards structured error payloads to Cloudflare Workers Observability (console logging).
Auth Context and Data Flow
src/context/AuthContext.jsx ("use client") implements a stale-while-revalidate auth pattern:
- On mount,
GET /api/sessionreturns the cookie snapshot immediately — fast first paint. - In the background,
GET /api/proxy?endpoint=/customer/profilerevalidates the snapshot against the live ERP. A401from the ERP clears the zombie session.
The context exposes { user, loading, isLoggedIn, isVerified, login, logout, refresh }. isVerified reflects profile_verified from the customer profile — used by B2B tenants to gate price visibility for approved accounts vs. merely logged-in accounts.
Cart Context and Data Flow
src/context/CartContext.jsx ("use client") manages a dual-mode cart:
- Guest — items stored in
localStorageunder the keyluzine_cart, operated locally. - Authenticated — mutations go through
/api/proxyto the ERP endpoints (/cart/add-to-cart,/cart/remove-from-cart,/cart/list-cart-items); state is refreshed from the server after each mutation.
On login, the guest cart items are sent in the request body (cart: buildCartPayload(cartItems)) so the ERP merges them server-side. After auth, CartContext hydrates from the server cart. On logout, the displayed cart is cleared to prevent guest sessions from seeing the previous user's cart.
Cart items for authenticated users use a lineKey of ${id}::${variantId} so different variants are distinct lines. Stock limits are enforced client-side for guest carts.
src/lib/cart.js exports the mapping helpers: buildCartPayload, normalizeServerCart, mapServerCartItem, and the server-cart mutation functions (addServerCartItem, updateServerCartQty, removeServerCartItem, clearServerCart).
Client Hooks
usePriceVisibility
src/hooks/usePriceVisibility.js centralizes the SHOW_PRODUCT_PRICE + SHOW_PRICE_PUBLIC gate:
const { priceVisible, addToCartVisible } = usePriceVisibility({
showProductPrice, // from resolveStoreSettings
showPricePublic, // from resolveStoreSettings
showAddToCart, // from resolveStoreSettings
});Price is visible when showProductPrice && (showPricePublic || isVerified). Add-to-cart visibility follows price visibility. For B2B tenants (SHOW_PRICE_PUBLIC = false), only accounts with profile_verified = true see prices.
useProductVariants
src/hooks/useProductVariants.js handles variant selection on the product detail page. It operates on the product object produced by formatProductDetail from src/lib/formatters.js, which exposes a normalized product.variants array.
Key return values:
Field | Description |
|---|---|
| Deduplicated attribute/option matrix (e.g. |
| Current UI selection state |
| Updates selection; clears conflicting attributes |
| The variant matching all selected attributes, or |
| Formatted price strings; variant overrides base product |
| Variant quantity overrides base stock |
| Variant images when available, else product images |
| Returns |
| Whether an option is valid given current selection |
Data Formatters
src/lib/formatters.js normalizes raw ERP responses into the shape components render:
Function | Input | Output |
|---|---|---|
| Raw product list item | Card-ready object with |
| Raw product detail response | PDP-ready object including |
| Raw banner API response |
|
| Raw SEO API response |
|
| Both settings responses | Branding/contact object ( |
| Raw service item |
|
| Raw form state | Normalized payload for |
Image paths from the ERP storage are resolved as ${apiBaseUrl}/storage/<path> by toStorageUrl inside formatters.js.