Platform fingerprints — Cartbase detection

Every Cartbase-powered storefront emits a small, deliberate set of signals so platform-detection tools (Wappalyzer, BuiltWith) classify the site as Ecommerce — the same mechanism Shopify uses (window.Shopify, X-ShopId, _shopify_s). Full spec: docs/cards/platform-fingerprints.md. Category discipline: these are the ONLY signals emitted — nothing that reads as a framework, CMS, or website builder, and NEVER on /api/admin/*.

SDK modules: @cartbase/storefront/platform, @cartbase/storefront/lib/cookie-names.


Response header — x-cartbase-version

  • Purpose — every /api/store/* response carries the platform version, so a single HTTP response proves the platform without loading a page.
  • Auth — none required to observe the header; it's stamped regardless of whether the underlying call succeeds or errors (both withLogging return paths).
  • Value — the platform's own semver (src/lib/platform/identity.tsPLATFORM_VERSION).
  • Scope/api/store/* only. /api/admin/* never carries it (admin is not a storefront).
# A successful call carries the header.
HEADERS=$(curl -sfD - -o /dev/null "$BASE/api/store/consent" -H "x-client-id: $CLIENT_ID")
echo "$HEADERS" | grep -qi '^x-cartbase-version: '

# An ERROR response (missing x-client-id → 400 missing_client_id) still
# carries it — the fingerprint is not a happy-path-only afterthought.
ERR_HEADERS=$(curl -sD - -o /dev/null "$BASE/api/store/consent")
echo "$ERR_HEADERS" | grep -qi '^x-cartbase-version: '

# Admin surface never carries it (even though the same withLogging wrapper
# runs the admin route too, once auth resolves).
ADMIN_HEADERS=$(curl -sD - -o /dev/null "$BASE/api/admin/users/me")
! echo "$ADMIN_HEADERS" | grep -qi '^x-cartbase-version: '

Meta tag — <meta name="generator" content="Cartbase" />

  • Purpose — the classic "what built this site" signal, read by every detection tool's HTML scanner.
  • SDKcreateStorefrontMetadata(overrides?: Metadata): Metadata (@cartbase/storefront/platform). Call it in the root layout instead of hand-writing export const metadatagenerator always resolves to PLATFORM_NAME ("Cartbase") and cannot be shadowed by a stray key in overrides (the helper's own assignment applies AFTER the spread).
  • Components — every storefront's app/layout.tsx (see examples/storefront/src/app/layout.tsx for the reference wiring).
// app/layout.tsx
import { createStorefrontMetadata } from "@cartbase/storefront/platform"

export const metadata: Metadata = createStorefrontMetadata({
  title: "My Store",
  description: "...",
})
// → renders <meta name="generator" content="Cartbase" /> on every page

This is a build-time Next.js Metadata API call, not a store-API endpoint — no curl to demonstrate; verified by tests/unit/storefront-lib.test.ts ("platform — generator metadata + window.Cartbase").


JS global — window.Cartbase

  • Purpose — the strongest detector signal (how window.Shopify works): a synchronous inline script sets window.Cartbase = {version, storeId} before hydration.
  • Shape{ version: string, storeId: string }. No PII, no email, no secret — store id + platform version only.
  • SDK<PlatformInit storeId={...} /> (@cartbase/storefront/platform), a server component shaped exactly like <ConsentInit /> (@cartbase/storefront/tracking/consent-init): one synchronous inline <script>, mounted once near the top of <body>.
  • Components — mount once per app, next to <ConsentInit />. Never mount in an admin bundle.
// app/layout.tsx, inside <body>, first children:
<ConsentInit />
<PlatformInit storeId={CARTBASE_CLIENT_ID} />

Also build-time/render-only — no store-API curl to demonstrate; the exact script shape (window.Cartbase={"version":"...","storeId":"..."};, no extra keys) is verified by tests/unit/storefront-lib.test.ts.


Cookies — _cartbase_cart (was _barter_cart_id)

  • Purpose — the cart-id cookie. The app still OWNS setting it (the SDK never persists the cart — see carts.md); the library owns the NAME so every consumer emits the same fingerprint instead of inventing its own prefix.
  • SDKCART_COOKIE, LEGACY_CART_COOKIE, readCartCookie(get) (@cartbase/storefront/lib/cookie-names). readCartCookie prefers the new name and falls back to the legacy _barter_cart_id name, so an existing visitor's cart survives the rename. Every WRITE uses the new name only.
  • SESSION_COOKIE (_cartbase_session) — the name is reserved for a future cookie-backed customer session. Nothing sets it today: auth is pure Bearer-JWT, persisted by the consuming app (see auth.md). Defined now so a future session mechanism launches with the fingerprint-correct name.
import { CART_COOKIE, readCartCookie } from "@cartbase/storefront/lib/cookie-names"

const cartId = readCartCookie((name) => jar.get(name)?.value) // reads either name
jar.set(CART_COOKIE, cart.id, { path: "/", maxAge: THIRTY_DAYS }) // writes the new name only

No store-API curl to demonstrate (this is a client-cookie contract, not a server response); verified by tests/unit/storefront-lib.test.ts ("platform — cookie names + back-compat read").