Skip to content

Sample Projects

Two reference clients are released under the same license as the SDKs. Use them as a starting point, copy-paste what you need, or bundle them as-is with your own branding.

Project Stack Repository
leaf-android Kotlin + Jetpack Compose github.com/shiroedev2024/leaf-android
leaf-desktop Tauri 2 + Vue 3 + TypeScript github.com/shiroedev2024/leaf-desktop

leaf-android

A full-featured demo that embeds leaf-sdk-android and showcases every SDK entry point.

Features

  • Connect / disconnect VPN from a Compose dashboard.
  • Outbound group selector with live ping (via the Runtime HTTP API).
  • Memory log viewer (api.getLogs).
  • Quick Settings tile (LeafVPNTileService).
  • leafvpn://install?clientId=... deep-link handler that auto-provisions a profile.
  • Import / view .leafsub offline bundles through ACTION_VIEW.
  • 8-language localization (English, Persian, Spanish, Arabic, Indonesian, Russian, Turkish, Uzbek).
  • In-app updater that reads the latest version.properties from the release artifact.

File map

Path Responsibility
app/src/main/java/.../MainApplication.kt Binds ServiceManagement, pushes a LeafConfig.
app/src/main/java/.../activity/BaseActivity.kt Binds / unbinds the :VPNService process on onStart / onStop.
app/src/main/java/.../activity/MainActivity.kt Permission requests, VPN consent intent, first-run setup, updateAssets.
app/src/main/java/.../viewmodel/LeafViewModel.kt All state + async calls; mirrors SDK listeners into LiveData.
app/src/main/java/.../VpnEventReceiver.kt Receives secure broadcasts sent from LeafVPNService.
app/src/main/java/.../component/*.kt Compose UI screens (Dashboard, Outbounds, Settings, UpdateScreen).
app/src/main/AndroidManifest.xml Permissions + QS_TILE + leafvpn:// + .leafsub handlers.

Building

git clone https://github.com/shiroedev2024/leaf-android.git
cd leaf-android
# Dockerized build (recommended):
./build.sh apk
# OR Gradle directly:
./gradlew :app:assembleRelease

The build.sh script mounts your local config.toml with a Kellnr token so the Rust side of leaf-sdk-android can be rebuilt from source if you clone the SDK submodule.

Integration patterns worth copying

  • Separate processes, separate listeners — binding is done from BaseActivity, but the VPN service lives in its own :VPNService process. MainApplication.onCreate() checks isMainProcess(context) before binding so the VPN process does not bind to itself, and LeafViewModel re-subscribes on every ServiceListener.onConnect().
  • LeafConfig set once on bindMainApplication pushes a LeafConfig.Builder() with the session name, notification strings, and a URI-serialized Intent so tapping the VPN notification opens MainActivity.
  • State-based UI — every SDK callback is converted to a sealed class (LeafState.Started, SubscriptionState.Fetching, …) exposed through LiveData / StateFlow, so UI composables stay dumb.
  • Three-level outbound pickerLeafViewModel.getOutboundList(tag) queries OUT by default, _selectedSubgroupTag drills into a country subgroup, and changeSelectedOutbound also updates OUT → <country> so routing follows. See Proxy Groups & Naming for the tag convention.
  • Parallel pingsLeafViewModel.refreshPings() spawns one coroutine per outbound against api.getOutboundHealth(name); fall-through from tcpMs to udpMs.
  • Memory log tailLeafViewModel.startLogger() polls api.getLogs(200, offset) every second when memory_logger=true, feeding MemoryLoggerActivity.
  • Offline .leafsub importsimportOfflineSubscription(path, passphrase, keyIds, verifyingKeys, cb) is wired to Intent.ACTION_VIEW (see the <data android:pathPattern=".*\\.leafsub"/> filters in the manifest). The sample ships with a pre-installed Ed25519 verifying key so subscriptions signed by the panel pass offline.
  • leafvpn://install?profile=<base64> deep linkAutoProfileActivity base64-decodes the profile query parameter (a UUID) and calls sm.updateSubscription(uuid, cb). The same scheme is used by desktop and any QR-code flow you design.
  • Quick Settings TileLeafVPNTileService binds inside onStartListening, flips the tile to STATE_ACTIVE on LeafListener.onStartSuccess, and toggles via ServiceManagement.isLeafRunning.
  • Auto-update channelUpdateChecker.fetchUpdate(arch, currentVersion) hits <base>/downloads/android/<arch>/<currentVersion> with retries and surfaces a JSON UpdateResponse. The sample sets BASE_URL_RELEASE to your panel — see UpdateConstants.kt.
  • 8-language localisationLocaleAwareApplication + MainApplication.languages map (en, fa, ar, es, in, ru, tr, uz). User changes document.cookie-style via the settings screen.

leaf-desktop

A Tauri 2 application that embeds leaf_sdk_desktop. Ships signed installers for Windows (MSI), macOS (DMG), and Linux (deb / AppImage).

Features

  • Dashboard with connection toggle, usage graph, ping, and log tail.
  • Subscription management UI (enter client ID, auto/manual update, import .leafsub).
  • Outbound group selector with live failover status.
  • System tray icon with start/stop and a "show main window" action.
  • Auto-updater (tauri-plugin-updater) and single-instance guard (leafvpn:// deep links focus the existing window).
  • Window-state persistence and dark/light theme.

File map

Path Responsibility
src-tauri/Cargo.toml Depends on leaf_sdk_desktop from Kellnr.
src-tauri/build.rs Uses leaf-build-deps to fetch the matching leaf-ipc binary and copies it into sidecar_cache/.
src-tauri/src/main.rs Wraps every SDK function as a #[tauri::command]; registers tray + deep-link handlers.
src-tauri/src/window_manager.rs Show/hide/toggle main window with persistent state.
src-tauri/src/tray.rs / tray_icon_manager.rs Tray icon updates that follow LATEST_LEAF_STATE.
src/ Vue 3 + Pinia UI, listens for core-event / leaf-event / subscription-event.

Building

git clone https://github.com/shiroedev2024/leaf-desktop.git
cd leaf-desktop
yarn install
yarn tauri dev          # dev run
yarn tauri build        # platform installers in src-tauri/target/release/bundle

Requires Rust, Node 16+, and platform build dependencies for Tauri (see the Tauri docs for your OS). Provide a Kellnr token via CARGO_REGISTRIES_KELLNR_TOKEN so the SDK can be fetched.

Integration patterns worth copying

  • Build-time sidecar fetchingsrc-tauri/build.rs calls LeafBuilder::new().version("1.4.1").build() to download, verify, and unzip the right leaf-ipc from static-public; tauri.conf.json then registers it as externalBin. See Distribution & Registries.
  • Single source of truth for stateLATEST_CORE_STATE, LATEST_LEAF_STATE, LATEST_SUBSCRIPTION_STATE, PENDING_LEAFSUB_PATHS all live in once_cell::Lazy<Mutex<Option<...>>>. Tray icon and frontend both read from these globals, so a core restart survives hot-reloads.
  • Event bus — every callback emits a named Tauri event (core-event, leaf-event, subscription-event, file-watch-event, file-opened). src/store/leaf.ts is the canonical Pinia store that subscribes.
  • Liveness ping loopuseLeafStore.startPingTimer() calls invoke('ping') every 5 s after CoreState::STARTED; two consecutive failures trigger resetLeafState() so the UI recovers when the core is killed externally.
  • leafvpn://install?profile=<base64> deep linksrc/store/deeplink.ts listens via tauri-plugin-deep-link::onOpenUrl and falls back to getCurrent() for cold-starts from OS launchers. Supports both standard and URL-safe base64 (auto-pads).
  • .leafsub file associationsbundle.fileAssociations in tauri.conf.json registers the extension, and tauri-plugin-single-instance funnels dropped paths into PENDING_LEAFSUB_PATHSfile-opened event → import_offline_subscription.
  • Quit-safety — the tray Quit action refuses to exit while the core is running; it shows a native notification via tauri-plugin-notification instead.
  • Windows Wintun managementleaf_sdk_desktop::setup_wintun writes wintun.dll next to leaf-ipc on startup, and remove_wintun_dll() runs on RunEvent::ExitRequested.
  • Window statetauri-plugin-window-state persists position / size / maximise / visibility to window_state.json.
  • File watcherstart_file_watcher watches the leaf-ipc binary path; when the app's auto-updater replaces the sidecar the frontend gets file-watch-event and can show a "restart required" banner.
  • Built-in auto-updatertauri-plugin-updater signs artefacts with minisign (pubkey in tauri.conf.json) and polls https://litevpn.top/downloads/tauri/{{target}}/{{arch}}/{{current_version}}; replace with your own URL / pubkey when forking.
  • Linux distro detectionhelper::detect_linux_system_info surfaces apt / rpm / pacman so the UI can show the correct install instruction for the right distro.

Using the samples as a template

Both projects are released under the Apache 2.0 license. Recommended workflow to bootstrap your own client:

  1. Fork the matching sample repo (leaf-android or leaf-desktop).
  2. Replace icons, strings, package names, and branded URLs.
  3. Point the app at your own panel (updateSubscription("YOUR_CLIENT_UUID") or deep-link host).
  4. Keep the version passed to updateAssets in sync with the SDK version you depend on — see Architecture — Asset Versioning Rule.
  5. Sign & distribute.

Issues / questions

Contributions and issues are tracked in the GitHub repositories: