Deep Dive: Zeus Lightning Wallet Architecture

Deep Dive: Zeus Lightning Wallet Architecture

Deep Dive: Zeus Lightning Wallet Architecture

How a mobile wallet supports multiple Lightning implementations

I’m Khushal, a Summer of Bitcoin Fellow ‘25 contributing to Zeus, an open source Bitcoin Lightning wallet. I’ve contributed 43+ pull requests across Core Lightning support, BOLT 12, routing, and other wallet features. I like breaking down how Bitcoin and Lightning software works and writing about the architecture behind it.

Introduction

Most Lightning wallets are built for a single implementation. Zeus takes a different approach, supporting LND, Core Lightning, LNDHub, and embedded on-device nodes through a unified interface.

This design choice has significant architectural implications. Supporting multiple backends means that every feature and API call must work across implementations that often have different APIs, different capabilities, and different data formats. The patterns Zeus uses to solve this problem are applicable to any Lightning application.

Zeus is open-source under the AGPLv3 license, written in TypeScript and React Native, and runs on both iOS and Android. It targets users who want full control over their Lightning node: channel management, routing fee configuration, coin control, UTXO labeling, and direct connections over Tor.

This article examines how Zeus achieves multi-backend support through its abstraction layer, explores features like BOLT 12 offers and submarine swaps, and identifies patterns useful for developers building Lightning applications.

Zeus Architecture Overview

Zeus is built on React Native with TypeScript, using MobX for state management. The codebase is organized around a few key directories:

  • backends/ Adapters for each Lightning implementation
  • stores/ MobX state stores for different domains
  • views/ React Native screens and components
  • utils/ Shared utilities including BackendUtils
  • models/ Data models for channels, invoices, payments.

The most important architectural decision is the separation between backends and the rest of the application. UI components and stores never call backend implementations directly. Instead, they go through BackendUtils, a routing layer that selects the appropriate backend based on user configuration.

This means a component rendering a list of channels doesn’t need to know whether the user is connected to an LND node, a Core Lightning node, or running an embedded node on their phone. It calls BackendUtils.getChannels() and gets back a normalized list.

Zeus currently supports seven backend implementations:
image

Each backend implements the same interface, but not every backend supports every feature. LND doesn’t support BOLT 12 offers. LNDHub doesn’t support channel management. Embedded nodes don’t expose forwarding history. The architecture handles this through capability detection, which we’ll examine in the next section.

Backend Abstraction Layer

The core challenge Zeus solves is this: LND, Core Lightning, and LNDHub all have different APIs. A channel list from LND looks different than one from Core Lightning. Some features exist in one implementation but not others. How do you write UI code once that works everywhere?

Zeus solves this with a two-layer abstraction.

  • BackendUtils: The Routing Layer

BackendUtils is a singleton that holds references to all backend instances. When any part of the app needs to make a backend call, it goes through BackendUtils, which routes the call to the currently active backend.
image

The call method is important. If a backend doesn’t implement a method, it returns false instead of throwing an error. This allows the UI to gracefully handle missing features.

  • Capability Detection

Not every backend supports every feature. BOLT 12 offers work on Core Lightning and LDK Node, but not on LND. Channel management works on most backends, but not on LNDHub. Forwarding history is available on LND and Core Lightning, but not on embedded nodes.
Zeus handles this through **supports*() methods. Each backend declares what it can do:
image

The UI checks these before rendering features:

image

This pattern means features appear and disappear based on what the connected backend actually supports, without crashes or error states.

  • Data Normalization

LND and Core Lightning return data in different formats. A forwarding event from LND has chan_id_in, while Core Lightning returns in_channel. Rather than handling this difference everywhere in the UI, Zeus normalizes data in the model layer:
image

Views use inChannelId and outChannelId, never the raw fields. The model handles which backend the data came from.

  • Feature Support Across Backends
    For reference, here’s what each backend supports:
    image

This architecture allows Zeus to support everything from custodial LNDHub accounts to full self-custodial embedded nodes, all with the same codebase.

Feature Implementation Deep Dives

This section examines five features in Zeus, covering their purpose, implementation, and the patterns they demonstrate.

BOLT 12 Withdrawal Requests

