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
withLoggingreturn paths). - Value — the platform's own semver (
src/lib/platform/identity.ts→PLATFORM_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.
- SDK —
createStorefrontMetadata(overrides?: Metadata): Metadata(@cartbase/storefront/platform). Call it in the root layout instead of hand-writingexport const metadata—generatoralways resolves toPLATFORM_NAME("Cartbase") and cannot be shadowed by a stray key inoverrides(the helper's own assignment applies AFTER the spread). - Components — every storefront's
app/layout.tsx(seeexamples/storefront/src/app/layout.tsxfor 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 pageThis 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.Shopifyworks): a synchronous inline script setswindow.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.
- SDK —
CART_COOKIE,LEGACY_CART_COOKIE,readCartCookie(get)(@cartbase/storefront/lib/cookie-names).readCartCookieprefers the new name and falls back to the legacy_barter_cart_idname, 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 onlyNo 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").