@ikeboy003/cart
v0.4.0
Published
Headless cart primitive: local-first state + a configurable durable sync path. Framework-agnostic core, optional React binding. No POS/payment/store logic baked in.
Maintainers
Readme
@ikeboy003/cart
Headless cart primitive. Local-first cart state + a configurable durable sync
path. Import the engine, inject config, design your own UI. The library knows
nothing about any POS, payment provider, store, framework, or item kind — like
postgrest-rs, behaviour comes from config, not baked-in business logic.
Install
npm i @ikeboy003/cartUse
import { createCart, httpSync } from "@ikeboy003/cart";
const cart = createCart({
namespace: "plane-things", // scopes storage keys + cart id
storage: localStorage, // or memory / custom Storage
sync: httpSync({ endpoint: "/api/cart" }), // your durable write path
debounceMs: 400,
});
cart.add({ id: "tee:L", quantity: 1, priceCents: 3199, title: "Formation Tee" });
cart.count; // 1
cart.subtotalCents; // 3199
await cart.flush(); // force the durable mirror current (call before checkout)React (separate entry, optional):
import { CartProvider, useCart } from "@ikeboy003/cart/react";
<CartProvider namespace="plane-things" sync={httpSync({ endpoint: "/api/cart" })}>
<App />
</CartProvider>;
const { items, count, subtotalCents, add, remove, updateQuantity, clear, flush } = useCart();What it does (and only this)
local cart state · add · remove · updateQuantity · count / subtotalCents
· debounced sync · flush · restore from storage + durable · the
CartSyncAdapter interface.
It does not know about Square, Toast, Stripe, Cloudflare/D1, auth, fulfillment, or item kind. Those are the app's concern.
Line shape
The minimum for cart mechanics is { id, quantity, priceCents }; title/sku
are optional and metadata passes through untouched. The engine is generic, so
map your product into a typed line and keep type-safety end to end:
type MerchLine = CartItem & { metadata: { variationId: string; team: string } };
const cart = createCart<MerchLine>({ namespace: "plane-things", /* … */ });When does sync happen
- Every mutation →
localStoragesynchronously (the live cart, zero latency). - Durable push → coalesced ~
debounceMsafter the last change, plus a forced flush onvisibilitychange: hidden/pagehide(so the backend is current before the tab closes / checkout).keepalivelets that final PUT land during unload. restore()→ pulls from the durable store once when the local cart is empty (returning / cross-device). Local wins when both exist.
The flow it slots into
frontend cart (this lib)
-> durable cart row (your sync adapter; usually Cloudflare D1)
-> checkout / payment
-> payment webhook
-> graft pipeline looks up the durable cart
-> creates merchant/POS records (Square today, Toast tomorrow — swappable)The durable row is the bridge. The app owns the /api/cart route (writes
whatever store it uses) and the post-payment pipeline. This package owns only
the left edge.
Sync adapter
httpSync posts to an app route:
PUT {endpoint} body: CartSnapshot -> 2xx (upsert)
GET {endpoint}?id=&namespace= -> { items } | 404Implement CartSyncAdapter yourself to target anything else (direct DB client,
KV, IndexedDB, …) — push(snapshot) / pull(id, namespace).
