Welcome to JetKVM development! This guide will help you get started quickly, whether you're fixing bugs, adding features, or just exploring the codebase.
- A JetKVM device (for full development)
- Go 1.24.4+ and Node.js 22.21.1
- Git for version control
- SSH access to your JetKVM device
Recommended: Development is best done on Linux or macOS.
If you're using Windows, we strongly recommend using WSL (Windows Subsystem for Linux) for the best development experience:
This ensures compatibility with shell scripts and build tools used in the project.
-
Clone the repository:
git clone https://github.com/jetkvm/kvm.git cd kvm -
Check your tools:
go version && node --version -
Find your JetKVM IP address (check your router or device screen)
-
Deploy and test:
./dev_deploy.sh -r 192.168.1.100 # Replace with your device IP -
Open in browser:
http://192.168.1.100
That's it! You're now running your own development version of JetKVM.
cd ui
npm install
./dev_device.sh 192.168.1.100 # Replace with your device IPNow edit files in ui/src/ and see changes live in your browser!
# Edit Go files (config.go, web.go, etc.)
./dev_deploy.sh -r 192.168.1.100 --skip-ui-build./dev_deploy.sh -r 192.168.1.100 --run-go-testsssh root@192.168.1.100
tail -f /userdata/jetkvm/last.log/kvm/
├── main.go # App entry point
├── config.go # Settings & configuration
├── display.go # Device UI control
├── web.go # API endpoints
├── cmd/ # Command line main
├── internal/ # Internal Go packages
│ ├── confparser/ # Configuration file implementation
│ ├── hidrpc/ # HIDRPC implementation for HID devices (keyboard, mouse, etc.)
│ ├── logging/ # Logging implementation
│ ├── mdns/ # mDNS implementation
│ ├── native/ # CGO / Native code glue layer (on-device hardware)
│ │ ├── cgo/ # C files for the native library (HDMI, Touchscreen, etc.)
│ │ └── eez/ # EEZ Studio Project files (for Touchscreen)
│ ├── network/ # Network implementation
│ ├── sync/ # Synchronization primatives with automatic logging (if synctrace enabled)
│ ├── timesync/ # Time sync/NTP implementation
│ ├── tzdata/ # Timezone data and generation
│ ├── udhcpc/ # DHCP implementation
│ ├── usbgadget/ # USB gadget
│ ├── utils/ # SSH handling
│ └── websecure/ # TLS certificate management
├── pkg/ # External packages that have customizations
│ ├── myip/ # Get public IP information
│ └── nmlite/ # Network link manager
├── resource/ # netboot iso and other resources
├── scripts/ # Bash shell scripts for building and deploying
└── static/ # (react client build output)
└── ui/ # React frontend
├── localization/ # Client UI localization (i18n)
│ ├── jetKVM.UI.inlang/ # Settings for inlang
│ └── messages/ # Messages localized
├── public/ # UI website static images and fonts
└── src/ # Client React UI
├── assets/ # UI in-page images
├── components/ # UI components
├── hooks/ # Hooks (stores, RPC handling, virtual devices)
├── keyboardLayouts/ # Keyboard layout definitions
├── paraglide/ # (localization compiled messages output)
├── providers/ # Feature flags
└── routes/ # Pages (login, settings, etc.)
Key files for beginners:
web.go- Add new API endpoints hereconfig.go- Add new settings heretailscale.go- Tailscale status and control-server logicui/src/routes/- Add new pages hereui/src/components/- Add new UI components here
# Deploy everything to your JetKVM device
./dev_deploy.sh -r <YOUR_DEVICE_IP>cd ui
npm ci
./dev_device.sh <YOUR_DEVICE_IP>Please click the Build button in EEZ Studio then run ./dev_deploy.sh -r <YOUR_DEVICE_IP> --skip-ui-build to deploy the changes to your device. Initial build might take more than 10 minutes as it will also need to fetch and build LVGL and other dependencies.
# Skip frontend build for faster deployment
./dev_deploy.sh -r <YOUR_DEVICE_IP> --skip-ui-build# Test connection to device
ping 192.168.1.100
# Check if JetKVM is running
ssh root@192.168.1.100 ps aux | grep jetkvmThe file /userdata/jetkvm/last.log contains the JetKVM logs. You can view live logs with:
ssh root@192.168.1.100
tail -f /userdata/jetkvm/last.logssh root@192.168.1.100
rm /userdata/kvm_config.json
systemctl restart jetkvmChange the TARGET_IP in .vscode/settings.json to your JetKVM device IP, then set breakpoints in your native code and start the Debug Native configuration in VSCode.
The code and GDB server will be deployed automatically.
- Deploy your changes:
./dev_deploy.sh -r <IP> - Open browser:
http://<IP> - Test your feature
- Check logs:
ssh root@<IP> tail -f /userdata/jetkvm/last.log
# Run all tests
./dev_deploy.sh -r <IP> --run-go-tests
# Frontend linting
cd ui && npm run lint# Test login endpoint
curl -X POST http://<IP>/auth/password-local \
-H "Content-Type: application/json" \
-d '{"password": "test123"}'JetKVM exposes Tailscale control-server configuration through JSON-RPC so self-hosted control planes (for example Headscale) can be used.
getTailscaleStatusreturns current state and effectivecontrolURLgetTailscaleControlURLreturns the effective control server URLsetTailscaleControlURLupdates and persists the URL (empty value resets to default)
Notes:
- URLs must be
http://orhttps://and include a host. - Query strings, fragments, user info, and non-root paths are rejected.
- Control server changes are applied with
tailscale set --login-server=.... - When apply fails, the previous configured URL is restored and not persisted.
The UI has been set up with some end-to-end tests to ensure basic functionality. It's ideal that as you add featured, you add new tests and update existing ones. At minimum, ensure that existing end-to-end tests continue to pass.
The end-to-end tests require a connection to GitHub and the GitHub GH CLI to be installed and authorized. See installation instructions. After confirming the GH install works, authorize the CLI using
gh auth loginBefore starting a pull-request (PR) on GitHub, make sure that the system still passes all end-to-end tests. Use the following command after ensuring the setup above has been completed. The test will do a complete native, UI build, and device service. It will then ask for the IP of your test device. Warning, this will deploy your changes to the specified JetKVM device, so recovery may be required if something severe breaks. It may also reset the configuration of the test device, so be prepared to re-adopt and configure when done. You will need to ensure the KVM is connected to an HDMI and USB port of an actual machine that is on and active so that keyboard status, mouse movement, and display capture are testable.
make test_e2e DEVICE_IP=<IP># Fix permissions
ssh root@<IP> chmod +x /userdata/jetkvm/bin/jetkvm_app_debug
# Clean and rebuild
go clean -modcache
go mod tidy
make build_dev# Check network
ping <IP>
# Check SSH
ssh root@<IP> echo "Connection OK"# Clear cache and rebuild
cd ui
npm cache clean --force
rm -rf node_modules
npm installIf while trying to build you run into an error message similar to :
In file included from /workspaces/kvm/internal/native/cgo/ctrl.c:15:
/workspaces/kvm/internal/native/cgo/ui_index.h:4:10: fatal error: ui/ui.h: No such file or directory
#include "ui/ui.h"
^~~~~~~~~
compilation terminated.
This means that your system didn't create the directory-link to from ./internal/native/cgo/ui to ./internal/native/eez/src/ui when the repository was checked out. You can verify this is the case if ./internal/native/cgo/ui appears as a plain text file with only the textual contents:
../eez/src/ui
If this happens to you need to enable git creation of symbolic links either globally or for the KVM repository:
# Globally enable git to create symlinks
git config --global core.symlinks true
git restore internal/native/cgo/ui # Enable git to create symlinks only in this project
git config core.symlinks true
git restore internal/native/cgo/uiOr if you want to manually create the symlink use:
# linux
cd internal/native/cgo
rm ui
ln -s ../eez/src/ui ui rem Windows
cd internal/native/cgo
del ui
mklink /d ui ..\eez\src\uiMake sure you clean up your node modules and do an npm ci (not npm i) to ensure that you get the exact packages required by package-lock.json. This is especially important when switching branches!
cd ui && rm -rf node_modules/ && npm ci && cd ..If you are working on upgrades to the UI packages use this command to wipe the slate clean and get a new valid package-lock.json:
cd ui && rm -rf node_modules/ package-lock.json && npm i && cd ..You can also run the device-side go code under a debug session to view the logs as the device is booting up and being used. To do this use the following command in your development command-line (where the IP is the JetKVM device's IP on your network) to see a very detailed synctrace of all mutex activity:
./dev_deploy.sh -r <IP> --enable-sync-trace- Backend: Add API endpoint in
web.go - Config: Add settings in
config.go - Frontend: Add UI in
ui/src/routes/ - Test: Deploy and test with
./dev_deploy.sh
- Go: Follow standard Go conventions
- TypeScript: Use TypeScript for type safety
- React: Keep components small and reusable
- Localization: Ensure all user-facing strings in the frontend are localized
# Enable debug logging
export LOG_TRACE_SCOPES="jetkvm,cloud,websocket,native,jsonrpc"
# Frontend development
export JETKVM_PROXY_URL="ws://<IP>"- Check logs first:
ssh root@<IP> tail -f /userdata/jetkvm/last.log - Search issues: GitHub Issues
- Ask on Discord: JetKVM Discord
- Read docs: JetKVM Documentation
- Fork the repository
- Create a feature branch
- Make your changes
- Test thoroughly
- Submit a pull request
- Code works on device
- Tests pass
- Code follows style guidelines
- Frontend user-facing strings localized
- Documentation updated (if needed)
- Enable
Developer Modeon your JetKVM device - Add a password on the
Accesstab
# Access profiling
curl http://api:$JETKVM_PASSWORD@YOUR_DEVICE_IP/developer/pprof/# Enable trace logging (useful for debugging)
export LOG_TRACE_SCOPES="jetkvm,cloud,websocket,native,jsonrpc"
# For frontend development
export JETKVM_PROXY_URL="ws://<JETKVM_IP>"
# Enable SSL in development
export USE_SSL=trueThe application uses a JSON configuration file stored at /userdata/kvm_config.json.
-
Update the Config struct in
config.go:type Config struct { // ... existing fields NewFeatureEnabled bool `json:"new_feature_enabled"` }
-
Update the default configuration:
var defaultConfig = &Config{ // ... existing defaults NewFeatureEnabled: false, }
-
Add migration logic if needed for existing installations
We modified the LVGL code a little bit to remove unused fonts and examples. The patches are generated by
git diff --cached --diff-filter=d > ../internal/native/cgo/lvgl-minify.patch && \
git diff --name-only --diff-filter=D --cached > ../internal/native/cgo/lvgl-minify.delThe browser/client frontend uses the paraglide-js plug-in from the inlang.com project to allow compile-time validated localization of all user-facing UI strings in the browser/client UI. This includes title, text, name, description, placeholder, label, aria-label, message attributes (such as confirmText, unit, badge, tag, or flag), HTML element text (such as <h?>, <span>, or <p> elements), notifications messages, and option label strings, etc.
We do not translate the console log messages, CSS class names, theme names, nor the various value strings (e.g. for value/label pair options), nor URL routes.
The localizations are stored in .json files in the ui/localizations/messages directory, with one language-per-file using the ISO 3166-1 alpha-2 country code (e.g. en for English, de for German, etc.)
The translations are extracted into language files (e.g. en.json for English) and then paraglide-js compiles them into helpers for use with the m-function-matcher. An example:
<SettingsPageHeader
title={m.extensions_atx_power_control()}
description={m.extensions_atx_power_control_description()}
/>If you enable the Sherlock plug-in, the localized text "tooltip" is shown in the VSCode editor after any localized text in the language you've selected for preview. In this image, it's the blue text at the end of the line :
-
Locate a string that is visible to the end user on the client/browser
-
Assign that string a "key" that reflects the logical meaning of the string in snake-case (look at existing localizations for examples), for example if there's a string
This is a teston the thing edit page it would be "thing_edit_this_is_a_test""thing_edit_this_is_a_test": "This is a test",
-
Add the key and string to the ui/localization/messages/en.json like this:
- Note if the string has replacement parameters (line a user-entered name), the syntax for the localized string has
{ }around the replacement token (e.g. This is your name: {name}). An complex example:
{m.mount_button_showing_results({ from: indexOfFirstFile + 1, to: Math.min(indexOfLastFile, onStorageFiles.length), total: onStorageFiles.length })} - Note if the string has replacement parameters (line a user-entered name), the syntax for the localized string has
-
Save the en.json file and execute
npm run i18n:resortto resort the language files,npm run i18n:validateto validate the translations, andnpm run i18n:compileto create the m-functions (you can usenpm run i18nto do all three steps in order) -
Edit the .tsx file and replace the string with the calls to the new m-function which will be the key-string you chose in snake-case. For example
This is a testin thing edit page turns intom.thing_edit_this_is_a_test()- Note if the string has a replacement token, supply that to the m-function, for example for the literal
I will call you {name}, usem.profile_i_will_call_you({ name: edit.value })
- Note if the string has a replacement token, supply that to the m-function, for example for the literal
-
When all your strings are extracted, run
npm run i18n:machine-translateto get a first-stab at the translations for the other supported languages. Make sure you use an LLM (you can use aifiesta to use multiple LLMs) or a translator of some form to back-translate each new machine-generation in each language to ensure those terms translate reasonably.
- Get the ISO 3166-1 alpha-2 country code (for example AT for Austria)
- Create a new empty file in the ui/localization/messages directory (example at.json)
- Add the new country code to the ui/localization/jetKVM.UI.inlang/settings.json file in both the
"locales"and the"languageTags"section (inlang and Sherlock aren't exactly current to each other, so we need it in both places). That file also declares thebaseLocale/sourceLanguageTagwhich is"en"because this project started out in English. Do NOT change that. - Add the locale name of the language to all the ui/localization/messages/ files (example
"locale_at.json": "Österreichisches Deutsch",)- In the en.json file, use the name of the language in that language. For example
"locale_es": "Español". - In all other translation files, use the name of the language in the language of the containing file (example, in local_da.json (Danish), we have
"locale_de": "Tysk",for German).
- In the en.json file, use the name of the language in that language. For example
- Run
npm run i18n:machine-translateto do an initial pass at localizing all other existing messages to the new language then correct anything that looks incorrect. We're aiming for translations that make sense to the native speakers of the target language.- Note you will get an error DB has been closed, ignore that message, we're not using a database.
- Note you likely will get errors while running this command due to rate limits and such (it uses anonymous Google Translate). Just keep running the command over and over... it'll translate a bunch each time until it says Machine translate complete.
- Run
npm run i18n:validateto ensure that language files and settings are well-formed. - Run
npm run i18n:find-excessto look for extra keys in other language files that have been deleted from the master-list in en.json. - Run
npm run i18n:find-dupesto look for multiple keys in en.json that have the same translated value (this is normal) - Run
npm run i18n:find-unusedto look for keys in en.json that are not referenced in the UI anywhere.- Note there are a few that are not currently used, only concern yourself with ones you obsoleted.
- Run
npm run i18n:auditto do all the above checks. - Using inlang CLI to support the npm commands.
- You can install the Sherlock VS Code extension in your devcontainer.
Happy coding!
For more information, visit the JetKVM Documentation or join our Discord Server.

