🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@cross-deck/web

Package Overview
Dependencies
Maintainers
1
Versions
41
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@cross-deck/web

Crossdeck SDK for browsers and Node.js — verified subscriptions, entitlements, and product telemetry in one client.

latest
Source
npmnpm
Version
1.13.5
Version published
Maintainers
1
Created
Source

@cross-deck/web

The Crossdeck SDK for browsers and Node.js. One package, one mental model, every Crossdeck client API in five lines of setup.

npm install @cross-deck/web

Quick start

import { Crossdeck } from "@cross-deck/web";

// 1. Boot once at app start. Synchronous and idempotent.
Crossdeck.init({
  appId: "app_web_xxx",         // from the Crossdeck dashboard
  publicKey: "cd_pub_live_…",   // publishable key, safe in client code
  environment: "production",    // "production" or "sandbox"
});

// 2. Telemetry — fire-and-forget, batched in the background.
Crossdeck.track("paywall_viewed", { variant: "v3" });

// 3. Auth + entitlements happen inside an async boot function (or a
//    React useEffect, or any other async context). Top-level await is
//    not portable across all bundlers.
async function bootCrossdeck() {
  // Wire identify() to YOUR auth state — never hardcode a placeholder.
  await Crossdeck.identify(currentUser.id);
  await Crossdeck.getEntitlements();   // warm the local cache

  // 4. Sync access checks (microsecond reads from cache).
  if (Crossdeck.isEntitled("pro")) {
    showProFeatures();
  }
}

React quick start

For React apps, install Crossdeck once at the root and use the useEntitlement hook from @cross-deck/web/react so components re-render when entitlements arrive:

"use client"
import { useEffect } from "react";
import { Crossdeck } from "@cross-deck/web";
import { useEntitlement } from "@cross-deck/web/react";

export function CrossdeckProvider({ children }) {
  useEffect(() => {
    Crossdeck.init({
      appId: "app_web_xxx",
      publicKey: "cd_pub_live_…",
      environment: "production",
    });
    Crossdeck.getEntitlements();   // warm the cache (fire-and-forget)
  }, []);
  return children;
}

export function ProBadge() {
  const isPro = useEntitlement("pro");
  return isPro ? <span className="badge">Pro</span> : null;
}

useEntitlement subscribes to the SDK's reactive cache via Crossdeck.onEntitlementsChange(), so every component using the hook re-renders the moment entitlements change. SSR-safe: returns false on the server and hydrates correctly on the client.

That's the full happy path.

What it does

  • Auto-tracking, on by default. Sessions, page views, and device info (OS, browser, locale, timezone, screen size, app version) ship from boot. No instrumentation needed for the basics. Disable any of them via autoTrack: { sessions: false } etc.
  • One identity for every device + user. Pre-login events get an anonymousId. After login, identify() links them to your user ID through Crossdeck's identity graph. The SDK persists both so subsequent app launches resume where you left off.
  • Synchronous entitlement reads. getEntitlements() populates a local cache. isEntitled("pro") is a Set lookup — no network call, no waiting.
  • Batched telemetry. track() queues events in memory; the SDK flushes every 5 seconds (configurable) or when the buffer hits 20 events. Network failures re-queue the batch — events aren't lost on a flaky connection.
  • Outdated-version PARK (v1.8.0). If the server ever stops accepting this SDK version's event format (HTTP 426 / sdk_version_unsupported), events are parked, not lost: held in the durable on-device queue (up to 1,000 events across page loads via localStorage), flushing hushes, one console warning names the exact version to update to, and everything delivers automatically after you upgrade. See the durability contract.
  • Boot heartbeat. On init() the SDK pings /v1/sdk/heartbeat so the dashboard's Apps page can show you "last seen" per install. Disable with autoHeartbeat: false.
  • Stripe-style errors. Every async method throws CrossdeckError with type, code, requestId, and status — same shape as Stripe's SDKs, so generic error handlers transfer.

