Companion to DESIGN.md. Phased, each phase ends with a usable increment.
Status (2026-07-16): Phases 0, 1, 3, 4, 5 are ✅ complete (each exit criterion verified end-to-end on the kind dev stack). Remaining: Phase 6 (hardening beyond what shipped). Per-phase notes below record deviations.
Scope change (2026-07-16): Python app support (frameworks,
image/ociEnvsources, Nebi pixi environments — the old Phase 2) was removed from this pack in the morning (overlap with python-capability-pack's framework/image model), then reintroduced the same day in a simpler form: any source (inlinezip upload /git/pvc) carrying a pixi project, launched by a named pixi task (runtime.pixiTask) that must serve on0.0.0.0:8080. One shared pixi runtime image (pythonImagechart value) — no per-app images, no framework table, no build pipeline. Theframeworkfield andruntime.commandremain dropped.
- The
AppCR is the contract. Build it first; everything else produces or reconciles it. - Vertical slices. Each phase delivers a thing you can demo, not a horizontal layer.
- API is the single authority. UI and MCP are thin clients over apps-api from the start.
Goal: repo + CRD + a static app reconciles to a running pod behind Nebari SSO.
- Scaffold
nebari-apps-pack/per DESIGN §15;pack-metadata.yaml, Helm chart skeleton. - Define the
AppCRD (apps.nebari.dev/v1alpha1) — types in Go (kubebuilder) + generated CRD YAML. - apps-operator MVP: reconcile
source.type: inline|git→ Deployment(nginx) + Service +NebariApp(routing/auth/landing) + status. - Local dev loop (k3d/minikube + Tilt; mirror
k8s-deployconventions). - Exit:
kubectl applya staticApp→ reach it athttps://<sub>.<cluster>behind Keycloak.
Done.
pvcsources landed too. Deviations: local dev is kind + Makefile (mirroring software-pack-template, the adopted source of truth) rather than k3d/Tilt; app URLs are<sub>.apps.<cluster-domain>(dedicated apps zone); TLS is chart-toggleable (tls.enabled, off in local dev). NebariApp contract pinned to nebari-operatorv0.1.0-alpha.19with a contract test.
Goal: create/manage apps over HTTP; CR is written by the API, not by hand.
- FastAPI service: OIDC bearer auth (Keycloak, split-horizon issuer), RBAC by group/namespace.
POST/GET/PATCH/DELETE /apps,:stop/:start,/capabilities.- App CR rendering + apply via k8s client; DB (async SQLAlchemy) for metadata cache + audit.
- Status read-back from CR;
/apps/{id}/status. - Expose apps-api as a
NebariApp. - Exit: launch + delete a static app entirely through the REST API.
Done, and beyond:
/logs,/events,/analytics/summary, and multipart zip/.html upload (POST /apps/upload→ inline source). Deviations: no DB — the API is stateless and the CR remains the sole source of truth (a cache/audit DB can come later if listing scale demands it); authorization = valid Keycloak JWT + managed-namespace checks (finer group-based RBAC deferred); the API is not its ownNebariApp— it is served same-origin at/apithrough the UI's nginx.
Removed 2026-07-16, reintroduced the same day in a simpler form. The original Phase 2 (framework table,
image/ociEnvsources, Nebi env delivery) was cut for overlapping python-capability-pack's model, then Python support returned as:
runtime.pixiTaskon theAppCRD — set = pixi runtime, empty = static nginx.- Any source (
inlinezip upload /git/pvc) carrying a pixi project (pixi.toml/pyproject.tomlat the root).- Operator: source copied into a writable
/appemptyDir →pixi install(--lockedwith a lockfile) →pixi run <task>; TCP + generous startup probes; task must serve on0.0.0.0:8080(PORTinjected). SharedpythonImagechart value.- UI: app-type toggle + Launch task field; API/MCP:
pixiTask/pixi_task; skill:assets/python/starter. Inline files now support nested paths (ConfigMap generated keys + volume items).
Goal: the jhub-apps-style form launcher, JupyterHub-free.
- React + Vite + shadcn/ui; Keycloak SSO; TanStack Query + Jotai.
- Catalog/dashboard, launch form (source tabs, resources, env vars, access), app detail with status + logs viewer + events + metrics, edit/stop/start/delete.
- Expose as
NebariApp(landing-page tile "Apps"). - Exit: a user launches + manages an app end-to-end from the browser.
Done. Deviations: built on the official nebari-design system (shadcn-compatible registry) instead of stock shadcn/ui, following the chat-pack react baseline; keycloak-js SPA PKCE with runtime config (auth works with one image on or off); TanStack Query without Jotai (no client state warranted it yet). Includes a dashboard with analytics (status/source/namespace breakdowns, replica readiness) and zip/.html upload in the launch form. Detail view ships status + conditions + logs + events; pod metrics and a pre-populated edit form are still open. The UI lives at
apps.<cluster-domain>itself.
Goal: natural-language launch/manage from a coding agent.
- FastMCP server (streamable HTTP) exposed as a
NebariApp. - Keycloak device flow auth (
authenticatetool; token cache/refresh). - Tools:
launch_app,list_apps,get_app,get_app_status,get_app_logs,update_app,stop_app/start_app,remove_app,describe_cluster— all thin wrappers over apps-api. - LLM-oriented tool descriptions;
launch_appidempotent on(namespace, name). - Exit: from Claude Code/Codex: "launch this site" → running app.
Done. Deviations: no separate
NebariApp/hostname — the MCP is served atapps.<cluster-domain>/mcpthrough the UI's nginx, and the UI's NebariApp provisions the device-flow client alongside the SPA client. Beyond plan: middleware verifies JWTs at the MCP layer (all tools exceptauthenticate) before apps-api verifies them again; device-flow tokens are cached per MCP session; bearer passthrough supported. (list_frameworks/list_environmentswere removed with the Python scope cut, leaving 11 tools.)
Goal: agents generate apps in the exact expected layout.
- Skill (
/new-nebari-app): scaffold a static starter and anebari-app.yamlmanifest (1:1 withApp.spec). - Emits the NL launch instruction for the MCP; reads
nebari-app.yamlon launch. - Exit: agent generates an app, user says "launch it," MCP reads the manifest and deploys.
Done (
skill/new-nebari-app/: SKILL.md + manifest reference + static starter template). Deviations: the agent readsnebari-app.yamland maps it ontolaunch_app(the in-cluster MCP has no filesystem access) — the mapping table lives in the skill's reference. Exit verified: scaffold → "launch it" → Running → serving, via the live MCP. (The Streamlit/FastAPI starters were removed with the Python scope cut.)
- Pod hardening (non-root, RO FS, seccomp, limits), default-deny NetworkPolicies.
- Metrics (ServiceMonitor for operator + apps), events aggregation, audit surfacing.
- Public-app confirmation guardrails; secret handling via Secret refs.
- Docs: README, install guide, examples (sample
AppCRs + ArgoCD Application). - Exit: install via Helm/ArgoCD on a clean cluster following the docs; security review passes.
Shipped along the way: pod hardening (non-root, dropped capabilities, seccomp
RuntimeDefault, injection-safe git init containers, size/type-capped uploads), events aggregation in API/UI, docs site (Astro Starlight, 9 pages) + README + examples, CI (lint/test/build-image for all four components). Remaining: default-deny NetworkPolicies, ServiceMonitor/metrics, audit surfacing, resource limit defaults on app pods, public-app confirmation guardrails in the UI, an ArgoCD Application example, and a security review.
- CI/CD: image builds for operator/api/ui/mcp; chart lint; e2e against k3d (mirror other packs). Status: image builds (GHCR, 4 images), chart/CRD/example lint, and per-component test workflows are in; a CI e2e job against kind is still to add (the flow runs manually today).
- Versioning/release: semver tags + chart publish to
oci://quay.io/nebari/charts(pack convention). Status: not started. - Testing: operator envtest + reconcile unit tests; api pytest; ui vitest; e2e for the static-app flow. Status: operator fake-client reconcile + NebariApp contract tests; api pytest; mcp pytest against the real API in-process; docs build tests; ui has type-checked builds but no vitest suite yet.
- Phases 0→1 were the critical path; 3 (UI), 4 (MCP), and 5 (skill) built directly on them.
- Phase 5 (skill) depends on the
nebari-app.yamlschema being frozen (end of Phase 1). ✅ - Defer scale-to-zero unless prioritized.
- nebari-operator
NebariAppcontract drift — pin the version; add a contract test. ✅ Mitigated: pinned tov0.1.0-alpha.19, contract tests in the operator suite. - Untrusted agent-generated content — tenancy + hardening must land before any public exposure. ⏳ Pod hardening + namespace tenancy shipped; NetworkPolicies remain (Phase 6).
- Keycloak device-flow client provisioning — confirm nebari-operator exposes this for the MCP.
✅ Confirmed and in use:
auth.deviceFlowClienton the UI's NebariApp provisions it.