BOLT 12 introduces offers, which are static, reusable payment requests without expiry. A withdrawal request (or invoice request) is the inverse: instead of asking someone to pay you, you request that they send you an invoice so you can pay them.

Use cases include refunds, pull payments, and recurring subscriptions. Unlike LNURL-withdraw, withdrawal requests are native to the Lightning protocol and don’t require an HTTP server.

Zeus implements this for Core Lightning and LDK Node backends:
image

The flow: Alice creates a withdrawal request in Zeus, shares the lnr1… string with Bob, Bob’s wallet calls sendinvoice to pay her, and Alice receives the funds.

LND does not support BOLT 12, so these methods return false through the capability detection system.

Core Lightning Integration

Zeus connects to Core Lightning nodes through CLNRest, a REST API that uses runes for authentication. Runes are bearer tokens with optional restrictions, similar to macaroons but simpler.

image

One challenge with CLNRest is that its API responses differ from LND. Channel IDs, peer information, and forwarding events all have different field names. Zeus handles this through model normalization, as shown in Section 3.

Routing History

Node operators need visibility into payment forwarding to understand which channels generate fees. Zeus fetches forwarding history and allows filtering by channel.

The implementation differs between backends. LND supports server-side filtering (since v0.20.0), while Core Lightning returns all events and filtering happens client-side:
image

image

The FeeStore then normalizes and filters the data before the UI renders it.

WIF Private Key Sweeps

WIF (Wallet Import Format) is a standard encoding for Bitcoin private keys. Zeus can sweep funds from a WIF key into the user’s wallet, useful for importing paper wallets or recovering funds from other software.

The implementation detects which address type contains funds by checking multiple derivation paths:
image

WIF validation happens before any network calls:

image

List and Disconnect Peers

Peer management is essential for node operators. Zeus implements listing connected peers and disconnecting them across all backends that support it:
image

The store validates that the peer exists before attempting disconnection, and updates local state immediately on success. This pattern of optimistic UI updates with error handling is common throughout Zeus.

State Management with MobX

Zeus uses MobX for state management instead of Redux. MobX provides reactive state with less boilerplate: when observable data changes, components that depend on it re-render automatically.

The codebase has 31 specialized stores, each handling a specific domain:

  • SettingsStore node configuration, authentication, user preferences
  • ChannelsStore channel list, peer management, channel operations
  • BalanceStore on-chain and Lightning balances
  • FeeStore fee estimation, routing history, forwarding events
  • SwapStore submarine swap operations with Boltz
  • InvoicesStore invoice creation and tracking
  • PaymentsStore outgoing payment history

Stores are instantiated with dependency injection and observe each other through reactions:
image

Observable State and Actions

State is marked with @observable, mutations happen in @action methods, and async updates use runInAction:

image

Reactions for Side Effects

MobX reactions trigger side effects when observed data changes. Zeus uses this for automatic data enrichment:
image
When this.channels changes, the reaction fires, enriching channels with node aliases and applying filters. The UI doesn’t need to call multiple methods in sequence; it just updates channels and the reaction handles the rest.

This pattern keeps components simple. They read from stores, call actions, and MobX handles the reactivity.

Things Worth Knowing

  • API differences are everywhere. LND returns chan_id_in, Core Lightning returns in_channel. Same data, different names. Handling this in the model layer once is better than scattering conditionals across the UI.
  • Check capabilities before rendering. If a backend doesn’t support BOLT 12, don’t show the button. The supports*() pattern saves users from hitting dead ends.
  • Read the code before writing. Zeus recommends starting with code review before your first PR. It’s good advice. You’ll understand why abstractions exist and how to use them correctly.

Conclusion

Zeus solves a real problem: supporting multiple Lightning implementations without fragmenting the codebase. The backend abstraction layer, capability detection, and model normalization patterns make this possible.

For developers building Lightning applications, these patterns are worth studying. The code is open source and well-organized. Whether you’re building a wallet, a node management tool, or anything that talks to Lightning nodes, the approach Zeus takes to multi-backend support is a solid reference.

Resources:

GitHub: https://github.com/ZeusLN/zeus

Documentation: https://docs.zeusln.app

Contributing guide: https://docs.zeusln.app/contribute/how-you-can-contribute

Telegram: https://t.me/zeusLN

Write a comment