diff --git a/rauc/custom.go b/rauc/custom.go
index 6f1aa355908c58395ddd5b45ff3eb0feb062f211..cf061b1076e0a9fab4a5ee8e9279ae5da4b92e37 100644
--- a/rauc/custom.go
+++ b/rauc/custom.go
@@ -10,6 +10,7 @@ import (
 	"os"
 
 	"git.ostc-eu.org/OSTC/OHOS/components/sysota/boot"
+	"git.ostc-eu.org/OSTC/OHOS/components/sysota/ota"
 )
 
 // CustomBootBackend is an interface between boot loader and RAUC.
@@ -161,3 +162,197 @@ func runSetSlotStateCmd(backend CustomBootBackend, args []string) error {
 		return ErrUnexpectedArgument
 	}
 }
+
+// ErrProtocolViolation records unexpected usage of RAUC custom boot backend hook.
+//
+// This can happen if the logic in SystemOTA is flawed or if RAUC changes
+// radically, extending the protocol in a way that requires updates to
+// SystemOTA.
+var ErrProtocolViolation = errors.New("violation of protocol between SystemOTA and RAUC")
+
+// BootProtocolAdapter uses SystemOTA boot.Protocol and ota.SystemState to implement CustomBootBackend.
+//
+// TODO(zyga): this adapter does not yet use boot.Protocol.Reboot, which is required
+// for correct operation. This will change once SystemOTA can be used as a RAUC
+// install hook.
+type BootProtocolAdapter struct {
+	proto boot.Protocol
+	state *ota.SystemState
+}
+
+// NewBootProtocolAdapter returns a RAUC CustomBootBackend implemented boot.Protocol and boot.Mode.
+//
+// RAUC and SystemOTA differ in how the boot loader is handled. RAUC uses a
+// stateful protocol, where each slot has a persistent state and there is a
+// persistent primary slot used for booting. SystemOTA uses a transactional
+// interface and has only one element of state that is not captured in the boot
+// loader itself, namely boot.Mode, where the system may be expected to normally
+// or in the one-off try mode.
+//
+func NewBootProtocolAdapter(proto boot.Protocol, state *ota.SystemState) *BootProtocolAdapter {
+	return &BootProtocolAdapter{proto: proto, state: state}
+}
+
+// PrimarySlot returns the name of the RAUC primary boot slot.
+//
+// SystemOTA does not model the primary slot directly. The system boot.Mode
+// state is used to pick either active slot (in normal mode) or inactive slot
+// (in try mode).
+func (adapter *BootProtocolAdapter) PrimarySlot() (boot.Slot, error) {
+	switch adapter.state.BootMode {
+	case boot.Normal:
+		return adapter.proto.QueryActive()
+	case boot.Try:
+		return adapter.proto.QueryInactive()
+	default:
+		return boot.InvalidSlot, boot.ErrInvalidBootMode
+	}
+}
+
+// SetPrimarySlot sets the name of the RAUC primary boot slot.
+//
+// SystemOTA does not model the primary slot directly. When in normal boot mode,
+// attempt to set the inactive boot slot as primary commences an update
+// transaction. This transaction is finished with a call to SetSlotState, which
+// either commits or rolls back the transaction.
+//
+// In case of invalid input the error is boot.ErrInvalidSlot, Other errors are
+// those from the concrete implementation of boot.Protocol or
+// ErrProtocolViolation.
+func (adapter *BootProtocolAdapter) SetPrimarySlot(slot boot.Slot) error {
+	if slot != boot.SlotA && slot != boot.SlotB {
+		return boot.ErrInvalidSlot
+	}
+
+	active, err := adapter.proto.QueryActive()
+	if err != nil {
+		return err
+	}
+
+	inactive := boot.SynthesizeInactiveSlot(active)
+
+	switch {
+	case adapter.state.BootMode == boot.Normal && slot == active:
+		// Affirmation of an expected, synthesized state.
+		return nil
+	case adapter.state.BootMode == boot.Normal && slot == inactive:
+		// Begin update transaction.
+		if err := adapter.proto.TrySwitch(slot); err != nil {
+			return err
+		}
+
+		adapter.state.BootMode = boot.Try
+
+		return nil
+	case adapter.state.BootMode == boot.Try && slot == inactive:
+		// Commmit of an update transaction.
+		if err := adapter.proto.CommitSwitch(); err != nil {
+			return err
+		}
+
+		adapter.state.BootMode = boot.Normal
+
+		return nil
+	case adapter.state.BootMode == boot.Try && slot == inactive:
+		// Affirmation of an expected, synthesized state.
+		return nil
+	default:
+		return ErrProtocolViolation
+	}
+}
+
+// SlotState returns the good/bad state of a given slot.
+//
+// SystemOTA does not model the state of each slot directly. It is assumed that the
+// active slot is always good and that the inactive slot is always bad. Note
+// that boot.Protocol.TrySwitch does not modify the active slot, so the system
+// does attempt to boot into a known "bad" slot, this is intentional and matches
+// what RAUC expects.
+func (adapter *BootProtocolAdapter) SlotState(slot boot.Slot) (boot.SlotState, error) {
+	if slot != boot.SlotA && slot != boot.SlotB {
+		return boot.InvalidSlotState, boot.ErrInvalidSlot
+	}
+
+	active, err := adapter.proto.QueryActive()
+	if err != nil {
+		return boot.InvalidSlotState, err
+	}
+
+	if slot == active {
+		return boot.GoodSlot, nil
+	}
+
+	return boot.BadSlot, nil
+}
+
+// SetSlotState sets the good/bad state of a given slot.
+//
+// SystemOTA does not model the state of each slot directly. The only state is
+// boot.Mode - either normal boot or try-boot and whatever the bootloader
+// considers to be the active slot.
+//
+// SetSlotState is expected to be called to initiate and finalize a bundle
+// install operation, or in other words a system update.
+//
+// When SetSlotState is called to initialize the update operation, the inactive
+// slot is marked as bad and then set as the primary boot slot. SystemOTA
+// recognizes this, verifies that the state is set to bad and does not do
+// anything more.
+//
+// When SetSlotState is called called to finalize a successful update operation
+// the state of the primary slot is changed to good. Note that at this stage
+// boot.Mode is Try and that the primary slot is still the inactive slot.
+//
+// When SetSlotState is called to finalize a failed or aborted update operation
+// the only difference from the previous situation is that the state is bad
+// instead of good.
+//
+// In case of invalid input the error is boot.ErrInvalidSlot,
+// boot.ErrInvalidSlotState. Other errors are those from the concrete
+// implementation of boot.Protocol or ErrProtocolViolation.
+func (adapter *BootProtocolAdapter) SetSlotState(slot boot.Slot, state boot.SlotState) error {
+	if slot != boot.SlotA && slot != boot.SlotB {
+		return boot.ErrInvalidSlot
+	}
+
+	if state != boot.GoodSlot && state != boot.BadSlot {
+		return boot.ErrInvalidSlotState
+	}
+
+	active, err := adapter.proto.QueryActive()
+	if err != nil {
+		return err
+	}
+
+	inactive := boot.SynthesizeInactiveSlot(active)
+
+	switch {
+	case adapter.state.BootMode == boot.Normal && slot == active && state == boot.GoodSlot:
+		// Affirmation of an expected, synthesized state.
+		return nil
+	case adapter.state.BootMode == boot.Normal && slot == inactive && state == boot.BadSlot:
+		// Affirmation of an expected, synthesized state.
+		return nil
+	case adapter.state.BootMode == boot.Try && slot == inactive && state == boot.GoodSlot:
+		// Commmit of an update transaction.
+		if err := adapter.proto.CommitSwitch(); err != nil {
+			return err
+		}
+
+		adapter.state.BootMode = boot.Normal
+
+		return nil
+	case adapter.state.BootMode == boot.Try && slot == inactive && state == boot.BadSlot:
+		// Rollback of an update transaction.
+		// Note that we set boot mode even if CancelSwitch failed because by
+		// this time, we have rolled back already.
+		adapter.state.BootMode = boot.Normal
+		if err := adapter.proto.CancelSwitch(); err != nil {
+			return err
+		}
+
+		return nil
+	default:
+		return ErrProtocolViolation
+	}
+}