Free, open-source, self-hosted IoT platform. Write TypeScript device scripts, run the DeviceSDK server on your own hardware (Raspberry Pi, NUC, NAS, any Docker host), and connect ESP32 / Raspberry Pi Pico microcontrollers to it over WebSocket.
No cloud, no SaaS, no per-message billing. Your hardware, your data. Licensed under AGPL-3.0-only.
No breaking changes. A device script that works today will work in five years. Breaking changes are treated as bugs. If one ever ships, it's a regression, not a feature.
Update only when you want to. Releases are infrequent and intentional. If a new release has something you want, update. If not, don't. Your current setup keeps working exactly as before.
Compatibility flags. When behavior genuinely has to evolve, opt-in compatibility flags (inspired by Cloudflare Workers' compatibility dates) let you migrate on your own schedule. You will never wake up to a broken device because of a forced upgrade.
The whole platform (REST API, device and watcher WebSockets, and the dashboard UI) is a single container listening on one port. Save the following as docker-compose.yml (no repo clone needed):
services:
devicesdk:
image: ghcr.io/device-sdk/devicesdk:latest
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- ./data:/data
environment:
ALLOW_REGISTRATION: "true"Then start it:
docker compose up -d
# open http://localhost:8080 → the first account you register becomes the adminDevices on your LAN connect to ws://<this-machine>:8080. All state (SQLite database, device scripts, firmware images) is persisted under the ./data volume you control.
The server also advertises itself over mDNS as devicesdk.local, so you can reach it and flash devices against it without knowing its LAN IP (http://devicesdk.local:8080). Set MDNS_HOSTNAME to a different name to run several DeviceSDK servers on one network.
Useful environment variables:
| Variable | Default | Purpose |
|---|---|---|
PORT |
8080 |
HTTP port for the API, WebSockets, and dashboard |
DATA_DIR |
/data |
Root for all persistent state |
ALLOW_REGISTRATION |
true |
Set false to close sign-ups after your account exists |
SECURE_COOKIES |
false |
Set true when serving behind an HTTPS reverse proxy |
MDNS_HOSTNAME |
devicesdk |
Advertised mDNS name (<name>.local); change to run multiple servers on one LAN |
MDNS_ENABLED |
true |
Set false to disable mDNS advertisement |
See docs/public/quickstart.md for the full zero-to-first-deploy walkthrough.
The devicesdk CLI (npm, runs on Node) builds and deploys device scripts to your server. Run devicesdk login to authenticate - the CLI discovers your server over mDNS automatically. Pass --host only if mDNS isn't available on your network, you set a custom MDNS_HOSTNAME, or the CLI runs on the same machine as the server:
npx @devicesdk/cli login # auto-discovers via mDNS
# if mDNS doesn't work: npx @devicesdk/cli login --host http://devicesdk.local:8080
# same machine as server: npx @devicesdk/cli login --host http://localhost:8080
npx @devicesdk/cli init hello-world
cd hello-world
npx @devicesdk/cli deploy
npx @devicesdk/cli logs <project-id> <device-id> --tailPrerequisites: Node.js 22+, pnpm 9.x, and Bun 1.3.14+ (server runtime).
git clone https://github.com/device-sdk/devicesdk && cd devicesdk
pnpm install
pnpm buildTo build the Docker image yourself instead of pulling from GHCR:
docker build -t devicesdk .pnpm + Turborepo monorepo. Bun is the server runtime only: the CLI runs on plain Node for npm users.
| Package | Name | Description |
|---|---|---|
apps/server |
@devicesdk/server |
The backend: Bun + Hono + Chanfana + Zod + bun:sqlite. One process, one port: REST API (/v1/*), device + watcher WebSockets, dashboard SPA, OpenAPI docs (/api-docs), and a bundled MCP server (/mcp) for AI coding agents |
apps/dashboard |
@devicesdk/dashboard |
Vue 3 + Quasar SPA: local email/password auth, project/device/token management. Served same-origin by the server |
apps/simulation |
@devicesdk/simulation |
Vue 3 device-simulation UI (static export consumed by the CLI dev command) |
apps/website |
@devicesdk/website |
Vue 3 + Vite SSG marketing & docs site |
packages/core |
@devicesdk/core |
Shared TypeScript types and the DeviceEntrypoint base class (published to npm) |
packages/cli |
@devicesdk/cli |
CLI tool (devicesdk): login, init, build, dev, deploy, flash, logs, status, inspect |
packages/typescript-config |
@repo/typescript-config |
Shared tsconfig base |
firmware/esp32 |
ESP32 firmware (ESP-IDF, WebSocket client) | |
firmware/pico |
Raspberry Pi Pico W firmware (C++, lwIP WebSocket client) | |
examples/* |
Example device projects (basic, temperature-to-discord, esp32c3-clock) |
| Command | Description |
|---|---|
pnpm install |
Install all dependencies |
pnpm build |
Build all packages (Turbo handles dependency order) |
pnpm local |
Run the server (:8080) + dashboard (:9000) concurrently |
pnpm dev --filter <pkg> |
Start a dev server for one package |
pnpm test --filter <pkg> |
Run tests for a package |
pnpm lint --filter <pkg> |
Lint a package (Biome) |
pnpm check-types --filter <pkg> |
Type-check a package |
pnpm dev --filter @devicesdk/server # Bun server (bun run --watch) on port 8080
pnpm dev --filter @devicesdk/dashboard # Quasar dev server on port 9000
pnpm dev --filter @devicesdk/simulation # Vite dev server on port 9002The server stores all state under DATA_DIR (default ./data): devicesdk.sqlite (WAL),
scripts/{userId}/{projectSlug}/{deviceSlug}/{versionId}.js, and firmwares/.
The @devicesdk/cli package provides the devicesdk command. The server URL is resolved from
DEVICESDK_API_URL → the --host flag → the host saved in ~/.devicesdk/credentials.json by
devicesdk login --host <url>.
| Command | Description |
|---|---|
login |
Authenticate the CLI against your server (auto-discovers via mDNS; pass --host to pin a specific server) |
init |
Scaffold a new project |
build |
Bundle device scripts with esbuild |
dev |
Local dev server with the workerd-based simulator |
deploy |
Deploy scripts to your server (creates an immutable version) |
flash |
Flash firmware onto a Pico W or ESP32 |
logs |
View and stream device logs (--tail for real-time) |
status |
Show live connection status for devices in a project |
Devices running DeviceSDK firmware connect over WebSocket to the server you run. Each device connection is handled by an in-process device session that loads and runs your TypeScript device script. Because the server is yours and the scripts are your own code, scripts run in-process: there is no sandboxed cloud runtime.
Devices within the same project can call methods on each other via type-safe RPC
(this.env.DEVICES["other-device"].method()). The CLI auto-generates devicesdk-env.d.ts
with full TypeScript types for inter-device communication.
Tech stack: Bun + Hono (server), Chanfana (OpenAPI), bun:sqlite (storage),
Vue 3 + Quasar (dashboard), Vue 3 (simulation UI), Vue 3 + Vite SSG (website).
The Pico W and ESP32 firmware implement a WebSocket client that connects to your DeviceSDK
server. Wi-Fi credentials and the server host are configured at flash time. The firmware uses
plain ws:// when the configured host includes an explicit port (self-hosted LAN) and TLS on
443 for bare hostnames. The host can be an mDNS name. Flashing with
--host http://devicesdk.local:8080 lets the device resolve the server over mDNS, so it keeps
working even if the server's DHCP lease changes. Prebuilt binaries are published to rolling
GitHub Releases and bundled into the Docker image; devicesdk flash fetches the matching binary
from your server.
Firmware builds gracefully skip when toolchains (idf.py, cmake) aren't installed, so they
won't block pnpm build.
- Quickstart
- CLI Reference
- Platform Architecture
- Roadmap: Home Assistant integration is the flagship next step