<!-- docs/ADR/2026-07-4.0-boundary.md -->

# ADR: Unified BLE 4.0 native and desktop boundary protocols

**Status:** Accepted design baseline
**Date:** 2026-07-25
**Supersedes:** the deleted dual-byte, Electron fallback, and native-port boundary decisions

## Decision

There is one authoritative in-memory contract and separate, versioned native
and IPC wire projections. Boundaries carry serializable records, paths,
generations, operation correlations, errors, and owned binary values; they do
not carry live object references, global numeric handles, or product policy.
The production projections are
[`primitives.ts`](../../src/backend-contract/primitives.ts),
[`electron.ts`](../../src/backend-contract/electron.ts), and the explicit host
declarations under [`src/backend-contract/host`](../../src/backend-contract/host).

## React Native binary protocol decision

React Native 0.86 and Expo 57 remain supported floors. 4.0 uses exactly one
owned, versioned C++ JSI binary transport for normal radio payloads. TypeScript
TurboModule Codegen may be used only for supported control/bootstrap shapes;
its inability to generate binary typed-array signatures neither changes the
floor nor authorizes another byte path. The Phase-0 experiments that established
this constraint were deleted at promotion, as required by G1. The accepted
production direction is implemented by
[`rn-jsi-binary-runtime.ts`](../../src/native-protocol/rn-jsi-binary-runtime.ts),
the shared native protocol under [`native/protocol`](../../native/protocol), and
the Android and Apple JSI bindings.

- The control/bootstrap module may install and negotiate the versioned JSI
  boundary but never transports BLE bytes.
- The Android installer is `installExecutionRuntime` on the negotiated control
  module. It installs exactly one `__unifiedBleNativeProtocolV2` host object
  whose only byte primitives are retain, independent-copy delivery, release,
  and retained-resource counters. The object keeps only a weak lease to the
  negotiated attachment; close or module invalidation makes every later call
  fail closed instead of reviving an attachment or retaining a buffer.
- Every request, result, and notification uses that one transport, validates
  protocol version and payload size, honors `byteOffset`/`byteLength`, copies or
  explicitly transfers according to the ownership record, and creates
  independent delivered `Uint8Array` values.
- Native work receives a serializable opaque correlation that includes the
  active backend generation and dispatch epoch. It is never a public caller
  transaction ID. Cancellation, timeout, destroy, reset, and late completion
  choose exactly one terminal record under the shared arbiter.
- Normal radio paths never use Base64, a parallel bridge, a compatibility
  fallback, or numeric native handles. A native load, Codegen result, or header
  compile is not live-radio evidence.

Native protocol, backend-contract, capability-schema, event-schema, and trace
axes are independently negotiated. A malformed record, range mismatch, unknown
required field/event, detached/oversize byte value, or stale path fails closed
before radio dispatch. Optional additive fields have the exact negotiated
schema rule; silent downgrade is prohibited.

The Android link-control command/result additions (`requestPriority`, `readPhy`,
`requestPhy`, and `readMtu`) preserve native payload protocol v2 but change the
generated command/field/result ABI. They therefore require ABI v3. The React
Native boundary offers and validates ABI v3 independently from protocol v2, and
an installed ABI-v2 native binary is rejected before JSI runtime installation or
radio admission. There is no extension downgrade or compatibility shim.

## Electron IPC decision

Electron main is the only production radio owner. A sandboxed renderer receives
a narrow preload proxy for IPC protocol v2; it never receives Node, native
addon, direct radio, or another window's resource authority. Each envelope
binds protocol version, attachment, authenticated sender/window/session
identity, opaque operation correlation, command, validated payload, quota, and
generation.

Main validates the complete schema and sender authorization before invoking a
backend; owns per-renderer leases, stream limits, cancellation, and cleanup;
and rejects stale, malformed, oversized, cross-window, or unnegotiated records.
Reload, navigation, crash, close, destroy, and backend restart close admission,
release/revoke owned resources in dependency order, and return an explicit
snapshot requiring subscription rebind. No subscription or privileged object
silently survives renderer reload. An optional bounded orphan exists only for
confirmed cleanup and is non-adoptable.

## Shared boundary invariants

- Every wire record carries the applicable version, attachment, backend/adapter
  generations, and nested connection/database/occurrence generations where it
  names a resource.
- Limits apply before allocation or dispatch: bytes, operations, retained
  buffers, scan/notification ingress, IPC messages, restoration journals, and
  callbacks are bounded. Overflow exposes counters and a terminal/control
  notice; it does not allocate indefinitely.
- Safe platform detail is structured and redacted. Raw addresses, names,
  payloads, security material, paths, and secrets do not enter normal logs,
  traces, IPC diagnostics, or network export without explicit reviewed action.
- Cleanup is idempotent. A late native/IPC callback releases temporary data,
  changes no public state, and cannot revive a resource or correlation.

## Export and artifact impact

Only explicit host subpaths load a JSI installer, native add-on, or Electron
preload bridge. The root and `backend-sdk` declarations remain host-neutral;
wire schemas and generated projections are mechanically checked from one
authority. Packaging rejects the draft, byte fallback, unapproved binary,
unexpected archive path, and ABI/schema mismatch before they can become a
runtime boundary.

## Rejected alternatives

- A Codegen binary signature workaround, text encoding, a second bridge, or a
  host-specific byte API: rejected because it duplicates transport semantics and
  hides copy/cancellation behavior.
- Electron renderer radio, Web Bluetooth renderer fallback, injectable mock
  success, or unrestricted preload: rejected because it crosses the process
  trust boundary or misstates capability.
- Reusing live JS objects, public numeric handles, unversioned records, or
  unchecked dynamic payloads: rejected because reload, restart, duplicate UUID,
  and hostile-input behavior cannot be made deterministic.

## Consequences and gates

Boundary tests cover version negotiation, malformed/unknown records, exact
typed-array slices, zero length, size limits, post-submit mutation, independent
delivery ownership, cancellation and late events, teardown/reload, stale paths,
renderer authorization, cross-window isolation, and counter cleanup. Native
implementation additionally needs Android and Apple compilation, supported
control/bootstrap Codegen checks, Expo CNG and classic integration, Hermes, and
physical-device evidence before an evidence label is promoted.

The old dual-byte/native/Electron records are deleted with their implementation
paths. Their historical characterization cannot justify a fallback. The draft
boundary declarations and Phase-0 binary experiments are deleted or
mechanically replaced before G1. Repository absence tests keep them from
becoming a second implementation authority again.

## Related decisions

- [Public API ADR](2026-07-4.0-public-api.md)
- [Backend contract ADR](2026-07-4.0-backend-contract.md)
- [RN restoration bootstrap ADR](2026-07-4.0-rn-restoration-bootstrap.md)
- [Packaging ADR](2026-07-4.0-packaging.md)
