aiyu-ayaan/Domus
Domus
A local-first smart-home hub for your home server.
- active
- 2 stars
- Rust
- MIT
- pushed today

// readmeaiyu-ayaan/Domus
Domus
A local-first smart-home hub for your home server. Rust backend, SQLite, React web UI (with an Electron desktop shell), and pluggable integrations for Tuya / Smart Life (QR-code login, no developer account), TP-Link Tapo (local), Xiaomi Mi Home (local, tokens from your Mi account), Philips Hue (local bridge) and MQTT (Zigbee2MQTT, ESPHome and anything else that speaks Home Assistant discovery).
- Automatic discovery of new devices, with an inbox or auto-add
- Live dashboard: power draw, bill so far and projected bill, air quality, motion / door / leak / battery readings
- Rooms, device groups, scenes and automations; triggers at a time, at sunrise or sunset (with an offset), on a device change, power level or going offline
- Ask before switching: mark a main switch and every app, group, scene and manual automation run warns before it is turned on or off
- Electricity billing with flat, slab and time-of-day tariffs in any currency
- Multiple users with admin / member / guest roles and access per room or per device, each see-only, control or edit
- Web, Electron desktop (Linux, Windows) and a native Android app, kept in sync live
- 27 languages in the web, desktop and Android apps, following your system's language with a picker in Settings; right to left for Arabic and Urdu
- An Updates page with the release notes (Markdown, tables and images) in every app
- HTTPS by default; credentials and device keys encrypted at rest; one-file backup and restore
Architecture and diagrams: development/docs/ARCHITECTURE.md, summarised below.
UI design profile: design-system/domus/MASTER.md.
Progress: development/TODO.md.
How it fits together
flowchart LR
subgraph Clients
Web["React web UI"]
Desk["Electron desktop"]
And["Android app<br/>(Kotlin, Compose)"]
end
subgraph Server["Domus server: Rust (Axum, Tokio), one binary or Docker image"]
direction TB
TLS["HTTPS (rustls)"] --> Auth["JWT auth<br/>role and access read from the database"]
Auth --> Modules["modules<br/>users · devices · rooms · scenes · automations<br/>energy · notifications · integrations"]
Auth --> WS["live events (WebSocket)"]
Modules --> Mgr["DeviceManager<br/>poll · discover · command"]
Mgr --> Bus(("EventBus"))
Bus --> Engine["AutomationEngine<br/>time · sun · state triggers"]
Engine --> Mgr
Bus --> WS
Workers["workers<br/>poller · discovery · scheduler"] --> Mgr
Workers --> Engine
Mgr --> Vault["Vault<br/>AES-256-GCM"]
end
subgraph Plugins["integrations"]
Tuya["Tuya / Smart Life<br/>cloud, QR login"]
Tapo["Tapo<br/>local"]
Xiaomi["Xiaomi<br/>local miIO"]
Hue["Philips Hue<br/>local bridge"]
Mqtt["MQTT<br/>Home Assistant discovery"]
end
DB[("SQLite<br/>data/domus.db")]
Keys[["data/master.key · jwt.secret · tls/"]]
Web & Desk & And -->|"HTTPS + WSS"| TLS
Mgr --> Plugins
Modules --> DB
Vault -.-> Keys
Modules -. "place search (name only)" .-> Geo(("Open-Meteo<br/>geocoding"))
Clients talk only to the server; the server talks to your devices (locally where the vendor allows it) and keeps everything in one SQLite file. The cloud is used only where a vendor offers no local control (Tuya) or to fetch device keys (Xiaomi), plus two optional lookups: place search for the home location and GitHub for update checks.
Data model
erDiagram
users ||--o{ user_room_access : "rooms"
rooms ||--o{ user_room_access : ""
users ||--o{ user_device_access : "devices"
devices ||--o{ user_device_access : ""
rooms ||--o{ devices : contains
integration_accounts ||--o{ devices : "linked via"
integration_accounts ||--o{ discovered_devices : "found via"
devices ||--o{ device_group_members : ""
device_groups ||--o{ device_group_members : ""
devices ||--o{ energy_metrics : samples
devices ||--o{ air_metrics : samples
users {
text id PK
text role
bool all_rooms
bool kid
bool must_change_password
text reset_code_hash
}
user_room_access {
text user_id
text room_id
bool can_control
bool can_edit
}
user_device_access {
text user_id
text device_id
bool can_control
bool can_edit
}
devices {
text id PK
text integration_type
text state_json
text secrets_enc
bool is_online
bool confirm_changes
}
discovered_devices {
text id PK
text status
text secrets_enc
}
integration_accounts {
text id PK
text credentials_enc
text status
}
energy_metrics {
text device_id
real power_watts
real energy_kwh
datetime recorded_at
}
air_metrics {
text device_id
real pm25
datetime recorded_at
}
automations {
text triggers_json
text conditions_json
text actions_json
bool is_enabled
}
scenes {
text actions_json
text kind
}
electricity_settings {
real rate_per_kwh
text currency
text slabs
text time_of_use
}
notifications {
text level
text title
bool is_read
bool admin_only
}
app_settings {
text key PK
text value_json
}
Credentials and device keys are stored sealed (*_enc) under data/master.key, which is not in the
database. The full set of diagrams, one per feature, is in
development/docs.
Screenshots

Desktop (Electron and web)
Android
Android tablet (two columns from 840dp, Settings as list and detail)
Mobile web
All captures are in res/screenshots; the full-quality walkthrough is res/domus.mp4.
Download
| Server (Docker) | aiyuayaan/domus on Docker Hub, amd64 and arm64 |
| Android | Domus-<version>.apk from the latest release |
| Linux desktop | the one-line installer below, or the .deb / .AppImage from the release |
| Windows desktop | Domus-<version>-Setup.exe from the release |
Server
One file is all the host needs:
mkdir domus && cd domus
wget https://raw.githubusercontent.com/aiyu-ayaan/Domus-Rust/master/docker-compose.yml
docker compose up -d
Open https://<server-ip>:8443 and create the administrator account. docker compose logs domus
shows the certificate fingerprint the apps ask you to confirm.
docker compose pull && docker compose up -d # update
DOMUS_VERSION=beta docker compose up -d # follow a channel: latest (default), beta, alpha, or 0.0.1
TZ=Asia/Kolkata DOMUS_PORT=9443 docker compose up -d
docker compose --profile mqtt up -d # also start the optional Mosquitto broker
Put DOMUS_VERSION, TZ and DOMUS_PORT in a .env file next to the compose file to keep them.
Linux desktop app
Installs the newest release for the current user (x86_64, no root): the AppImage, a menu entry with
the Domus icon and a domus command. The app then keeps itself up to date.
curl -fsSL https://raw.githubusercontent.com/aiyu-ayaan/Domus-Rust/master/scripts/install-linux.sh | sh
# ... | sh -s -- --channel beta beta builds too (or alpha)
# ... | sh -s -- --uninstall remove it; settings are kept
Android
Download the APK from the release page and open it. Later updates are offered when the app opens and in Settings → Updates, on the Stable, Beta or Alpha channel, and installed from there; Android asks once to allow Domus to install them.
The Updates page (Settings → Updates on Android, Settings → About → Updates on web and desktop) shows what you run, the newest release on your channel and its release notes, which are written in Markdown.
Releases, version numbers and the update channels: development/docs/RELEASE.md.
Requirements
- Rust 1.80+ (
cargo) - Node.js 18+ and
pnpm
Run in development
pnpm install
npm run dev # backend (https://127.0.0.1:8443) + web (http://localhost:3000)
npm run dev:backend # backend only
npm run dev:web # web only (proxies /api to the backend)
npm run dev:desktop # Electron; pick the server on its connect page
Open http://localhost:3000. The first visit asks you to create the administrator account.
Run on the home server
pnpm build:web # builds apps/web/dist, served by the backend
APP_ENV=production cargo run --release --manifest-path backend/Cargo.toml
Then open https://<server-ip>:8443.
Or build the Docker image from this checkout (host networking, data in the domus_data volume):
docker compose -f docker-compose.local.yml up -d --build
Remote access through a Cloudflare Tunnel, the home/remote address the apps switch between, and the
desktop packages (pnpm build:desktop) are described in
development/docs/DEPLOYMENT.md.
On first start Domus creates data/ with:
| File | Purpose |
|---|---|
domus.db | SQLite database |
master.key | encrypts integration credentials and device keys — back it up |
jwt.secret | signs sessions |
tls/ | self-signed certificate (replace via tls.cert_path / tls.key_path) |
Settings live in config/default.toml; any key can be overridden with
DOMUS__SECTION__KEY environment variables (see .env.example).
Backup and restore
Everything lives in data/. Back up the whole folder, or take the one-file backup:
Back up. Sign in as an administrator → Settings → Backup → Download backup. The file is a copy of the database with the master key stored inside, so the linked accounts in it can still be opened after a restore. It is made while the server runs. Keep it private: whoever holds it can open those accounts. The JWT secret and the TLS certificate are not in it.
Restore. Stop the server, then:
# Docker
docker compose cp domus-backup-2026-10-11.db domus:/app/data/restore.db
docker compose stop domus
docker compose run --rm domus domus-backend restore data/restore.db
docker compose start domus
# Without Docker, from the repo root (uses ./data)
domus-backend restore ~/Downloads/domus-backup-2026-10-11.db
What was there is kept next to it as *.before-restore. After a restore on a new machine people sign in
again and the apps ask to trust the new certificate. If you set the key with DOMUS_MASTER_KEY, the backup
holds no key and the restored server needs the same variable. More in
development/docs/DEPLOYMENT.md.
Forgot the administrator password
Domus has no mail server, so nothing is sent by email. In order of effort:
-
Another administrator opens Settings → People, turns on Reset code for you and tells you the 8-digit code (good for 30 minutes and 5 wrong tries). On the sign-in screen choose Forgot password?, enter your username and then the code.
-
From the server, which works while Domus runs or is stopped and needs no sign-in:
docker compose exec domus domus-backend reset-password <username> # prints a new random password docker compose exec domus domus-backend reset-password <username> 'new-pass' # or choose one (8+ characters) domus-backend reset-password <username> # without Docker, from the repo rootSign in with it and change it in Settings → Password.
Five wrong passwords for a username lock that name for 15 minutes in production (the lock is lifted by restarting the server, and is off in development). Details and the rules for generated passwords and kid's accounts: development/docs/ACCOUNTS.md.
Languages
English, Hindi, Spanish, Portuguese (Brazil), Indonesian, Arabic, French, Russian, German, Turkish, Japanese, Korean, Italian, Vietnamese, Thai, Chinese (Simplified and Traditional), Polish, Bengali, Tamil, Telugu, Marathi, Gujarati, Kannada, Malayalam, Punjabi and Urdu. Arabic and Urdu are laid out right to left.
Each app follows the language of your browser or system, and shows English when it has no translation for it. To choose another one, open Settings > Language (Appearance on web and desktop); System default goes back to following the system. On Android 13 and later it also appears under the phone's Settings > Apps > Domus > Language.
The translations are machine-made and have not been reviewed by native speakers, so wording may be off. A fix is a one-line change in the language's catalog. How the languages work, how to add one and the rules for new text: development/docs/LOCALIZATION.md.
Connecting devices
| Brand | How |
|---|---|
| Tuya / Smart Life | Integrations → Link with QR code. Enter the User Code from Smart Life (Me → Settings → Account and Security → User Code), then scan the QR in the app. |
| Tapo | Integrations → Link account with your TP-Link ID. Devices on the LAN are found automatically and controlled locally. An H100 hub brings its sensors, switches and thermostats with it. Cameras are listed but not supported yet. |
| Xiaomi | Integrations → Link account with your Mi account and server region. If Xiaomi asks for two-step verification, enter the code it emails or texts you in the same panel. The account can be an email, a phone number with country code (+91…) or a Xiaomi ID. Tokens are fetched once; control is local. Devices can also be added with a token. |
| Philips Hue | Press the round button on the bridge, then Integrations → Philips Hue → Link within 30 seconds. Leave the address empty to find the bridge automatically. Lights and Hue smart plugs are controlled locally. |
| MQTT | Integrations → MQTT → Link with your broker's address, port and login. Devices that announce themselves through Home Assistant MQTT discovery (Zigbee2MQTT, ESPHome, …) appear under Discovered: lights, switches, plugs, fans and sensors. With the bundled broker (--profile mqtt) the address is mqtt, port 1883. |
See development/docs/INTEGRATIONS.md for what each integration covers.
Electricity billing
Energy → Electricity plan: pick any currency, and choose a flat price per kWh or slabs for unit-based tariffs (e.g. Pune/MSEDCL: 0–100 units at one rate, 101–300 at the next, and so on). The bill is calculated per billing cycle, plus the fixed monthly charge.
Tariffs can also carry time-of-day windows: hours (on the home's clock, past midnight allowed) whose units cost a set amount more or less, for example a night rebate or a peak surcharge, on top of the flat or slab price.
New devices appear under Devices → Discovered. Turn on Add new devices automatically there to skip the inbox.
Sunrise and sunset
Set where the home is in Settings → Home location: use this phone's or browser's location, search for the town (the server asks the free Open-Meteo geocoder; only the name you type leaves the home), or type the latitude and longitude. The coordinates are rounded to about a kilometre and stay on your server. An automation can then run at sunrise or sunset, minutes before or after. Without a location, a sun trigger never fires.
Ask before switching
A main switch that cuts power to everything plugged into it is easy to turn off by mistake. Open the device and turn on Ask before switching: every app then asks before turning it on or off, and so do room All on / All off, device groups, scenes (including scenes inside scenes) and a manual Run of an automation that would switch it. The setting is kept on the server, so it is the same on web, desktop and Android. Automations that fire on their own cannot ask and run as written, and nothing outside Domus (a vendor app, a wall switch) is covered.
License
Domus is released under the MIT License. Third-party libraries keep their own licenses; they are listed under About → Acknowledgements in every app.
Tests
cargo test --workspace
pnpm --filter @domus/web exec tsc -b
// stack
- Rust
- TypeScript
// specs
- status
- Working
- type
- Open Source
- year
- 2026
- language
- Rust
- license
- MIT
- stars
- 2
- branch
- master
Repository data last synced