Where we came from
BridgeKit did not appear in a vacuum. It is the declared successor to a stack of bridging patterns that accumulated over years. Understanding what came before is the fastest way to understand why BridgeKit is shaped the way it is.
The predecessors
Section titled “The predecessors”The host app reached native capabilities through four overlapping mechanisms:
| Pattern | What it was | What it cost |
|---|---|---|
NativeModules (raw) | Direct, lowest-level calls into native modules. | Fully manual marshalling, no types, no streaming primitive. |
connectGet* | Per-function Nitro module calls. | One bespoke module per function; codecs by hand. |
feature-actions | Ad-hoc per-feature action handlers. | No contracts, no codegen, types not enforced at the boundary. |
NativeEventEmitterModule | A separate channel for anything push-based. | Event names are strings; emitters leak across runtime reloads. |
Every native ↔ JS surface required wiring across several of these at once: a module to call in, an emitter to subscribe to, hand-written codecs in the middle, and no single place that described the contract. The result was predictable.
The pain, concretely
Section titled “The pain, concretely”These are the failure modes that drove the design — each one is addressed directly by a BridgeKit decision.
No type safety across the boundary
Section titled “No type safety across the boundary”A renamed field or a mistyped event name compiled fine and crashed at runtime. There was no shared definition that both sides were checked against.
BridgeKit’s answer: the contract is the single typed source of truth. The generator bakes a contract hash into both the Kotlin token and the JS descriptor; member sets are diffed at bind time and skew surfaces as one structured warning, not a silent break.
sanitizeParams crashes
Section titled “sanitizeParams crashes”Raw user objects were handed to the bridge’s AnyMap converter, which choked on
unexpected shapes ({ a: undefined }, functions, exotic values).
BridgeKit’s answer: schema-driven encoding is always on, prod included. The codec walks declared fields only, skipping
undefinedand functions — raw user objects never reach the converter. That kills the crash class at the root.
Event-emitter semantics
Section titled “Event-emitter semantics”NativeEventEmitter had no backpressure and no lifecycle story. After a JS runtime reload,
native could keep pushing into dead callbacks; rapid discrete events could be lost.
BridgeKit’s answer: streams replace events. Lossless bounded buffers by default, single multiplexed collection per source, and epoch-scoped callbacks that are released when a runtime tears down.
The render-gating dance
Section titled “The render-gating dance”Reading a native value at render time meant guarding on “is it ready yet?” and threading a loading flag through the tree.
BridgeKit’s answer: state carries a required initial value that seeds both stores before any provider binds.
get()is total;useBridgeStatereturns{ value, status }wherestatusdistinguishesInitial/Available/Unprovided.
Duplication of host capabilities
Section titled “Duplication of host capabilities”The same host capability was re-implemented per feature: getUserSegments ×7, openUrl
×5, closeFeature ×4, height-setters under five different names.
BridgeKit’s answer: shared host contracts owned by the platform team (e.g.
lidlplus.host,axion.widget). Features depend on them; codegen warns when a feature member name shadows a host-contract member.setHeightis defined once.
The migration stance
Section titled “The migration stance”BridgeKit is intentionally a brand-new, isolated package. It does not modify
feature-actions or nitro-actions. The adoption path was deliberate and is now under way:
- BridgeKit shipped and was validated against an on-device acceptance matrix — on both Android (Kotlin) and iOS (Swift), at parity.
- With validation done, the old systems freeze — no new actions, modules or emitters.
- Existing features migrate contract by contract. A thin native adapter can back a
BridgeKit provider with an existing
FeatureActionsHandlerregistration, so migration never requires a dual implementation; a feature can keep a handful of actions on FeatureActions while moving the rest onto a contract. - The first pilot was the highest-value, smallest surface: Connect’s OTP polling became
an
otpCodesstream — discrete codes replacing a poll loop.