Crossdeck Trust — human-proof at signup

Render the branded Crossdeck Trust panel and mint a single-use attestation that proves a real browser loaded your page. It's a cross-origin iframe (un-restylable, identical on every install); the SDK hands you the token programmatically — no hidden field, no DOM.

// React — @cross-deck/web/react
import { CrossdeckTrust } from "@cross-deck/web/react";

<CrossdeckTrust publicKey="cd_pub_live_…" onToken={(t) => setTrustToken(t.token)} />
// or headless:  const { ref, token, status } = useTrustToken({ publicKey: "cd_pub_live_…" });  // <div ref={ref} />
<!-- Vue — @cross-deck/web/vue -->
<script setup>
import { useTrustToken } from "@cross-deck/web/vue";
const { el, token } = useTrustToken({ publicKey: "cd_pub_live_…" });
</script>
<template><div ref="el" /></template>
// No framework:
const { ready } = Crossdeck.trust.panel({ target: "#cd-trust", publicKey: "cd_pub_live_…" });
const { token } = await ready;   // { token, expiresAt } | { token: null }

Pass your publishable key to the panel directly (the Stripe / Turnstile pattern; it also falls back to init()'s key). Send token to your server and verify it at the gate (crossdeck.trust.gate({ email, ip, token }) in @cross-deck/node). Fail-open: if the panel can't mint (adblocker, offline, outage) you get { token: null }, the signup proceeds, and the server scores the absent token — it never throws and never blocks your form.

Auto-tracked events

EventWhen
session.startedWhen a new session begins — first visit, or the first page load after >30 min of inactivity. A session resumed across a page navigation does not re-fire it. Carries sessionId.
session.endedOn a real session end only: >30 min of inactivity, or an explicit Crossdeck.stop(). Carries sessionId and durationMs.
page.viewedOn initial load + every SPA navigation (history.pushState, replaceState, popstate). Carries path, url, search, hash, title, referrer.

Every event — auto-tracked and developer-emitted — is enriched with the device-info payload below.

Sessions survive page loads (v1.6.0+). A session is a visit, not a page. The SDK persists the session (id, start time, last-activity time, first-touch acquisition) to the same storage as identity, so a full-page navigation on a multi-page site resumes the same session rather than starting a new one. The window is a rolling 30-minute inactivity timer bumped by every tracked event (auto or custom); only real 30-min inactivity or Crossdeck.stop() ends it. A page unload (pagehide/beforeunload) is treated as a navigation, not an end. Quick tab switches (Cmd-Tab) likewise don't end the session — matching GA4's session-window convention. With persistIdentity: false (or a MemoryStorage adapter) the session is in-memory only and resets per page.

Cross-subdomain identity (v1.11.0+). A visitor is one person across every subdomain of your site — your marketing site (example.com) and your app (app.example.com) share one anonymous identity, so first-touch source, journey, and conversion stitch into one timeline instead of splitting at the hop. The SDK scopes its identity cookie to your registrable domain by default (cookieDomain: "auto"); set an explicit cookieDomain: ".example.com" to pin it, or "none" for host-only. Cross-device identity (same person, phone + laptop) is resolved server-side by identify(userId) — this is the browser half.

Per-session acquisition (v0.6.0+): when a session starts the SDK reads window.location.search and document.referrer and captures utm_source, utm_medium, utm_campaign, utm_content, utm_term, plus referrer. It also captures the paid-traffic click ids many platforms send instead of UTMs — gclid, fbclid, msclkid, ttclid, li_fat_id, twclid. Non-empty values are auto-attached to every subsequent event of that session — attribution stays pinned to the entry URL even after SPA route changes strip the params away, and across full-page navigations within the session.

First-touch origin survives the visit (v1.12.0+). The touch that won a visitor now outlives the session that captured it, and the subdomain hop to your app.

Previously a new session re-read the current URL, so a visitor who arrived from a campaign on Monday, returned directly on Tuesday and signed up on Wednesday converted with empty acquisition — the channel that actually won them was overwritten by a visit carrying no params. Now:

  • The first touch carrying a real signal (any utm_* or click id) is persisted write-once. A later session that lands with no params restores it, so the conversion still carries its source.
  • It is stored in both localStorage and the shared registrable-domain cookie — the same dual store as identity — so origin captured on example.com is still known on app.example.com. Without the cookie leg, localStorage is per-origin and every acquisition dies at exactly the hop where signup happens.
  • referrer alone is not a touch. Every visit has one; treating it as a signal would freeze the first referrer forever and stop any genuine campaign registering. referrer always describes the current visit.
  • Not last-touch-wins: a newer campaign is the current touch for its own session, but the stored first touch is never overwritten.

Consent posture is unchanged — with persistIdentity: false (or a MemoryStorage adapter) nothing is persisted and behaviour falls back to the current page.

Campaign-arrival connect (v1.10.0+): when a page is reached from a Crossdeck-tagged campaign link, the landing URL carries an opaque cd_ref tag (a partner contact id — never an email). On session start the SDK posts it once to the arrival endpoint so the backend can bind this anonymous session to the tagged person and pull their integration record (e.g. HubSpot deals/pipeline) onto their journey — no login required. It's automatic (no method to call), browser-only, fire-and-forget (never throws, never blocks page init), and idempotent server-side. A later real identify(userId) still converges the same person.

Auto-attached device info

Every event's properties is enriched with whatever the SDK can detect:

{
  os: "macOS" | "iOS" | "Android" | "Windows" | "Linux",
  osVersion: "14.4",
  browser: "Safari" | "Chrome" | "Firefox" | "Edge" | "Opera",
  browserVersion: "17.5",
  locale: "en-US",
  timezone: "Africa/Johannesburg",
  screenWidth: 2560,
  screenHeight: 1440,
  viewportWidth: 1440,
  viewportHeight: 900,
  devicePixelRatio: 2,
  appVersion: "1.2.3",   // only when you set Crossdeck.init({ appVersion })
}

No fingerprinting, no IP collection on the event document, no canvas hashing. Privacy by default. Caller-supplied properties always override auto-detected ones, so you can override appVersion per event if you A/B builds.

API

Crossdeck.init(options)

Boot the client. Idempotent — calling twice with the same options is fine. (Crossdeck.start() is kept as a deprecated alias for v0.2.x callers.)

Crossdeck.init({
  appId: "app_web_xxx",               // required — from the dashboard
  publicKey: "cd_pub_live_…",         // required
  environment: "production",          // required — "production" | "sandbox"
  baseUrl: "https://api.cross-deck.com/v1",  // override for self-host or emulator
  appVersion: "1.2.3",                // attached to every event as properties.appVersion
  autoTrack: true,                    // default — sessions, page views, device info
  // …or granular: autoTrack: { sessions: false } keeps page views + device info
  autoHeartbeat: true,                // default; set false for high-frequency boots
  eventFlushBatchSize: 20,            // default
  eventFlushIntervalMs: 5_000,        // default
  storage: customStorage,             // override the persistence adapter
  debug: false,                       // verbose §16 debug signals when true
});

Crossdeck checks the key prefix matches environment: cd_pub_test_… must declare "sandbox", cd_pub_live_… must declare "production". Mismatches throw CrossdeckError({ code: "environment_mismatch" }) at init time so a typo can't silently route prod telemetry into sandbox dashboards.

The publishable key is safe to ship in client code. Crossdeck enforces origin allowlists (web), bundle-ID binding (mobile), and rate limits at the edge — see docs/api-keys for the full security model.

await Crossdeck.identify(userId, options?)

Link the anonymous device to a developer-supplied user ID. Persists the resolved Crossdeck customer ID for follow-up reads. Returns the AliasResult:

{
  object: "alias_result",
  crossdeckCustomerId: "cdcust_…",
  linked: [
    { type: "developer", id: "user_847" },
    { type: "anonymous", id: "anon_…" },
  ],
  mergePending: false,   // see "Merge candidates" below
  env: "production",
}

If mergePending: true, both identifiers already pointed at different customers. Crossdeck never silently merges — your dashboard's operations queue surfaces the merge for human confirmation.

Entitlement-cache isolation (v1.4.0). Every identify(userId) switches the local entitlement cache to a per-user storage slot — localStorage["crossdeck:entitlements:<sha256(userId)>"] — and unconditionally wipes the in-memory snapshot. A user-switch on a shared device CANNOT cross-read a prior user's cached entitlements, even if the in-memory clear is somehow skipped, because the storage keys are physically separate. reset() then wipes every per-user slot on the device (logout-grade).

Read-cost cross-match (v1.9.0). If @cross-deck/buckets/web is installed, identify(userId) also tells the browser read-cost collector who the session is — so every database read this session makes attributes to that user, and you can see which user's which feature is driving your read bill (npx @cross-deck/buckets, or the dashboard). reset() clears it on logout. Decoupled and zero-dependency — the SDK never imports Buckets; with no collector installed it's a silent no-op. This is operational, not an analytics event.

Idempotency-Key (v1.4.0). Every syncPurchases() derives a deterministic Idempotency-Key from the request body — same signed transaction in produces the same key out. The backend short-circuits repeats with idempotent_replay: true in the response, so a network blip / app crash mid-flight that re-fires the same purchase doesn't double-process. The key is a UUID-shaped SHA-256 digest of crossdeck:purchases/sync:<rail>:<jws|token>, so two SDKs reporting the same Apple transaction land on the same key.

await Crossdeck.getEntitlements()

Fetch the current customer's active entitlements. Returns an array of PublicEntitlement and updates the local cache.

const ents = await Crossdeck.getEntitlements();
// [{ object: "entitlement", key: "pro", isActive: true, validUntil: 1717891200, source: {...}, updatedAt: ... }]

Crossdeck.isEntitled(key) → boolean

Synchronous read from the local cache. Returns false until you've called getEntitlements() once (or purchaseApple() resolved). After that, it's a Set lookup — call as often as you want.

Crossdeck.track(name, properties?)

Queue a telemetry event. Returns immediately. Events flush in batches; force a flush with flush():

Crossdeck.track("checkout_started", { product: "annual_pro" });
// …later, e.g. before page unload:
window.addEventListener("beforeunload", () => {
  void Crossdeck.flush();
});

Event names match [A-Za-z0-9_.\-:]+, max 128 chars. Properties are JSON-serialisable, max 8 KB per event after JSON encoding.

An empty event name throws synchronously with CrossdeckError({ code: "missing_event_name" }) — the JS SDKs reject invalid input at the call site with a typed, catchable error (the Swift SDK's fire-and-forget surface drops + debug-logs instead; same invariant — invalid input never crashes your app and never reaches the wire — platform-native signalling). Codified as the invalid-input-rejected-natively contract.

In debug: true mode the SDK warns (one signal per call) when property keys look like PII — email, password, token, secret, card, phone. Crossdeck never strips fields automatically; the warning is so accidental leaks surface during development, not in prod logs.

await Crossdeck.syncPurchases(input)

Forward purchase evidence (Apple StoreKit 2) directly to Crossdeck for verification — closes the purchase-to-entitled latency from seconds to milliseconds (faster than waiting for the App Store webhook). (purchaseApple() is kept as a deprecated alias.)

// Inside an async transaction handler — wrap top-level awaits.
async function forwardTransaction(transaction) {
  await Crossdeck.syncPurchases({
    signedTransactionInfo: transaction.jsonRepresentation, // from StoreKit 2
    signedRenewalInfo: subscription.signedRenewalInfo,     // optional
    appAccountToken: "uuid-…",                             // optional
  });
}

Stripe and Google purchases are verified via webhooks (Stripe Connect platform endpoint, Google Play RTDN) — there's no SDK-side push for those.

await Crossdeck.heartbeat()

Manually send a heartbeat. Called automatically by init() unless autoHeartbeat: false. Returns the readiness summary the dashboard uses to display SDK installation status.

Crossdeck.reset()

Wipe persisted identity + EVERY per-user entitlement cache slot on this device + queued events. Call on logout. The next session generates a fresh anonymousId and starts a clean identity-graph entry. The per-user scope of the cache wipe (introduced v1.4.0) means a shared-device logout cannot leave a separate user's entitlements readable from localStorage.

Call it on a real sign-out only — never on your auth listener's "not signed in yet" boot callback. reset() is not a no-op on an anonymous device: it deletes the anonymousId from every store (including the shared cross-subdomain cookie) and mints a new one. Resetting on boot re-anonymises the visitor on every page load, so the pre-signup journey they arrived with — true source, UTMs, what they read — is orphaned from the account they create moments later. Latch that you have actually seen a signed-in user before treating a null as a sign-out. See Guard reset().

Crossdeck.flush()

Force-flush the in-memory event queue. Useful before page unload or when shutting down a script. (flushEvents() is kept as a deprecated alias.)

Crossdeck.setDebugMode(enabled)

Toggle the verbose debug-signal vocabulary at runtime (NorthStar §16). When enabled, the SDK emits a fixed set of console.info lines tagged [crossdeck:sdk.<signal>] for sdk.configured, sdk.first_event_sent, sdk.no_identity, sdk.purchase_evidence_sent, sdk.environment_mismatch, sdk.sensitive_property_warning, and sdk.parked (fired once when the queue parks on an outdated-version rejection).

Crossdeck.diagnostics()

Diagnostic snapshot — useful for development consoles and bug reports:

{
  started: true,
  anonymousId: "anon_…",
  crossdeckCustomerId: "cdcust_…" | null,
  developerUserId: "user_…" | null,
  sdkVersion: "1.9.1",
  baseUrl: "https://api.cross-deck.com/v1",
  entitlements: { count: 2, lastUpdated: 1717891200000 },
  events: { buffered: 0, dropped: 0, inFlight: 0, lastFlushAt: 1717891200000, lastError: null },
}

Bank-grade contracts

The SDK ships its own contracts registry — every behavioural guarantee the SDK makes (per-user cache isolation, deterministic Idempotency-Key, queue durability, etc.) lives in contracts/**/*.json at the monorepo root and is bundled into every release. The customer's lockfile pins SDK code + contracts atomically — drift between what the SDK does and what it claims is structurally impossible. See contracts/README.md for the full architecture.

CrossdeckContracts — typed access to the bundled registry

import { CrossdeckContracts } from "@cross-deck/web";

CrossdeckContracts.all();                              // enforced contracts only
CrossdeckContracts.allIncludingHistorical();           // + proposed + retired
CrossdeckContracts.byId("per-user-cache-isolation");
CrossdeckContracts.byPillar("entitlements");
CrossdeckContracts.withStatus("proposed");
CrossdeckContracts.findByTestName("identify(B) makes A's entitlements unreachable from in-memory");
CrossdeckContracts.sdkVersion;        // "1.9.1"
CrossdeckContracts.bundledIn;         // "@cross-deck/web@1.9.1"

The Contract type is exported alongside; the binary-stability promise (which fields are guaranteed across patch/minor releases) is documented inline on src/contracts.ts and in contracts/README.md.

Crossdeck.reportContractFailure(input) — surface contract test failures

When a contract test asserts and fails — in your CI, a dogfood run, or a customer integration test — fire a typed crossdeck.contract_failed event over the Crossdeck reliability channel. This is one-way operational telemetry to the Crossdeck operations team (Privacy Policy §6, "Flow B"); it never enters your track() pipeline, never shows in your dashboard, never bills against your event quota. The wire shape is schema-locked at contracts/diagnostics/contract-failed-payload-schema-lock.json:

Crossdeck.reportContractFailure({
  contractId: "per-user-cache-isolation",
  failureReason: "expected isolation across user switch, got cross-read",
  runContext: process.env.CI ? "ci" : "dogfood",
  runId: process.env.GITHUB_RUN_ID ?? crypto.randomUUID(),
  testRef: {
    file: "tests/entitlement-cache-isolation.test.ts",
    name: "identify(B) makes A's entitlements unreachable from in-memory",
  },
});

No new endpoint, no special ingest path — the event lands in the same pipeline every other track() call does. It surfaces immediately in the dashboard's live event feed, the breakdown chart (group by contract_id, sdk_platform), and any alert rule with event = crossdeck.contract_failed.

Properties stamped on the wire:

PropertySource
contract_idcaller
sdk_version, sdk_platformauto-stamped by the SDK
failure_reason, run_context, run_idcaller
test_file, test_nameset when testRef is provided
device_classoptional, set by caller (categorical bucket)
verification_phaseauto-stamped when the runtime verifier layer (below) is the source — boot or hot_path

The wire shape is locked by contracts/diagnostics/contract-failed-payload-schema-lock.json; per-SDK assertion tests gate it on every release.

For per-test-framework hooks (Vitest afterEach, etc.) see contracts/README.md § Reporting contract failures.

Runtime self-verification (v1.5.1+)

The Web SDK actively tests its own structural contracts at runtime — not just in Crossdeck's CI. Every init() runs a boot self-test; every relevant SDK operation (identify(), track(), syncPurchases(), error parse) is observed by a hot-path verifier that asserts the contract claim held. PASS results stream to your devtools console; FAIL results stream silently to Crossdeck's reliability workspace via a dedicated one-way channel that never touches your appId, your dashboard, or your event quota.

Crossdeck.init({
  appId: "app_web_xxx",
  publicKey: "cd_pub_test_xxx",
  environment: "sandbox",
  // Defaults shown — both are true in dev (NODE_ENV !== "production"),
  // false in production. Set explicitly to override.
  verifyContractsAtBoot: true,   // run the boot self-test on init
  logVerifierResults: true,      // print PASS lines to console
  // Sovereignty kill-switch — disables the entire layer end-to-end:
  // no verifiers run, no console output, no reliability-channel
  // writes. Default false.
  disableContractAssertions: false,
});

What you'll see in your devtools console (with logVerifierResults: true):

[crossdeck] Contract self-verification — running 5 tests
   ✓ per-user-cache-isolation — slot rotated A:7c44…ee20 → B:a3f2…01b9
   ✓ idempotency-key-deterministic — apple JWS → a66b1640-…
   ✓ error-envelope-shape — { type, code, message, request_id } parsed
   ✓ flush-interval-parity — eventFlushIntervalMs = 2000ms
   ✓ super-property-merge-precedence — caller > super > device
[crossdeck] Self-verification passed — 5 passed, 0 failed (8ms)

[crossdeck.identify] ✓ per-user-cache-isolation — slot rotated _anon → a3f2…01b9
[crossdeck.track]    ✓ super-property-merge-precedence — caller(2) > super(1) > device

Operator's-view configuration. The same verifyContractsAtBoot + logVerifierResults flags are exposed as a per-app remote config in the dashboard at /dashboard/apps/. Flip a toggle there and the next SDK boot picks it up — no code change, no redeploy. Code wins on precedence (code option > dashboard remote config > DEBUG/RELEASE default), so engineers retain ultimate control. disableContractAssertions is intentionally code-only — see docs/contracts/ § sovereignty.

Full architecture + per-verifier walkthrough at docs/contracts/ § Runtime self-verification.

Errors

Every async method can throw CrossdeckError. Synchronous methods throw on configuration mistakes (calling before init(), invalid key prefix, env mismatch).

import { CrossdeckError } from "@cross-deck/web";

try {
  await Crossdeck.identify("user_847");
} catch (err) {
  if (err instanceof CrossdeckError) {
    console.error(err.type, err.code, err.requestId);
    if (err.code === "invalid_api_key") {
      // ...
    }
  }
}

Error fields:

FieldWhat it is
typeOne of authentication_error, permission_error, invalid_request_error, rate_limit_error, internal_error, network_error, configuration_error, version_error. Same vocabulary the backend uses.
codeSpecific machine-readable code, e.g. invalid_api_key, origin_not_allowed, rate_limited, network_error, sdk_version_unsupported.
minVersion, surfaceSet on version_error (HTTP 426) responses — the exact SDK version to update to. The queue PARKs on this error instead of dropping (see "Outdated-version PARK" above).
messageHuman-readable description.
requestIdServer-issued ID. Echo it in support tickets — we'll have a one-line log entry that explains the decision.
statusHTTP status code if the error came from an API response.

Node usage

The SDK works the same way in Node 18+:

import { Crossdeck, MemoryStorage } from "@cross-deck/web";

Crossdeck.init({
  appId: process.env.CROSSDECK_APP_ID!,
  publicKey: process.env.CROSSDECK_PUBLIC_KEY!,
  environment: "sandbox",         // or "production"
  storage: new MemoryStorage(),   // session-only, no localStorage
  autoHeartbeat: false,           // skip the boot ping in scripts
});

For server-side flows where you need to read any customer's state (not just the caller's), use the secret key path — that ships when the /v1/server/* endpoints land.

Security

Publishable keys aren't secrets — they're identifiers, safe to ship in client code. See docs/api-keys for the full security model: how keys are stored, how requests are verified, what's enforced where. Highlights:

  • Origin allowlists on web keys (configured in the Crossdeck dashboard) reject requests from unauthorised origins.
  • Tenant isolation — a leaked key can read its own project's customer data only, never another tenant's.
  • Env partition — a cd_pub_live_… key cannot read cd_pub_test_… data and vice versa.
  • No raw payment credentials ever pass through this SDK or sit in a Crossdeck database. Apple .p8s, Stripe secret keys, Google service-account JSON — all in Google Cloud Secret Manager, runtime-only access from the Crossdeck backend.

Identity & cookies (v0.6.0+)

The SDK persists anonymousId and crossdeckCustomerId so a returning user keeps the same Crossdeck identity across page loads. By default in browsers it writes to BOTH localStorage (primary) and a 1st-party document.cookie (secondary, Path=/, Max-Age=2y, SameSite=Lax, Secure over HTTPS). The redundancy keeps "10k unique visitors" actually meaning 10k humans even when one store is wiped by ITP, private browsing, or "clear site data."

The cookie holds only the same anonymousId already in localStorage — no fingerprintable data, no PII. Same security posture as Stripe, Segment, and PostHog's 1st-party identity cookies.

Disabling persistence. Customers running strict consent flows (e.g. cookies disabled until the visitor opts in via a consent banner) should pass persistIdentity: false to Crossdeck.init. That switches the SDK to in-memory only — no localStorage, no cookie, identity is recreated on every page load. Re-init with persistIdentity: true once consent lands.

Crossdeck.init({
  appId, publicKey, environment,
  persistIdentity: false,  // strict consent — opt in later
});

Cookie disclosure. If your privacy policy enumerates cookies, list this one as a "1st-party functional / analytics cookie used to keep the same visitor identity across page loads." The Crossdeck cookie name uses the configured storagePrefix (default crossdeck:) followed by anon_id (and cdcust_id once a user signs in).

Versioning

This package follows semver. The wire-format types (PublicEntitlement, AliasResult, etc.) are duplicated from the backend's v1-types.ts — they're the stable contract, not a shared module. Breaking changes to those types only ship in major versions.

License

MIT.

Keywords

subscriptions

FAQs

Package last updated on 28 Jul 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts