Over winter break I went deep on production SDK architecture. I spent two and a half weeks studying JSTag—Lytics' data collection SDK—then pulled out the infrastructure patterns that let it survive scale and third-party integrations.
I validated the extraction by building: SDK Kit (a plugin-based SDK framework), Auth HI! (a Chrome extension built on top of it), and early groundwork for Experience SDK (a personalization runtime).
The thesis is simple: production systems teach composition in a way textbooks can't. When an architecture has lived through constraints (scale, real users, years of evolution), you can learn faster by studying what held. In practice, the biggest wins showed up during development: debugging stayed local because behavior surfaced through events, and iteration stayed fast because low coupling meant changes rarely cascaded.
Why I Studied SDK Architecture
The Repeat-Pattern Moment
During Lytics' first vibeathon in November, I built dev-agent, a semantic code search MCP server. Then I started sketching Auth HI! (a Chrome extension) and Experience SDK (a personalization runtime). Early designs looked identical: plugins, events, configuration layers. I was rebuilding the same architecture three times without understanding why these patterns kept emerging. That's the trigger to study the source of truth.
Why JSTag is Worth Studying
JSTag is Lytics' Web SDK, a cornerstone of how we get data into the CDP (Customer Data Platform). It collects behavioral data from websites and surfaces materialized profiles back to the browser in real time. Originally created by Aaron Raddon (CTO) in 2012, it was rewritten by Chris Thorn (architect/principal) in 2016 with a plugin architecture that became JSTag v3.0. This is what I studied: a system that's survived production constraints (millions of events daily across thousands of domains, third-party integrations, consent management, multiple transport strategies). I focused on the infrastructure patterns that enable that scale and flexibility.
The winter break (Dec 16 - Jan 2) gave me 2.5 weeks to dig in.
Method: Systematic Pattern Extraction
My research followed a spec-driven workflow across four phases:
- Deep Source Analysis - JSTag's codebase: plugin registration, capability injection, event system, specific plugins (Context, Poll, Consent, Queue, Transport)
- Industry Pattern Research - How other SDKs solve similar problems: consent management (Segment, Mixpanel, OneTrust), transport methods (Google Analytics, Adobe), queue/batching patterns (VWO, Sitecore)
- Documentation & Specs - Translated findings into 15+ research notes, formal specs, 9 granular tasks, GitHub issues
- Implementation - Built SDK Kit following the specs, test-driven
How I Studied: Plugin Registration to Event Coordination
I started at the entry point: how plugins get loaded. JSTag's use() function takes a plugin closure and calls it with three injected parameters: plugin (capabilities), instance (SDK), config (configuration). Each plugin is a function that receives everything it needs explicitly. No reaching for globals, no hidden dependencies. The plugin object provides five capabilities: ns() to claim a namespace, defaults() to set configuration, expose() to add public APIs, and emit()/on() for event coordination. This is dependency injection applied to plugins: explicit, testable, portable. The event system enables loose coupling. Plugins communicate via events with wildcard pattern matching (on('user:*') matches emit('user:login')). Reading the code, I recognized these patterns: Microkernel (minimal core + plugins), Observer (event system), Dependency Injection (capability injection). They're composed specifically for SDK architecture.
// Plugin registration: inject capabilities
function use(closure: PluginClosure): SDK {
const plugin = { ns, defaults, expose, emit, on }; // Bound capabilities
closure(plugin, this, this.config);
return this;
}
// Plugin: functional, explicit dependencies
function storagePlugin(plugin, instance, config) {
plugin.ns('storage');
plugin.defaults({ storage: { prefix: 'sdk_' } });
plugin.expose({ storage: { get, set, remove } });
instance.on('sdk:init', () => plugin.emit('storage:ready'));
}
What JSTag's Composition Gets Right for SDKs
Microkernel + Functional Plugins
JSTag's core is minimal: plugin registration (use), basic utilities, that's it. Everything else comes from plugins: events, config, storage, transport. Plugins aren't classes with inheritance hierarchies; they're functions that receive injected capabilities. This means no fragile base class problem, no diamond inheritance issues, no this binding complexity. For SDK architecture, this matters because the core never changes (stability), features are opt-in (tree-shakeable), and third-parties can extend without touching core code.
// Minimal core
class SDK {
use(pluginFn: PluginFunction): this { /* inject & execute */ }
}
// Functional plugin: just a function
function myPlugin(plugin, instance, config) {
plugin.ns('my.plugin');
plugin.expose({ myMethod() {} });
}
Capability Injection as the Ergonomics Layer
Plugins receive everything they need as parameters: plugin (capabilities), instance (SDK), config (configuration). This makes dependencies explicit. You can see what a plugin can do by looking at its signature. It makes testing trivial by letting you inject mocks instead of real objects. It makes plugins portable with no ambient authority and no reaching into global state. The alternative is plugins that reach for window.SDK or access internal SDK state directly, creating hidden coupling that breaks when you change the SDK or move to a different environment (Node, workers, etc.). Explicit capability injection trades a few extra parameters for clarity and testability.
// Explicit dependencies via injection
function analyticsPlugin(plugin, instance, config) {
const apiKey = config.get('analytics.apiKey'); // Explicit config access
plugin.emit('analytics:ready'); // Explicit event capability
}
Event-Driven Coordination as the Scaling Layer
Plugins coordinate via events, not direct method calls. A storage plugin emits storage:set when data changes; an analytics plugin listens to storage:* and reacts. Neither knows about the other. This loose coupling means you can add, remove, or replace plugins without changing existing code. Wildcard patterns (on('user:*')) make it easy to listen to event families without knowing all specific event types upfront. For SDKs that grow over time with third-party plugins, this prevents the coupling explosion that happens when Plugin A calls Plugin B's methods directly.
// Event-driven: loose coupling
plugin.emit('data:ready', { payload });
instance.on('data:*', (event, data) => { /* react */ });
The Key Insight: Infrastructure vs. Product Shape
JSTag as a product is opinionated and batteries-included: it auto-loads multiple essential plugins because data collection for Lytics requires all of them. But the infrastructure underneath—the plugin registration system, capability injection, event coordination, functional plugin signature—is generic and reusable.
SDK Kit keeps the infrastructure and makes composition explicit. Users compose only what they need. JSTag demonstrates the patterns at scale; SDK Kit makes them easy to apply elsewhere.
| Aspect | JSTag | SDK Kit |
|---|---|---|
| Purpose | Production data collection SDK | Learning instrument & SDK framework |
| Auto-loading | Multiple essential plugins | Zero plugins (user composes) |
| Opinionated | Yes (data collection for Lytics) | No (build any SDK) |
| Plugin extension | Plugin-to-plugin (hold()) | Plugin-to-user (expose()) |
What makes JSTag's composition valuable:
- Each pattern is battle-tested in production systems worldwide
- The specific combination targets SDK architecture needs
- Different from typical analytics SDKs: most provide fixed APIs (Mixpanel), Segment uses a sequential pipeline where plugins transform context objects (Before → Enrichment → Destination → After), JSTag uses event-driven coordination where plugins communicate via
emit()/on()rather than passing context objects through a pipeline
SDK Kit Design
Goals
- Minimal core - Events, config, plugin system. Nothing else.
- Type-safe - Full TypeScript, no
anyin public APIs - Tree-shakeable - Only load plugins you use
- Zero runtime dependencies - No external libs in core
- Platform-agnostic - Works in browser, Node, workers
The Public API Shape
Three injected parameters, five capabilities. That's the surface.
// The SDK
const sdk = new SDK();
sdk.use(storagePlugin);
sdk.use(analyticsPlugin);
await sdk.init();
// The Plugin Signature
function myPlugin(
plugin, // { ns, defaults, expose, emit, on }
instance, // SDK instance (access other plugin APIs)
config // Configuration (get/set)
) {
plugin.ns('my.plugin');
plugin.defaults({ my: { setting: 'value' } });
plugin.expose({ myMethod() {} });
instance.on('sdk:ready', () => plugin.emit('my:ready'));
}
Validation: Two Quick Proofs
Auth HI! (Primary Validation)
What it is: Chrome extension that automatically injects authentication headers into HTTP requests.
Why it's a harsh test: Service worker environment (no DOM, limited APIs), Chrome's declarativeNetRequest constraints, UI ↔ background worker coordination via message passing.
What I learned:
- Chrome extension APIs work cleanly inside plugins (
chromeStoragePlugin,requestInterceptorPlugin) - Event-driven coordination made debugging tractable: behavior surfaced through event streams instead of hidden call chains
- Low coupling kept iteration fast: changing one plugin rarely required touching others
- Plugin isolation made testing easier: mock storage, test interceptor logic independently
const sdk = new SDK({ name: 'auth-header-injector' });
sdk.use(chromeStoragePlugin)
.use(patternMatcherPlugin)
.use(requestInterceptorPlugin);
await sdk.init();
sdk.on('interceptor:*', (event, data) => { /* debug */ });
Experience SDK (Secondary Validation)
What it is: Client-side personalization runtime for modals, banners, inline content.
Why it fits: Many optional feature types (triggers, presentation, forms, tracking), explainability (every decision emits structured events), marketer-friendly (script tag, no build tools required).
What I learned:
- Plugin system made features truly optional (load only what marketers use: exit-intent trigger but no scroll trigger)
- Event-driven explainability works:
trigger:exitIntentemits{timestamp, reason}for debugging - Tree-shaking validated: minimal bundle with just modal plugin, full bundle with all plugins
Open Edges
Building with these patterns surfaced constraints worth noting:
Plugin dependencies: Order matters (storagePlugin before analyticsPlugin), but it's implicit. Documentation can specify requirements for now. Making dependencies declarative (plugin.requires(['storage'])) is an open question.
Plugin-to-plugin capabilities: JSTag's hold() lets plugins extend other plugins. SDK Kit doesn't have this yet. Plugins expose APIs to users, not to other plugins. Whether the added coupling is worth the flexibility depends on use case.
Lifecycle integration tests: Current tests cover plugins in isolation. Testing full lifecycle coordination requires a higher-level harness. Does Plugin A's handler run before Plugin B's listener fires? These are the questions integration tests would answer.
Closing
Studying JSTag shifted the way I frame SDK design. The work stops being a checklist of features and starts looking like composition: how pieces coordinate without locking each other in place. When behavior flows through events and boundaries stay explicit, debugging stays local and iteration stays cheap, even as the system grows.
SDK Kit gave me a contained place to surface those pressures. Auth HI! made them concrete. Experience SDK is where I'll keep pushing them.
That's the part I'm keeping: find something that survived, extract the structure, then validate it by building.
SDK Kit & Products
- SDK Kit Documentation
- SDK Kit on npm
- Auth Header Injector
- Experience SDK
- Respecting Intermediates (previous post)
Referenced SDKs & Frameworks
- Segment Analytics.js (analytics-next) - Pipeline-based plugin architecture
- Mixpanel JavaScript SDK - Traditional fixed API
- Babel - Plugin-based JavaScript compiler
- Webpack - Plugin-based module bundler
- Express - Middleware-based web framework