No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-01 01:02:13 -07:00
apps webui: ask the gate to label requests with the caller's role 2026-08-01 01:02:13 -07:00
flake.nix genai-api: TLS+SSO ingress for the genai-server portal 2026-07-31 22:38:45 -07:00
README.md model surfaces as parent instances so they nest as sub-apps 2026-07-31 23:48:47 -07:00

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.