Challenge
A wallet application lives from one rule that is not up for negotiation: private keys must not leave the device, and every transaction must be signed locally. Anything that softens this rule — plain-text cloud backups, server-held seeds, “convenient” password-based recovery — moves the risk onto the operator and turns the app into a target that moves entire vaults instead of individual devices.
At the same time, a serious wallet needs a backend: price data, token metadata, RPC access, broadcast against several chains, abuse protection. That backend role must be scoped tightly enough that it speeds up day-to-day use but authorises nothing outside its narrow perimeter. This boundary — and its open documentation — sits at the centre of WalletPro.
On top of that, two native clients (Android, iOS) and multiple chains must be maintained from one repository without implementing each chain integration twice.
Approach
Five principles:
- Signing exclusively on the device — seeds and private keys stay in the platform keystore (Android Keystore, iOS Secure Enclave), never in the backend.
- JWT as an anti-abuse token, not proof of ownership — the API accepts any authenticated call for publicly readable on-chain data regardless of whether the address belongs to the caller.
- Broadcast cryptographically authorised — signed transactions are self-proving; the backend does not need to know who owns them.
- A single Rust core for backend services — a Cargo workspace with several crates so
wallet-api,wallet-workerand shared building blocks reuse the same types. - Open documentation of the boundary — audit reports and the OpenAPI description name the deliberate choice explicitly so scanner findings can be tracked as “accepted by design”.
What we built
Rust backend (services/, crates/, backend/)
A Cargo workspace with wallet-api as the HTTP service plus further crates under crates/. The service serves balances, transaction history, price data and token metadata and exposes the broadcast endpoint for signed transactions. Cargo.toml and Cargo.lock sit at the root and pull all crates into one shared build.
Android client (Kotlin, android/)
Native Android client with local key management via the Android Keystore. The CI workflow .github/workflows/android-tests.yml runs :app:compileDebugKotlin, :app:testDebugUnitTest and :app:lintDebug on Ubuntu with Temurin JDK 17 for every push or PR touching android/. Test and lint reports are uploaded as artifacts (testDebugUnitTest-reports, lint-results-debug).
iOS client (Swift, ios/)
Native iOS client with keys held in the Secure Enclave. Chain integrations follow the same models as on Android; API contracts are kept in sync through a shared OpenAPI description.
Shared mobile layer (mobile/)
A platform-adjacent layer for shared data models and helpers. Chain-specific logic (address validation, fee calculation, explorer URLs) lives there once and is consumed from both Android and iOS.
E2E and load tests (.maestro/, load-tests/)
Maestro flows for UI E2E tests cover the central paths (onboarding, receive, send, history). load-tests/ holds load tests against the Rust backend so the API stays responsive under realistic request volumes.
Infrastructure & monitoring (infra/, monitoring/, docker/)
Container definitions and monitoring configuration for the backend, including a Dockerfile and .env.docker.example as a template. secrets/ exists as a structure but values are not checked in (.gitleaks.toml and Trivy reports enforce that).
Security and audit reports
Several audit reports (AUDIT_REPORT_2026-02-21.md, DETAILED_AUDIT_REPORT_2026-02-21.md, SECURITY_AND_INTEGRATION_AUDIT_2026-02-21.md, MOBILE_APP_AUDIT_2026-02-21.md) document security findings and remediation status. SECURITY_FIXES_APPLIED.md records which findings were actually addressed.
Architecture
WalletPro splits strictly between device and backend. On the device, the seed and private keys live in the platform-native keystore (Android Keystore, iOS Secure Enclave). Signatures are produced there, not on the wire.
The Rust backend (wallet-api) has three roles: (1) a read API for publicly available on-chain data (balances, history, prices, token metadata), (2) a broadcast endpoint that forwards already-signed transaction payloads to the responsible RPC nodes, (3) light abuse protection (rate limiting, JWT-based authentication) via an /api/v1/auth/* challenge-response flow.
The JWT on data routes is explicitly not proof of ownership: it only confirms that the caller has passed the challenge and is not bound to a specific address. Any authenticated caller may query balances and history for any supported address — the same data is available on any public RPC or block explorer anyway. The POST /api/v1/transactions/broadcast endpoint is authorised cryptographically by the signature on the payload; the JWT grants no spending power over any address.
This boundary is documented in the repository (README, ARCHITECTURE.md, the OpenAPI description in services/wallet-api/src/openapi.rs) so that automated scanners flagging the data routes as “broken object-level authorization” can recognise the deliberate design.
On the client side, Android and iOS share data models via an OpenAPI-generated interface and a thin mobile/ layer for chain logic, so chain integration lives in one place.
Numbers & facts
| Metric | Value |
|---|---|
| Backend language | Rust (Cargo workspace) |
| Backend services | wallet-api, wallet-worker, shared crates under crates/ |
| Clients | Android (Kotlin) · iOS (Swift) |
| Shared models | OpenAPI + mobile/ layer |
| Signing | Exclusively on the client (Android Keystore, iOS Secure Enclave) |
| Auth model | JWT as anti-abuse token; broadcast cryptographically signed |
| CI | android-tests.yml — Kotlin compile, unit tests, lint on Ubuntu + Temurin JDK 17 |
| E2E / load | Maestro (.maestro/) + load tests (load-tests/) |
| Security tooling | Gitleaks, Trivy (.trivy-reports/) |
What we learned
Clear boundaries are more valuable than “strong” backend roles. A JWT that claims more than it can prove leads to security promises that are not kept. It is more honest to name the backend as a data channel and let broadcast be authorised by signature.
Documentation as part of architecture. A scanner finding on OWASP API #1 cannot be argued away — but it can be contextualised. Putting the reasoning into README and OpenAPI description saves hours in reviews and customer conversations and prevents ad-hoc “fixes” that would weaken the architecture.
Cargo workspaces scale well for multiple services. Instead of versioning each service as an isolated crate, a workspace gives uniform dependencies and one shared build. That reduces friction when a fix in a shared crate needs to reach several services.
CI must actually secure the main path. The Android workflow does not only run unit tests, it also enforces a successful compileDebugKotlin and a clean lintDebug. That keeps client refactors traceable instead of drifting into silent warnings.
Next steps
- Set up an analogous iOS CI workflow (Xcode build, unit tests) and route it into the same reports structure.
- Strengthen the OpenAPI contract as the single source of truth and ship generated SDKs for Android and iOS.
- Continue consolidating chain integrations under
mobile/so new chains can be added through clear, testable building blocks.
Related
- Context: What we build for — WalletPro shows how we treat trust boundaries and user control as impact.