Proposed (batch stackra-430, STA-1532). The acquirer SDK ADR that
0442 decision 6
asks for, written before the module, for Stripe Terminal. Nothing may rely on it until the owner
accepts it. Certification pending: written from Stripe’s public Terminal SDK documents, not yet
checked on a reader.
Context
Commerce, Mezzora and Jute take card-present payments on phones, tablets and Android tills with Bluetooth readers (Stripe Reader M2, BBPOS WisePad 3) and with the phone itself (Tap to Pay on iPhone and Android). Only Stripe Terminal’s native iOS and Android SDKs reach them. 0478 puts card terminal native code in one Expo module package,@stackra/native-card-terminal, behind the existing
CardTerminal port of @stackra/peripherals/contracts
(0339
decision 1), and 0442 decision 6 asks for one ADR per acquirer SDK: payments go through the tenant’s payment
Connection (OS payments, STA-664) and never complete offline. 0446
decision 4 already has @stackra/payments’ stripe-tap-to-pay SoftPos driver talk to Stripe only through the
StripeTerminalBridge port, which it left for the staff app to bind over Stripe’s React Native SDK; under 0478 an app
holds no native code, so the binding needs a home. The alternatives were Stripe’s React Native SDK package in each app
(rejected: a third-party native dependency per app, versioned outside our release, and a second copy of the SDK beside
the module) and Stripe’s server-driven readers only (WisePOS E, S700; rejected as the only path because handheld and
Tap to Pay checkout need the mobile SDK; those readers stay server-driven, see decision 7).
Decision
- Stripe Terminal is the first acquirer SDK of
@stackra/native-card-terminal. The package wraps Stripe Terminal’s native iOS SDK (StripeTerminalpod) and Android SDK (com.stripe:stripeterminaland its Tap to Pay artifact), declared only in its podspec and Gradle file and fetched from Stripe’s repositories at build time under Stripe’s terms (0478 decision 4). The Expo module,StackraCardTerminal, is a pass-through: discovery, connect, disconnect, the reader’s state, collect and confirm, cancel, and the connection-token handshake. It maps nothing; the TypeScript drivers read its answers. CardTerminaldriverstripe_terminal.StripeTerminalCardTerminalDriverpairs a Bluetooth reader by its serial number (TerminalPairing.terminalId;provideris the market pack’sstripe;addressis null), at the Stripe location the server names, and reports ready, busy or offline. A refused Bluetooth or location permission resolvespermission_deniedon capabilitybluetooth; no reader, another provider, a LAN address or an SDK error resolvesunavailableoncard_terminal(0253,CapabilityResult). The port is unchanged: it pairs and reports, and the payment stays the payments driver’s (0339 decision 1).- The bridge is bound by the package.
NativeStripeTerminalBridgeDriver(drivernative) binds@stackra/payments’StripeTerminalBridgeover the same module, so thestripe-tap-to-paySoftPos driver and a Bluetooth reader share one SDK instance and one connected reader. This replaces 0446 decision 4’s “the staff app binds that port over Stripe Terminal’s React Native SDK” once accepted; the port and the SoftPos driver do not change. - Connection tokens come from the server. The SDK asks for a connection token through the module; the package
answers through the
StripeTerminalServerport, which the host binds over a server action that asks the tenant’s Stripe payment Connection (OS integrations, STA-664) for a token. The app holds no Stripe secret key, no restricted key and no account id; a token request that fails is answered as a failure, never retried with a stored value. The reader’s Stripe location comes from the same port (a site setting on the server). - Never offline. A payment is a server-created payment intent whose client secret the server hands the device (0446 decision 3); the device retrieves, collects and confirms it, and the server captures, cancels and reconciles over the Connection. The module never enables the SDK’s offline mode and never creates an intent on the device, so a payment cannot complete without Stripe. A lost answer is cancel-then-reconcile on the server (0377 decision 2).
- Permissions are the package’s config plugin.
plugin/adds Bluetooth (always) and location (when in use) usage strings, thebluetooth-centralbackground mode, and, when the app’s runway turns Tap to Pay on, the proximity reader entitlement on iOS; on Android the Bluetooth scan and connect, fine location and NFC permissions, with NFC not required as a hardware feature. The usage strings are options with English defaults; the app’s locale overlays translate them. - Choice is data. Which Markets offer Stripe card-present payments is pack data (
payments.terminal-providers,payments.softpos); which devices may run it is the device catalogue’s; the Tap to Pay entitlement per app bundle is alaunch-gate:<pack>:<product>row (0446 decision 5). Server-driven Stripe readers (WisePOS E, S700) stay aTerminalGatewayof the Stripe Connection on the server and are not part of this package. - Mock. The
memorydriver of@stackra/peripheralsis theCardTerminalmock; the package’s TypeScript drivers are tested against a stand-in module that replays the SDK’s answers. The Swift and Kotlin are checked on the box and on certified readers (STA-739), not on a developer Mac.
Consequences
- Stripe card-present payments work in every Expo app that lists the package, with one SDK version per release.
- The staff app no longer needs Stripe’s React Native SDK; 0446’s bridge is bound by a package with tests.
- The server must offer a connection-token action over the tenant’s Stripe Connection (OS integrations) and a site setting for the Stripe location; until it does, pairing refuses.
- A second acquirer SDK (a Gulf SoftPOS SDK, 0377 decision 7) is its own ADR and its own driver in the same package.
PERIPHERAL_DRIVERS.cardTerminalgainsstripe_terminalthrough the integration batch, since the list is@stackra/peripherals’ file.
Enforcement
- planned — the 0478
validate-repository.mjscheck (module packages carrysrc/,ios/,android/,plugin/). packages/experience/native-card-terminal/src/native/__tests__/no-secret-key.test.ts— no file undersrc/,plugin/,ios/orandroid/holds a string shaped like a Stripe secret or restricted key (sk_,rk_).- planned — review by
architecture-guardianuntil both land.
Supersedes and sources
Supersedes: none. Amends, once accepted, 0446 decision 4 (who binds StripeTerminalBridge). Sources: ADR 0478
decision 8, ADR 0442 decision 6, batch stackra-430 (notes/handover/batches/stackra.yaml), Stripe Terminal’s public
iOS and Android SDK documents.