A minimal web app that lets guests open doors via Home Assistant — protected by a PIN code managed directly in HA.
- Guests visit the URL and enter the current PIN (stored in an
input_texthelper in Home Assistant) - After login, they see a button for every discovered lock entity — but only if the
input_booleanunlock allowance is switched on in HA and the visitor is on an allowed network - Pressing a button calls the appropriate HA service (
lock.unlock) for that entity - All
lockentities and their display names are auto-discovered at startup from Home Assistant — no hardcoded entity IDs required
The session token is HMAC-bound to the current PIN, so changing the PIN in HA instantly invalidates all active sessions.
- Home Assistant with a long-lived access token
- Two HA helpers:
- An
input_boolean— enables/disables door access (ENTITY_UNLOCK_ALLOWANCE) - An
input_text— holds the current PIN, leave empty to disable PIN requirement (ENTITY_DOOR_CODE)
- An
- One or more
lockentities in Home Assistant
cp .env.example .env
# edit .env with your HA URL, token, and helper entity IDs
docker compose up --buildSee the tutorial for a full step-by-step walkthrough.
Copy .env.example to .env and fill in the values:
| Variable | Required | Description |
|---|---|---|
SESSION_SECRET |
Yes | Random secret for HMAC signing (openssl rand -hex 32) |
HA_URL |
Yes | Home Assistant base URL, e.g. http://homeassistant:8123 |
HA_TOKEN |
Yes | Long-lived access token from HA |
ENTITY_UNLOCK_ALLOWANCE |
Yes | input_boolean entity ID that enables door unlocking |
ENTITY_DOOR_CODE |
Yes | input_text entity ID holding the login PIN |
IGNORED_ENTITIES |
No | Comma-separated entity IDs to hide from the dashboard |
ALLOWED_NETWORKS |
No | Comma-separated IPv4/IPv6 CIDRs allowed to operate locks |
LISTEN_ADDR |
No | Listen address (default: :8080) |
Docker Compose (recommended):
cp .env.example .env
# edit .env
docker compose up --buildLocal:
go run .On startup the app discovers all door entities from HA, then verifies it can reach all configured entities. If any are unreachable, it exits with an error.
| Document | Type | Contents |
|---|---|---|
| Tutorial | Tutorial | Step-by-step setup from clone to first door open |
| How-to guides | How-to | Rotate PIN, add/ignore doors, restrict by network, deploy updates |
| Reference | Reference | All env vars, HTTP routes, log prefixes, rate limits |
| Explanation | Explanation | Auth model, entity discovery, unlock allowance gate |
- Sessions are stateless HMAC cookies (no server-side storage)
- Login is rate-limited to 5 attempts per IP per 15 minutes
- Cookies are
HttpOnly,Secure,SameSite=Strict - The session signing key is derived from
SESSION_SECRET + current PIN, so rotating the PIN invalidates all sessions ALLOWED_NETWORKSrestricts door controls to trusted networks (e.g. home WiFi); visitors from other networks see a "Please join the WiFi" message after login
Request flow:
main.go— loads config from env, discovers door entities from HA, wires upApp, runs a startup health check, then startsnet/httpwith five routes.middleware.go—RequireAuthwraps protected handlers; on each request it fetches the current door code from HA and validates the session cookie HMAC against it (so rotating the code in HA instantly invalidates all sessions). Also hosts the in-memory IP rate limiter (5 attempts / 15 min window).auth.go— stateless session tokens:random_32_bytes_hex.HMAC-SHA256. The signing key is derived asHMAC(SESSION_SECRET, current_code), binding token validity to the code value.ha.go— thin HA REST API client. Callslock.unlockfor all discovered lock entities.GetAllStates()fetches all entities for auto-discovery.handlers.go—LoginHandler,DashboardHandler,OpenDoorHandler,HealthHandler. Door opening is gated on the unlock allowanceinput_booleanbeingon.templates/— two embedded HTML templates (login.html,dashboard.html) compiled into the binary via//go:embed.
