The previous post looked at Betaflight firmware from the source side.
That answered the lower-level question:
What runs on the flight controller?
This post looks at the tool most pilots actually touch:
How do I configure, flash, inspect, back up, and debug that flight controller?
That tool is Betaflight App, the modern Betaflight configurator.
It is not only a website.
It is a PWA, a browser hardware app, an Android wrapper, a desktop wrapper, a firmware flasher, an MSP client, a blackbox viewer, and a UI around a very hardware-specific workflow.
Betaflight App GitHub Source Code Betaflight App PWA Betaflight App Master Snapshot Betaflight App Releases License: GPL-3.0
What is Betaflight App?
Betaflight App is the configuration and management application for Betaflight flight-controller firmware.
The repo README describes the current app as a Progressive Web Application, with the latest stable release at:
https://app.betaflight.com
There is also a master snapshot at:
https://master.app.betaflight.com
That master snapshot is useful for testing, but the README is explicit that it can be buggy, broken, or corrupt flight-controller settings.
I would not use it casually on a real quad unless I was deliberately testing and had backups.
The stable PWA is the interesting part.
Browser applications have slowly crossed a boundary that used to require native desktop software.
With WebSerial, WebUSB, Bluetooth, service workers, and installable PWA behavior, a browser can now become a real hardware tool.
Betaflight App is a practical example of that shift.
Why It Matters
Firmware is powerful, but the configurator is where mistakes become easy.
A normal Betaflight session can involve:
- selecting a board target
- flashing firmware
- checking receiver channels
- changing UART ports
- tuning PID and filter settings
- setting modes and failsafe behavior
- reviewing GPS, sensors, motor order, OSD, VTX, and blackbox logs
- using the CLI to inspect or restore configuration
That means the app is not just a front-end.
It is the control surface for the aircraft.
The useful mental model is:
| Layer | Project |
|---|---|
| Radio link | ExpressLRS |
| Flight-controller firmware | Betaflight |
| Configuration and flashing UI | Betaflight App |
That also makes the app a good companion to the previous hardware posts.
ExpressLRS solved the radio link, Betaflight firmware runs the aircraft, and Betaflight App is how a pilot sees and changes the flight-controller state.
Repository Snapshot
I cloned and inspected:
repo: https://github.com/betaflight/betaflight-configurator
HEAD: 4757d943
describe: 10.10.0-994-g4757d943
package: betaflight-app
package version: 2026.6.0-alpha
license: GPL-3.0
required Node engine: 24.x
local Node used: v22.22.0
local npm used: 10.9.4
Recent commits in the checkout were active app-maintenance work: Autopilot PID debug labeling, Cloudflare deploy workflow permissions, OSD preview helper tests, dependency bumps, blackbox legend fixes, and number-input hit target fixes.
The top-level technology stack is:
- Vue 3 for the UI
- Vite 8 for development and production builds
vite-plugin-pwafor the service worker and PWA manifest- Pinia for state
- Vitest for tests
- WebSerial, WebBluetooth, WebSocket, WebUSB DFU, and virtual serial transports
- Capacitor for Android packaging
- Tauri 2 for desktop packaging
- Three.js, Leaflet, OpenLayers, D3, and app-specific visualization helpers
That is a modern web app, but it is aimed at hardware.
The App Architecture
The repo has a clean split between user-facing tabs, transport backends, Betaflight protocol logic, and native wrappers.
Important paths:
| Path | What It Contains |
|---|---|
src/components/tabs/ |
Main app screens: setup, ports, receiver, PID tuning, OSD, CLI, firmware flasher, blackbox, motors, GPS, sensors, VTX |
src/js/msp.js |
MSP communication flow used to talk to Betaflight |
src/js/msp/ |
MSP codes and helpers |
src/js/serial.js |
Serial orchestration |
src/js/serial_backend.js |
Backend selection and routing |
src/js/protocols/ |
WebSerial, WebBluetooth, WebSocket, WebUSB DFU, Capacitor, Tauri, and virtual transports |
src/composables/ |
Vue composables for ports, flashing, CLI, cloud build, OSD preview, motors, VTX, preflight, and related workflows |
src/blackbox-viewer/ |
Blackbox parsing and viewing code |
src-tauri/ |
Tauri desktop wrapper |
android/ |
Android packaging |
test/js/ |
Vitest coverage for MSP, transports, stores, tabs, cloud build, CLI sessions, and utilities |
The transport layer is the most interesting part for me.
Inside src/js/protocols/, the app has separate implementations for:
WebSerial.jsWebBluetooth.jsWebSocket.jsWebUsbDfuTransport.jsCapacitorSerial.jsCapacitorDfu.jsCapacitorTcp.jsTauriSerial.jsTauriTcp.jsVirtualSerial.jsusbdfu.jswebstm32.jsesp32.js
That explains how the same app can be a PWA, a desktop app, a mobile app, and a local development tool.
It also explains why a plain Docker container is not the right mental model here.
This project needs access to local hardware interfaces, browser permissions, native wrappers, and serial/DFU transport behavior.
PWA and Local Development
The Vite config sets the app root to src, builds into src/dist, and configures a generated service worker with vite-plugin-pwa.
The PWA manifest uses the package metadata and app icons:
name: Betaflight - Configuration and management application
short_name: Betaflight
theme_color: #ffffff
icons: /images/pwa/pwa-192-192.png, /images/pwa/pwa-512-512.png
For local development, the server defaults to:
http://localhost:8080
If local certificates are present, it can run HTTPS on:
https://local.betaflight.com:8443
That HTTPS detail matters because WebAuthn/passkey features require a secure context.
On this machine I did not have the local certificates, so the build and test output warned that WebAuthn features would be unavailable in HTTP mode.
Desktop and Android Packaging
The README still points users to standalone releases for Windows, Linux, macOS, and Android.
The source code backs that up in two ways.
First, Android uses Capacitor:
{
"appId": "betaflight.app",
"appName": "Betaflight App",
"webDir": "src/dist"
}
Second, Tauri is configured for desktop builds:
productName: Betaflight App
identifier: com.betaflight.app
targets: deb, rpm, appimage, dmg, nsis
frontendDist: ../src/dist
That means the app is fundamentally web-first now, but the repo still has serious native packaging work around it.
The Tauri source also shows one important platform caveat:
USB serial is unavailable on iOS; TCP is the only transport there.
That kind of detail is why I prefer reading the source before describing an app as “cross-platform.”
The wrapper can exist everywhere, but not every transport exists everywhere.
Local Trial
I tested the repo locally with the documented Node/npm path.
The repo asks for Node 24.x in package.json and .nvmrc currently says:
24.12.0
This machine was on:
node v22.22.0
npm 10.9.4
So npm install warned:
EBADENGINE Unsupported engine
required: node 24.x
current: node v22.22.0
But it still completed:
npm install
The install added 1123 packages and reported:
6 vulnerabilities (1 low, 5 moderate)
For reproducible development, I would use Node 24:
nvm install 24.12.0
nvm use 24.12.0
npm install
Build Result
The production build passed:
npm run build
The build generated the PWA service worker:
PWA v1.3.0
mode generateSW
precache 743 entries
dist/sw.js
dist/workbox-2fbc6a65.js
There were warnings worth documenting:
- local SSL certificates were missing, so the local server used HTTP and disabled WebAuthn features
../../images/pattern_dark.pngand../images/corner.svgstayed unresolved until runtime- some legacy Three.js helper imports are always undefined with the current Three.js package
- some dynamic imports are ineffective because the same modules are also imported statically
- some generated chunks are larger than Vite’s default 500 kB warning threshold
Those warnings did not block the build.
They are still useful because they tell contributors where old app history, packaging paths, and bundle boundaries are rubbing against the newer Vite/PWA setup.
Test Result
There was one important local gotcha.
If I ran:
npm test -- --run
immediately after a production build, the test command failed during pretest.
The reason was not a source test failure. The package test script runs lint first:
"pretest": "npm run lint",
"lint": "eslint --ext .js,.vue src"
Because the build output lives under src/dist, ESLint tried to lint generated production files such as bundled JS, sw.js, and Workbox output. That produced many generated-file lint errors.
After removing the generated build directory:
rm -rf src/dist
npm test -- --run
the clean source test path passed:
Test Files 54 passed (54)
Tests 642 passed (642)
Duration 15.04s
So the practical contributor note is:
# clean test path after a production build
rm -rf src/dist
npm test -- --run
That is a small but useful repo-specific learning.
Commands That Worked
For a source checkout:
git clone https://github.com/betaflight/betaflight-configurator.git
cd betaflight-configurator
nvm install 24.12.0
nvm use 24.12.0
npm install
npm run dev
For a production-style local PWA build:
npm run build
npm run preview
For tests:
rm -rf src/dist
npm test -- --run
For Android development:
npm run android:run
For opening the Android project in Android Studio:
npm run android:open
For Tauri desktop development:
npm run tauri:dev
For Tauri desktop bundles:
npm run tauri:build
The desktop build path may require platform-specific Tauri prerequisites; the repo wraps that with scripts/check-tauri-prereqs.mjs.
How I Would Use It Safely
For normal configuration work, I would start with the stable PWA:
https://app.betaflight.com
Then I would use the CLI tab before making risky changes:
version
status
diff all
Save the output somewhere boring and findable.
That matters especially if you do not know the exact flight-controller target.
The model name of a drone frame, such as an iFlight Cidora SL5, is not enough by itself.
The same family can ship with different flight controllers across revisions and bundles.
The app can help you read what the controller reports, but it cannot make an unsafe target guess safe.
Where It Fits with Betaflight Firmware
The firmware repo and the app repo share a boundary: MSP.
Betaflight firmware exposes configuration and telemetry through the MultiWii Serial Protocol. Betaflight App implements the UI and transport side of that conversation.
That is why the app repo has both:
- user-facing tabs such as Ports, Receiver, Motors, OSD, CLI, and Firmware Flasher
- protocol code such as MSP helpers, serial backends, DFU transports, and platform-specific wrappers
For readers who like source-first learning, this repo is a good way to understand the operational surface of Betaflight without starting inside embedded C.
It shows what the firmware needs to expose to be usable by pilots.
FAQ
Is Betaflight App the same as Betaflight firmware?
No.
Betaflight firmware runs on the flight controller.
Betaflight App runs on your browser, desktop, or mobile device and talks to the flight controller to configure, flash, inspect, and back up its settings.
Should I use the stable PWA or the master snapshot?
Use the stable PWA for normal work:
https://app.betaflight.com
Treat the master snapshot as a testing build:
https://master.app.betaflight.com
The upstream README warns that the unstable version may be buggy, broken, or corrupt flight-controller settings.
Does Betaflight App need Docker?
No.
This is not a self-hosted server app. It is a hardware-facing web/PWA application with serial, USB, DFU, Bluetooth, TCP, Android, and desktop wrapper paths.
Use the PWA, standalone release, or source build instead of trying to containerize it.
How is a PWA different from a simple static website?
A simple static website is usually just files served to a browser:
HTML
CSS
JavaScript
images
That can still be useful, fast, and easy to host.
This blog is mostly in that world.
A Progressive Web Application uses web files too, but it adds app-like behavior on top of them:
| Capability | Static Website | PWA |
|---|---|---|
| Served as HTML/CSS/JS | Yes | Yes |
| Can be installed from the browser | Usually no | Yes, when configured with a manifest and service worker |
| Can cache app assets for repeat use | Basic browser cache | Controlled by a service worker |
| Can feel like an app window | No | Yes |
| Can support offline or poor-network startup | Usually limited | Possible, depending on service-worker design |
| Can use browser hardware APIs | Possible with JavaScript | Common in serious PWAs when browser support allows it |
Betaflight App is a PWA because it has a manifest, generated service worker, installable app behavior, and browser hardware workflows such as WebSerial and WebUSB DFU.
So the important difference is not “PWA means dynamic and static means simple.”
The better distinction is:
static site: content-first web files
PWA: web files packaged with app-like lifecycle, caching, installation, and browser capabilities
If I already build static websites, what should I learn to build PWAs?
If you already know how to ship static websites, you are closer than it looks.
A PWA can still be deployed as static files. The difference is that you now need to think like an app developer, not only like a page author.
The learning path I would follow is:
| Topic | Why It Matters |
|---|---|
| Web app manifest | Gives the browser the app name, icons, theme color, display mode, and install metadata |
| Service worker | Lets the app intercept requests, cache assets, and control offline/update behavior |
| Cache strategy | Decides what is cached, when it updates, and how stale assets are avoided |
| HTTPS and secure contexts | Required for service workers and many browser capabilities |
| App shell design | Separates the stable UI shell from changing data/content |
| Responsive interaction design | Makes the installed app feel usable on desktop, tablet, and mobile |
| Browser storage | Covers IndexedDB, Cache Storage, localStorage, and what should not be stored client-side |
| Update lifecycle | Handles the awkward part: a user may have an old service worker and old cached assets |
| Offline and degraded-network behavior | Decides what should still work when the network is weak or absent |
| Browser APIs and permissions | Covers capabilities such as notifications, file access, Bluetooth, USB, serial, camera, and location where supported |
| Testing | Uses DevTools Application panel, Lighthouse/PWA checks, mobile testing, and real install tests |
For a simple static-site author, the first useful mini-project is:
1. Add a manifest file.
2. Add app icons.
3. Register a service worker.
4. Cache the app shell.
5. Show a useful offline fallback page.
6. Deploy over HTTPS.
7. Test installability in Chrome/Edge/Android.
After that, the harder work is not syntax. It is product judgment:
- What should work offline?
- What must always be fresh?
- What happens when the app updates?
- What data can safely live in the browser?
- Which browser APIs are actually available on the devices your users have?
Betaflight App is an advanced PWA because it goes beyond installability and caching. It also uses browser hardware workflows such as WebSerial and WebUSB DFU, which makes browser support and permissions part of the app design.
Can a PWA be deployed as a totally static site on Cloudflare Pages?
Yes, if the PWA does not need server-side code.
A PWA can be just static assets:
index.html
app JavaScript bundles
CSS
manifest.webmanifest
service-worker.js
icons
offline fallback page
static data files
That kind of PWA can be deployed to static hosts, including Cloudflare Pages.
Cloudflare Pages supports Git-based deployments where you configure the build command and build output directory, and it also supports direct upload of prebuilt assets. For a Vite-style PWA, the static output is commonly something like dist/; for Hugo, it is commonly public/.
Cloudflare Pages build configuration
Cloudflare Pages direct upload
The usual static PWA deployment checklist is:
npm run build
deploy the build output directory
serve over HTTPS
make sure manifest.webmanifest is reachable
make sure the service worker is reachable
make sure the service worker scope matches the app path
test a fresh visit
test installability
test reload after deployment
test offline or poor-network behavior
You only need server-side pieces when the app needs them.
Examples:
| Need | Static PWA Enough? | Add Backend? |
|---|---|---|
| Installable docs/app shell | Yes | No |
| Offline-first calculator/tool | Yes | No |
| Local-first notes with browser storage | Often yes | Maybe later for sync |
| Login and private user accounts | No | Yes |
| Payments or secret API keys | No | Yes |
| Database writes | No | Yes |
| Server-side rendering | No | Yes |
| Webhooks | No | Yes |
On Cloudflare Pages, backend behavior can be added with Pages Functions when needed, but it is not required for a static PWA.
So the short answer is:
PWA does not automatically mean server app.
PWA can be a static deployment.
Cloudflare Pages is a valid place to host that static PWA.
Add Functions only when the app needs backend behavior.
Can I install it as a desktop app?
Yes.
There are standalone releases for Windows, Linux, macOS, and Android. The current source tree also contains Tauri packaging for deb, rpm, appimage, dmg, and nsis targets.
On Linux, remember that serial access often requires adding your user to the dialout group:
sudo usermod -aG dialout ${USER}
Then log out and back in.
Why does local HTTPS matter?
Some browser capabilities require a secure context.
In this repo, the Vite config checks for local certificates. If they are missing, local development runs on http://localhost:8080 and WebAuthn features are disabled.
For ordinary UI development that may be fine. For passkey/WebAuthn work, set up the local certificates described by WEBAUTHN_SETUP.md.
Which Betaflight CLI commands are useful before changing anything?
These are the ones I would run before flashing, applying presets, or changing a board I do not fully know yet:
version
status
diff all
dump all
resource show all
serial
get serialrx_provider
get receiver
get gyro
get acc
get motor
get dshot
Use them for different jobs:
| Command | Why It Helps |
|---|---|
version |
Shows firmware version, target, build, and release context |
status |
Shows runtime state, sensors, arming-disable flags, CPU load, and board context |
diff all |
Best portable backup for most restores because it lists what differs from defaults |
dump all |
Broad/full configuration capture; useful for reference, audits, and before/after comparisons |
resource show all |
Shows pin/resource mapping, useful when checking motors, UARTs, LEDs, softserial, and board-specific wiring |
serial |
Shows serial port configuration and UART function assignments |
get serialrx_provider |
Confirms receiver protocol, such as CRSF for ExpressLRS |
get receiver |
Finds receiver-related settings |
get gyro / get acc |
Finds gyro and accelerometer related settings |
get motor / get dshot |
Finds motor output and DShot-related settings |
Yes, dump all is the “give me everything” style command.
But for restoring after a firmware update, diff all is often the cleaner artifact because it captures the settings that differ from that firmware and target’s defaults.
My habit would be to save both:
version
status
diff all
dump all
Then use diff all as the restore starting point and keep dump all as the full reference.
What should I back up before changing a drone?
At minimum, open the CLI tab and capture:
version
status
diff all
dump all
That gives you the firmware version, target identity, status context, portable configuration diff, and full configuration reference.
Do this before flashing, before applying presets, and before experimenting with unknown board targets.
Conclusion
Betaflight App is a good example of where modern PWA development is going.
It is not a marketing site pretending to be an app.
It is a real hardware tool built with Vue, Vite, service workers, WebSerial, WebUSB DFU, MSP, Android Capacitor wrappers, and Tauri desktop packaging.
The local result was strong: install completed despite a Node-version warning, the production PWA build passed, and the clean source test suite passed with 642 tests.
The main improvement I would make as a contributor is small but practical: keep generated src/dist output out of the lint path, so npm test behaves the same whether or not someone just ran npm run build.
For pilots, the bigger lesson is simpler.
Use the stable app, back up CLI output before changing anything, and treat board targets as hardware facts, not guesses.
Comments