Web (multi-package)

Web SDK overview

A multi-package monorepo — install only what you use. Bundle-size table, plugin architecture, and how the seven web packages compose into a single Sankofa surface.

The Sankofa Web SDK is the only SDK in the matrix that ships as multiple packages. Other client platforms (Flutter, RN, iOS, Android) bundle every supported product into a single artifact; web splits them apart so your users only download the bytes for the products you actually use.

If you only need analytics, you install one package and pay for one bundle. If you want feature flags too, you install another. This page explains the philosophy, lists the packages, and shows the bundle sizes so you can plan accordingly.

The seven packages

PackageBundle (gzipped)Purpose
@sankofa/browser~6.5 KBCore analytics — init, track, identify, setPerson, flush, reset. The base every other package extends.
@sankofa/catch~4 KBError capture, breadcrumbs, source-mapped stack traces.
@sankofa/switch~3 KBFeature flags + variant assignment + per-call exposure tracking.
@sankofa/config~2.5 KBTyped remote config — getString, getBool, getInt, getNumber, getJSON.
@sankofa/pulse~5 KBBehavior-triggered surveys with in-app rendering.
@sankofa/replay-rrweb~28 KBrrweb-powered DOM replay with input masking.
@sankofa/react~2 KBReact hooks — useFlag, useConfig, useExperiment, plus the <SankofaProvider> context.

@sankofa/browser is required. Everything else is optional.

A typical small app installing analytics + flags + config lands in ~12 KB gzipped — comparable to or smaller than other SaaS analytics SDKs in the same space.

The plugin architecture

Every non-core package is a plugin that you register at init time:

TypeScriptsrc/sankofa.ts
import { Sankofa } from "@sankofa/browser";
import { catchPlugin } from "@sankofa/catch";
import { switchPlugin } from "@sankofa/switch";
import { configPlugin } from "@sankofa/config";

await Sankofa.init({
apiKey: process.env.NEXT_PUBLIC_SANKOFA_KEY!,
endpoint: "https://api.sankofa.dev",
plugins: [
  catchPlugin(),
  switchPlugin({ defaults: { new_checkout: false } }),
  configPlugin({ defaults: { max_upload_mb: 25 } }),
],
});

Plugins are evaluated in registration order. Each plugin gets exclusive ownership of its own decision-handshake module (Switch owns flags, Config owns config items) — there's no namespace collision.

You can register plugins after init too, but they won't participate in the first decision handshake. Best practice: register every plugin at init.

Tree-shaking

Every package exports named symbols only. Dead-code elimination in modern bundlers (esbuild, Vite, Rollup, Webpack 5+) drops the unused parts of the SDK at build time.

A practical example: if you import Sankofa from @sankofa/browser but never call setPerson, the people-profile code is dropped from your bundle. Same for never-used flag variants, etc.

ESM vs CJS

Every package ships dual entry points:

  • ESM — the .mjs build, the recommended default. Used automatically by Vite, Next.js, Remix, and any bundler that respects the exports field.
  • CJS — the .cjs build, for legacy Node tooling and older bundlers.

There's no separate @sankofa/browser/esm import path — the bundler picks the right one from package.json's exports map.

Browser support

FeatureRequired browser
@sankofa/browser coreLast 2 versions of Chrome, Firefox, Safari, Edge; modern mobile browsers.
@sankofa/replay-rrwebChrome 70+, Firefox 65+, Safari 13+. (rrweb dependency.)
Service-worker offline queueBrowsers with IndexedDB and Service Workers — the same matrix as core.

We don't ship polyfills. If you target IE11 or pre-2019 browsers, you'll need to polyfill Promise, Map, and fetch yourself. We test against the latest two stable releases of every supported browser on every commit.

Initialize patterns by framework

TypeScriptapp/sankofa.ts
"use client";

import { Sankofa } from "@sankofa/browser";

let initialized = false;
export function ensureSankofa() {
if (initialized || typeof window === "undefined") return;
initialized = true;
Sankofa.init({
  apiKey: process.env.NEXT_PUBLIC_SANKOFA_KEY!,
  endpoint: "https://api.sankofa.dev",
});
}

Once initialized, every package shares the same client

You only call Sankofa.init once. Every plugin you register at init time runs against that single shared client — they share the queue, the session, the identity, and the decision handshake. There's no cross-package state synchronization to worry about.

Where to go next

Edit this page on GitHub