All projects

MACHBANK · 2025 – 2026

Server-Driven UI Framework

Senior Software Engineer

Reduced SDK feature time-to-market by 1+ month. Won MACHBANK's EPIC Project Award.

TypeScriptNode.jsFastifyReact NativeiOSAndroidAWS

The Problem

Every time MACHBANK’s SDK needed a UI change — a new step in a payment flow, a reordered screen, different button copy — the change required a full iOS/Android/Web release cycle. App store reviews, version fragmentation, and adoption lag meant even trivial product experiments took 4–6 weeks to reach users.

The root cause was architectural: business logic lived in mobile clients. If the server decided what data to show and clients decided how to render it, changing the “what” required changing the clients.

The fix was to invert the model: the server sends a description of the complete UI, and clients are generic renderers that never change for business reasons.

Architecture: Two Layers

The framework is split into two published npm packages, each with a distinct responsibility.


Layer 1: @soymach/mach-sdui-lib — The Component Library

This is the TypeScript library that BFF services use to compose UI responses. It defines the full vocabulary of MACHBANK’s UI: every component, layout, action, and interaction, as strongly-typed factory functions that produce plain JSON objects.

Component system

The library defines 26 UI components organized as molecules and organisms — Button, Alert, CardDetailBox, ListItemSelect, BottomSheetDetail, ReceiptHeader, and more. Every component is created via a factory function, never a class instance:

// A button with a remote navigation action
Components.Button({
  text: 'Confirm payment',
  class: ButtonClassName.Primary,
  action: Actions.NavigateRemote({
    source: 'POST_/payments/confirm',
    flowName: 'paymentFlow',
    loader: Loader({ type: LoaderType.Spinner }),
    onComplete: Actions.NavigateLocal({ flowName: 'receiptFlow' }),
    onError: Actions.RetryFlow(),
  }),
})

Factory functions return plain objects — fully JSON-serializable, with no prototype chain or class overhead. Clients don’t need to know how components were created; they just render what they receive.

Layouts, sections, and screens

UI is composed in three layers:

  • Sections (ButtonSection, HeroSection, DetailSection, etc.) group related components into named containers with auto-generated UUIDs
  • Layouts (8 types: LayoutComplete, LayoutWithoutNav, LayoutBottomSheetComplete, etc.) slot sections into nav, main, and footer placements
  • Screens hold a layout plus optional navigation behavior and analytics events

Actions and chaining

13 action types handle every user interaction: NavigateRemote, NavigateLocal, CallableRemote, OpenModal, OpenBottomSheet, UpdateContext, Close, RetryFlow, and more. Actions are composable — any action can chain to a next action on completion or error:

Actions.CallableRemote({
  source: 'POST_/payments/authorize',
  data: { amount: stateRefs.amount },
  onComplete: Actions.NavigateRemote({ source: 'GET_/payments/receipt', ... }),
  onError:    Actions.OpenBottomSheet({ flowName: 'errorSheet' }),
})

State binding

Dynamic content is bound through state refs — typed string literals that clients interpolate at render time:

const { state, stateRefs } = State({ amount: 15000, currency: 'CLP' })

Components.Button({ text: `Pay ${stateRefs.amount} ${stateRefs.currency}` })
// Serialized → { text: "Pay {state.amount} {state.currency}" }
// Client renders → "Pay 15000 CLP"

The IStateRef type is defined as `{state.${string}}`, so TypeScript enforces valid state references at compile time.

Build-time validation

The build() function is the final step before serialization. It:

  1. Auto-extracts all sections from layout placements — no manual sections array
  2. Asserts all section IDs are unique (throws on collision)
  3. Cross-validates that every referenced section ID exists, and every defined section is used

This means malformed SDUI responses are a compile + runtime error caught locally, never a client-side crash in production.


Layer 2: @soymach/mach-bff-sdk — The Server Infrastructure

This is the Fastify-based SDK that BFF services use to serve SDUI responses. It handles the protocol layer: version negotiation, request parsing, schema validation, trace propagation, and analytics.

Header-based protocol versioning

Clients send X-SDUI-Protocol-Min and X-SDUI-Protocol-Max headers with each request. The SDK negotiates the richest supported version within that range:

Client sends: X-SDUI-Protocol-Min: 1.0 / X-SDUI-Protocol-Max: 2.0
Server supports: ['1.0', '2.0', '3.0']
Negotiated version: 2.0 (highest in range)

If no version matches, the SDK returns 406 Not Acceptable. This decouples mobile app versions from backend deployments — old clients continue receiving their supported protocol version while new features ship to newer clients, without any routing changes.

Type-safe route definition

Routes are declared explicitly via defineRoutes, which enforces the METHOD_/path naming convention at compile time:

const routes = defineRoutes({
  'POST_/payments/create': {
    handler: { '1.0': createPaymentPresenter },
    schemas: { input: { body: paymentSchema } },
  },
  'GET_/payments/receipt': {
    handler: { '1.0': receiptPresenter },
    schemas: { input: {} },
  },
})

The type system ensures handlers declare which protocol versions they support ({ '1.0': fn }), making it impossible to accidentally omit a version. Path extraction is also type-safe — routes.path('GET_/payments/receipt') produces the correct string and errors at compile time on any typo.

Automatic common fields

Every endpoint automatically inherits documentNumber (Chilean RUT), channelCode, machId, and platform in its query schema. Consumer services don’t redeclare them — the SDK merges them, with consumer-defined properties taking precedence on conflict.

Impact

The framework eliminated mobile release cycles for all UI-driven features in the MACHBANK SDK. A UI change that previously required a 4–6 week mobile release cycle now ships with a backend deployment.

The project won MACHBANK’s EPIC Project Award and the ‘Achieving Ambitious Goals’ award.