@homeslate/server
v0.3.0
Published
Hono API for homeslate: displays, Google OAuth, and the calendar, commute, weather, and Home Assistant proxies.
Readme
@homeslate/server
The homeslate Hono API: display CRUD, the public kiosk route, the Google OAuth
routes, and the calendar, commute, weather-alerts, and Home Assistant device
proxies. Node 22.13+ (uses node:sqlite through @homeslate/adapters).
import { startHomeslateServer } from "@homeslate/server";
await startHomeslateServer({
dataDir: "./data",
publicBaseUrl: "http://homeslate.lan",
google: {
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
},
openRouteServiceApiKey: process.env.OPENROUTESERVICE_API_KEY,
finnhubApiKey: process.env.FINNHUB_API_KEY,
seedEmptyDisplay: true,
onListen: ({ port }) => console.log(`homeslate API on http://127.0.0.1:${port}`),
});Mount it yourself with createHomeslateServer(options), which returns the Hono
app so you can add routes, test it, or run it on another runtime.
Displays and viewer passcodes
GET /api/displayslists{ id, name }summaries.POST /api/displays,GET /api/displays/:id, andPUT /api/displays/:idreturn the document plus apasscodeEnabledflag. The passcode hash is never included in a response.PATCH /api/displays/:idaccepts{ passcode: string | null }to set a 4-digit viewer PIN or clear it withnull(isValidPinvalidates the shape;hashPasscodestores a scrypt hash on the display record).DELETE /api/displays/:idremoves the display.GET/PUT /api/public/:publicIdserve the kiosk. When the display has a passcode they answer401 { passcodeRequired: true }unless the request carries?passcode=<pin>(verifyPasscode).
hashPasscode, verifyPasscode, and isValidPin are exported for hosts that
manage passcodes themselves.
Home Assistant proxy
Set homeAssistant: { baseUrl, token } to enable the Home Assistant widgets.
The browser never sees the long-lived token; the server forwards an allowlisted
subset driven by the domain registry in @homeslate/widgets/server:
GET /api/homeassistant/entities?domain=…— entity pickers for a known domain.GET /api/homeassistant/states/:entityId— one entity, mapped per domain.GET /api/homeassistant/todo/:entityId/items— normalizedtodoitems.GET /api/homeassistant/weather/:entityId/forecast— normalized forecast.GET /api/homeassistant/calendar/:entityId/events— normalized events.GET /api/homeassistant/history/:entityId?hours=…— numeric history points.GET /api/homeassistant/image?path=…— allowlisted HA image proxy.POST /api/homeassistant/services/:domain/:service— allowlisted service calls (registry-gated: domain → service → allowed data keys).GET /api/homeassistant/status— transport/connection state.
Without homeAssistant, these routes answer 501 and the widgets stay
designable with a "set the env vars" notice.
Routes run on a HomeAssistantTransport, a generic seam
(listEntities(domain), getEntity(entityId), callService(domain, service,
entityId, data, options?), plus optional fetchImage(path) and
history(entityId, hours)). The built-in direct client implements all of it; an
injected transport (the relay)
forwards the core methods to a home agent. Adding a widget means adding a
domain-registry entry (entity-id shape, services, data keys, optional payload
builder/mapper), not a transport method. The image proxy and history route need
the optional methods, so relay mode answers 501 for history and falls back to a
placeholder for media artwork until the protocol grows them.
In a container, bind all interfaces so Docker's published port can reach the API — Docker forwards to the container's bridge address, not its loopback:
await startHomeslateServer({ dataDir: "/data", hostname: "0.0.0.0" });Keep the host-side port mapping on loopback (-p 127.0.0.1:8787:8787) so the
API stays off the LAN except through your proxy.
See the self-hosting guide.
