- Nix 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| apps | ||
| flake.nix | ||
| README.md | ||
homefree-genai
HomeFree apps for genai-server — TLS-fronted, SSO-gated ingress for a GenAI stack that may be running on another machine.
Why a plugin and not part of either project
genai-server is a generic service flake; HomeFree is a generic platform.
Neither should know about the other. This repo is the adapter: it speaks
HomeFree's app conventions (homefree.service-config, allocPort,
caddy_gated SSO) and points them at genai-server's endpoints.
The app
A parent app, genai, plus one sub-app per web surface. The parent holds
only what is shared; each surface owns the decisions that are genuinely
per-surface, because exposing ComfyUI to the WAN is a different act from
exposing the chat UI.
homefree.services = {
genai = {
enable = true; # master switch — gates every surface
backend-host = "logistikon.lan";
};
# Sub-apps default to enabled, SSO on, WAN off. Override as needed:
genai-comfy.enable = false; # don't publish ComfyUI at all
genai-voice.sso = false; # headless client that can't do OIDC
genai-webui.public = true; # reachable from the WAN
};
| sub-app | subdomain | fronts |
|---|---|---|
genai-api |
ai |
the portal — /v1/* and /svc/*, the whole stack |
genai-webui |
webui |
Open WebUI |
genai-comfy |
comfy |
ComfyUI |
genai-search |
search |
SearXNG |
genai-magentic |
magentic |
Magentic-UI |
genai-voice |
voice |
realtime speech-to-speech |
Defaults: sub-apps enable = true, sso = true, public = false.
Turning on the parent publishes the stack behind SSO on the LAN; turning
the parent off takes everything down regardless of the child toggles.
The surfaces are instances, not separate apps. They nest under the
GenAI card the same way MediaWiki sites nest under MediaWiki: the parent
declares an instances list, each entry becomes a sub-app labelled
genai_<subdomain>.
That label is not a choice — the admin UI derives it as
${parentLabel}_${inst.subdomain} and matches instances by that string.
The array has to exist in /etc/nixos/homefree-config.json. The admin
UI reads and writes services.genai.instances there and merges no Nix
schema defaults into it, so on a box whose config predates this the sub-app
toggles have nothing to write to — they revert a second after being set,
and the Config dialog comes up empty. Seed it once (see below); after that
the UI owns it, exactly as it owns the MediaWiki site list.
Setting parent WITHOUT a matching instances entry is what caused both of
those symptoms. parent does not mean "group these" in HomeFree; it means
"entry in the parent's instances array", and three handlers in
services-module.js hard-code that.
SSO is the whole security story here. Nothing behind these subdomains
authenticates — the portal, ComfyUI and the tool servers were built for a
trusted LAN — so the Caddy gate is the only thing between the internet and
an inference API with a code sandbox on it. sso.kind is metadata the
admin UI reads and gates nothing; the gate is reverse-proxy.oauth2,
derived from each surface's sso toggle. That distinction cost an open
endpoint once.
This app runs nothing
No container, no port, no data. Every surface fronts a service on the machine with the GPU — the portal in particular reads the model store, render directory and eval reports off local disk, so it has to live there. Getting the portal itself onto HomeFree means putting those disk-bound surfaces behind a wire protocol first; that is tracked in genai-server's ROADMAP.
One thing to set on the genai-server side
The portal builds its directory links from the host you are reading it on,
which behind a proxy yields https://ai.<domain>:3000 for Open WebUI — a
port not published there. Tell it where things really are:
services.genai-server.portal.serviceUrls = {
"Open WebUI" = "https://webui.<domain>";
"ComfyUI" = "https://comfy.<domain>";
"SearXNG" = "https://search.<domain>";
"Magentic-UI" = "https://magentic.<domain>";
"Realtime voice" = "https://voice.<domain>";
};
The voice page needs the last one specifically: it asks the portal where the voice service is published, and without it dials the page's own host.
Status
Ingress only.
genai-api only. Container apps for the CPU-capable services (voice, tts,
rag, memory, mcp, media-tools) are next; they will need the genai-server
flake as an input for lib.serverScripts.