appmanager-api: render, install
diff --git a/core/installer/Makefile b/core/installer/Makefile
index ec40393..293d31d 100644
--- a/core/installer/Makefile
+++ b/core/installer/Makefile
@@ -15,3 +15,6 @@
 
 rpuppy:
 	./pcloud install --ssh-key=/Users/lekva/.ssh/id_rsa --app=rpuppy --repo-addr=ssh://localhost:2222/lekva
+
+appmanager:
+	./pcloud appmanager --ssh-key=/Users/lekva/.ssh/id_rsa --repo-addr=ssh://localhost:2222/lekva
diff --git a/core/installer/app.go b/core/installer/app.go
index 6f766b9..85f7dfc 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -13,9 +13,12 @@
 type App struct {
 	Name      string
 	Templates []*template.Template
+	Schema    string
+	Readme    *template.Template
 }
 
 type AppRepository interface {
+	GetAll() ([]App, error)
 	Find(name string) (*App, error)
 }
 
@@ -38,36 +41,50 @@
 	return nil, fmt.Errorf("Application not found: %s", name)
 }
 
+func (r InMemoryAppRepository) GetAll() ([]App, error) {
+	return r.apps, nil
+}
+
 func CreateAllApps() []App {
-	tmpls, err := template.ParseFS(valuesTmpls, "values-tmpl/*.yaml")
+	tmpls, err := template.ParseFS(valuesTmpls, "values-tmpl/*")
 	if err != nil {
 		log.Fatal(err)
 	}
 	return []App{
 		// CreateAppIngressPublic(tmpls),
-		CreateAppIngressPrivate(tmpls),
-		CreateAppCoreAuth(tmpls),
-		CreateAppVaultwarden(tmpls),
-		CreateAppMatrix(tmpls),
-		CreateAppPihole(tmpls),
-		CreateAppMaddy(tmpls),
-		CreateAppQBittorrent(tmpls),
-		CreateAppJellyfin(tmpls),
-		CreateAppRpuppy(tmpls),
-		CreateAppHeadscale(tmpls),
+		CreateAppIngressPrivate(valuesTmpls, tmpls),
+		CreateAppCoreAuth(valuesTmpls, tmpls),
+		CreateAppVaultwarden(valuesTmpls, tmpls),
+		CreateAppMatrix(valuesTmpls, tmpls),
+		CreateAppPihole(valuesTmpls, tmpls),
+		CreateAppMaddy(valuesTmpls, tmpls),
+		CreateAppQBittorrent(valuesTmpls, tmpls),
+		CreateAppJellyfin(valuesTmpls, tmpls),
+		CreateAppRpuppy(valuesTmpls, tmpls),
+		CreateAppHeadscale(valuesTmpls, tmpls),
 	}
 }
 
-func CreateAppIngressPublic(tmpls *template.Template) App {
+func CreateAppIngressPublic(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/ingress-public.jsonschema")
+	if err != nil {
+		panic(err)
+	}
 	return App{
 		"ingress-public",
 		[]*template.Template{
 			tmpls.Lookup("ingress-public.yaml"),
 		},
+		string(schema),
+		nil,
 	}
 }
 
-func CreateAppIngressPrivate(tmpls *template.Template) App {
+func CreateAppIngressPrivate(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/ingress-private.jsonschema")
+	if err != nil {
+		panic(err)
+	}
 	return App{
 		"ingress-private",
 		[]*template.Template{
@@ -75,88 +92,144 @@
 			tmpls.Lookup("ingress-private.yaml"),
 			tmpls.Lookup("certificate-issuer.yaml"),
 		},
+		string(schema),
+		nil,
 	}
 }
 
-func CreateAppCoreAuth(tmpls *template.Template) App {
+func CreateAppCoreAuth(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/core-auth.jsonschema")
+	if err != nil {
+		panic(err)
+	}
 	return App{
 		"core-auth",
 		[]*template.Template{
 			tmpls.Lookup("core-auth-storage.yaml"),
 			tmpls.Lookup("core-auth.yaml"),
 		},
+		string(schema),
+		nil,
 	}
 }
 
-func CreateAppVaultwarden(tmpls *template.Template) App {
+func CreateAppVaultwarden(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/vaultwarden.jsonschema")
+	if err != nil {
+		panic(err)
+	}
 	return App{
 		"vaultwarden",
 		[]*template.Template{
 			tmpls.Lookup("vaultwarden.yaml"),
 		},
+		string(schema),
+		nil,
 	}
 }
 
-func CreateAppMatrix(tmpls *template.Template) App {
+func CreateAppMatrix(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/matrix.jsonschema")
+	if err != nil {
+		panic(err)
+	}
 	return App{
 		"matrix",
 		[]*template.Template{
 			tmpls.Lookup("matrix-storage.yaml"),
 			tmpls.Lookup("matrix.yaml"),
 		},
+		string(schema),
+		nil,
 	}
 }
 
-func CreateAppPihole(tmpls *template.Template) App {
+func CreateAppPihole(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/pihole.jsonschema")
+	if err != nil {
+		panic(err)
+	}
 	return App{
 		"pihole",
 		[]*template.Template{
 			tmpls.Lookup("pihole.yaml"),
 		},
+		string(schema),
+		nil,
 	}
 }
 
-func CreateAppMaddy(tmpls *template.Template) App {
+func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/maddy.jsonschema")
+	if err != nil {
+		panic(err)
+	}
 	return App{
 		"maddy",
 		[]*template.Template{
 			tmpls.Lookup("maddy.yaml"),
 		},
+		string(schema),
+		nil,
 	}
 }
 
-func CreateAppQBittorrent(tmpls *template.Template) App {
+func CreateAppQBittorrent(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/qbittorrent.jsonschema")
+	if err != nil {
+		panic(err)
+	}
 	return App{
 		"qbittorrent",
 		[]*template.Template{
 			tmpls.Lookup("qbittorrent.yaml"),
 		},
+		string(schema),
+		nil,
 	}
 }
 
-func CreateAppJellyfin(tmpls *template.Template) App {
+func CreateAppJellyfin(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/jellyfin.jsonschema")
+	if err != nil {
+		panic(err)
+	}
 	return App{
 		"jellyfin",
 		[]*template.Template{
 			tmpls.Lookup("jellyfin.yaml"),
 		},
+		string(schema),
+		nil,
 	}
 }
 
-func CreateAppRpuppy(tmpls *template.Template) App {
+func CreateAppRpuppy(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/rpuppy.jsonschema")
+	if err != nil {
+		panic(err)
+	}
 	return App{
 		"rpuppy",
 		[]*template.Template{
 			tmpls.Lookup("rpuppy.yaml"),
 		},
+		string(schema),
+		tmpls.Lookup("rpuppy.md"),
 	}
 }
 
-func CreateAppHeadscale(tmpls *template.Template) App {
+func CreateAppHeadscale(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/headscale.jsonschema")
+	if err != nil {
+		panic(err)
+	}
 	return App{
 		"headscale",
 		[]*template.Template{
 			tmpls.Lookup("headscale.yaml"),
 		},
+		string(schema),
+		nil,
 	}
 }
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index 0678114..f2e4369 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -13,6 +13,7 @@
 	"github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/plumbing/object"
 	gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
+	"sigs.k8s.io/yaml"
 )
 
 const appDirName = "apps"
@@ -32,20 +33,36 @@
 	}, nil
 }
 
-func (m *AppManager) Install(app App) error {
+func (m *AppManager) Config() (Config, error) {
 	wt, err := m.repo.Worktree()
 	if err != nil {
-		return err
+		return Config{}, err
 	}
 	configF, err := wt.Filesystem.Open(configFileName)
 	if err != nil {
-		return err
+		return Config{}, err
 	}
 	defer configF.Close()
 	config, err := ReadConfig(configF)
 	if err != nil {
+		return Config{}, err
+	}
+	return config, nil
+}
+
+func (m *AppManager) Install(app App, config map[string]any) error {
+	wt, err := m.repo.Worktree()
+	if err != nil {
 		return err
 	}
+	globalConfig, err := m.Config()
+	if err != nil {
+		return err
+	}
+	all := map[string]any{
+		"Global": globalConfig.Values,
+		"Values": config,
+	}
 	appsRoot, err := wt.Filesystem.Chroot(appDirName)
 	if err != nil {
 		return err
@@ -76,11 +93,25 @@
 			return err
 		}
 		defer out.Close()
-		if err := t.Execute(out, config); err != nil {
+		if err := t.Execute(out, all); err != nil {
 			return err
 		}
 		appKust.Resources = append(appKust.Resources, t.Name())
 	}
+	{
+		out, err := appRoot.Create(configFileName)
+		if err != nil {
+			return err
+		}
+		defer out.Close()
+		configBytes, err := yaml.Marshal(config)
+		if err != nil {
+			return err
+		}
+		if _, err := out.Write(configBytes); err != nil {
+			return err
+		}
+	}
 	appKustF, err := appRoot.Create(kustomizationFileName)
 	if err != nil {
 		return err
diff --git a/core/installer/cmd/app_manager.go b/core/installer/cmd/app_manager.go
index 1fc6d64..249dcfb 100644
--- a/core/installer/cmd/app_manager.go
+++ b/core/installer/cmd/app_manager.go
@@ -1,9 +1,19 @@
 package main
 
 import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"net/http/httputil"
+	"net/url"
 	"os"
 
 	"github.com/giolekva/pcloud/core/installer"
+
+	"github.com/labstack/echo/v4"
 	"github.com/spf13/cobra"
 	"golang.org/x/crypto/ssh"
 )
@@ -11,30 +21,37 @@
 var appManagerFlags struct {
 	sshKey   string
 	repoAddr string
+	port     int
 }
 
 func appManagerCmd() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:  "appmanager",
-		RunE: installCmdRun,
+		RunE: appManagerCmdRun,
 	}
 	cmd.Flags().StringVar(
-		&installFlags.sshKey,
+		&appManagerFlags.sshKey,
 		"ssh-key",
 		"",
 		"",
 	)
 	cmd.Flags().StringVar(
-		&installFlags.repoAddr,
+		&appManagerFlags.repoAddr,
 		"repo-addr",
 		"",
 		"",
 	)
+	cmd.Flags().IntVar(
+		&appManagerFlags.port,
+		"port",
+		8080,
+		"",
+	)
 	return cmd
 }
 
 func appManagerCmdRun(cmd *cobra.Command, args []string) error {
-	sshKey, err := os.ReadFile(installFlags.sshKey)
+	sshKey, err := os.ReadFile(appManagerFlags.sshKey)
 	if err != nil {
 		return err
 	}
@@ -42,14 +59,148 @@
 	if err != nil {
 		return err
 	}
-	repo, err := cloneRepo(installFlags.repoAddr, signer)
+	repo, err := cloneRepo(appManagerFlags.repoAddr, signer)
 	if err != nil {
 		return err
 	}
-	_, err = installer.NewAppManager(repo, signer)
+	m, err := installer.NewAppManager(repo, signer)
 	if err != nil {
 		return err
 	}
-	// TODO(gio): start server
+	r := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+	s := &server{
+		port: appManagerFlags.port,
+		m:    m,
+		r:    r,
+	}
+	s.start()
 	return nil
 }
+
+type server struct {
+	port int
+	m    *installer.AppManager
+	r    installer.AppRepository
+}
+
+func (s *server) start() {
+	e := echo.New()
+	e.GET("/api/app-repo", s.handleAppRepo)
+	e.POST("/api/app/:slug/render", s.handleAppRender)
+	e.POST("/api/app/:slug/install", s.handleAppInstall)
+	e.GET("/api/app/:slug", s.handleApp)
+	webapp, err := url.Parse("http://localhost:5173")
+	if err != nil {
+		panic(err)
+	}
+	// var f ff
+	e.Any("/*", echo.WrapHandler(httputil.NewSingleHostReverseProxy(webapp)))
+	// e.Any("/*", echo.WrapHandler(&f))
+	fmt.Printf("Starting HTTP server on port: %d\n", s.port)
+	log.Fatal(e.Start(fmt.Sprintf(":%d", s.port)))
+}
+
+type app struct {
+	Name   string `json:"name"`
+	Slug   string `json:"slug"`
+	Schema string `json:"schema"`
+}
+
+func (s *server) handleAppRepo(c echo.Context) error {
+	all, err := s.r.GetAll()
+	if err != nil {
+		return err
+	}
+	resp := make([]app, len(all))
+	for i, a := range all {
+		resp[i] = app{a.Name, a.Name, a.Schema}
+	}
+	return c.JSON(http.StatusOK, resp)
+}
+
+func (s *server) handleApp(c echo.Context) error {
+	slug := c.Param("slug")
+	a, err := s.r.Find(slug)
+	if err != nil {
+		return err
+	}
+	return c.JSON(http.StatusOK, app{a.Name, a.Name, a.Schema})
+}
+
+type file struct {
+	Name     string `json:"name"`
+	Contents string `json:"contents"`
+}
+
+type rendered struct {
+	Readme string `json:"readme"`
+	Files  []file `json:"files"`
+}
+
+func (s *server) handleAppRender(c echo.Context) error {
+	slug := c.Param("slug")
+	contents, err := ioutil.ReadAll(c.Request().Body)
+	if err != nil {
+		return err
+	}
+	global, err := s.m.Config()
+	if err != nil {
+		return err
+	}
+	var values map[string]any
+	if err := json.Unmarshal(contents, &values); err != nil {
+		return err
+	}
+	all := map[string]any{
+		"Global": global.Values,
+		"Values": values,
+	}
+	a, err := s.r.Find(slug)
+	if err != nil {
+		return err
+	}
+	var readme bytes.Buffer
+	if err := a.Readme.Execute(&readme, all); err != nil {
+		return err
+	}
+	var resp rendered
+	resp.Readme = readme.String()
+	for _, tmpl := range a.Templates {
+		var f bytes.Buffer
+		if err := tmpl.Execute(&f, all); err != nil {
+			fmt.Printf("%+v\n", all)
+			fmt.Println(err.Error())
+			return err
+		} else {
+			resp.Files = append(resp.Files, file{tmpl.Name(), f.String()})
+		}
+	}
+	out, err := json.Marshal(resp)
+	if err != nil {
+		return err
+	}
+	if _, err := c.Response().Writer.Write(out); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (s *server) handleAppInstall(c echo.Context) error {
+	slug := c.Param("slug")
+	contents, err := ioutil.ReadAll(c.Request().Body)
+	if err != nil {
+		return err
+	}
+	var values map[string]any
+	if err := json.Unmarshal(contents, &values); err != nil {
+		return err
+	}
+	a, err := s.r.Find(slug)
+	if err != nil {
+		return err
+	}
+	if err := s.m.Install(*a, values); err != nil {
+		return err
+	}
+	return c.String(http.StatusOK, "Installed")
+}
diff --git a/core/installer/cmd/apps.go b/core/installer/cmd/apps.go
index 0a58be2..baab5bf 100644
--- a/core/installer/cmd/apps.go
+++ b/core/installer/cmd/apps.go
@@ -72,7 +72,7 @@
 	if err != nil {
 		return err
 	}
-	return m.Install(*app)
+	return m.Install(*app, nil)
 }
 
 func cloneRepo(address string, signer ssh.Signer) (*git.Repository, error) {
diff --git a/core/installer/cmd/main.go b/core/installer/cmd/main.go
index 9b7a6ba..7a71b90 100644
--- a/core/installer/cmd/main.go
+++ b/core/installer/cmd/main.go
@@ -25,6 +25,7 @@
 	rootCmd.AddCommand(bootstrapCmd())
 	rootCmd.AddCommand(createEnvCmd())
 	rootCmd.AddCommand(installCmd())
+	rootCmd.AddCommand(appManagerCmd())
 }
 
 func main() {
diff --git a/core/installer/go.mod b/core/installer/go.mod
index fb5822d..ac66c3a 100644
--- a/core/installer/go.mod
+++ b/core/installer/go.mod
@@ -5,8 +5,9 @@
 require (
 	github.com/go-git/go-billy/v5 v5.3.1
 	github.com/go-git/go-git/v5 v5.4.2
+	github.com/labstack/echo/v4 v4.10.2
 	github.com/spf13/cobra v1.4.0
-	golang.org/x/crypto v0.1.0
+	golang.org/x/crypto v0.6.0
 	golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53
 	helm.sh/helm/v3 v3.9.0
 	sigs.k8s.io/yaml v1.3.0
@@ -72,13 +73,14 @@
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
 	github.com/klauspost/compress v1.13.6 // indirect
+	github.com/labstack/gommon v0.4.0 // indirect
 	github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
 	github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
 	github.com/lib/pq v1.10.4 // indirect
 	github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
 	github.com/mailru/easyjson v0.7.6 // indirect
-	github.com/mattn/go-colorable v0.1.12 // indirect
-	github.com/mattn/go-isatty v0.0.14 // indirect
+	github.com/mattn/go-colorable v0.1.13 // indirect
+	github.com/mattn/go-isatty v0.0.17 // indirect
 	github.com/mattn/go-runewidth v0.0.9 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
@@ -109,20 +111,22 @@
 	github.com/sirupsen/logrus v1.8.1 // indirect
 	github.com/spf13/cast v1.4.1 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
-	github.com/stretchr/testify v1.7.1 // indirect
+	github.com/stretchr/testify v1.8.1 // indirect
+	github.com/valyala/bytebufferpool v1.0.0 // indirect
+	github.com/valyala/fasttemplate v1.2.2 // indirect
 	github.com/xanzy/ssh-agent v0.3.0 // indirect
 	github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
 	github.com/xeipuuv/gojsonschema v1.2.0 // indirect
 	github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect
 	go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
-	golang.org/x/net v0.1.0 // indirect
+	golang.org/x/net v0.7.0 // indirect
 	golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
-	golang.org/x/sys v0.1.0 // indirect
-	golang.org/x/term v0.1.0 // indirect
-	golang.org/x/text v0.4.0 // indirect
-	golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
+	golang.org/x/sys v0.5.0 // indirect
+	golang.org/x/term v0.5.0 // indirect
+	golang.org/x/text v0.7.0 // indirect
+	golang.org/x/time v0.3.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368 // indirect
 	google.golang.org/grpc v1.43.0 // indirect
@@ -130,7 +134,7 @@
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 	k8s.io/api v0.24.0 // indirect
 	k8s.io/apiextensions-apiserver v0.24.0 // indirect
 	k8s.io/apimachinery v0.24.0 // indirect
diff --git a/core/installer/go.sum b/core/installer/go.sum
index 6c78d39..7562e09 100644
--- a/core/installer/go.sum
+++ b/core/installer/go.sum
@@ -451,6 +451,10 @@
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
+github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
+github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
+github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
 github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
 github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
 github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
@@ -479,12 +483,15 @@
 github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
-github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI=
 github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
@@ -658,19 +665,28 @@
 github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
+github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
 github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
 github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
@@ -753,8 +769,8 @@
 golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
-golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
+golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
+golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -843,8 +859,8 @@
 golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
-golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -942,15 +958,17 @@
 golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
-golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
-golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -960,14 +978,15 @@
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
-golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
 golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
+golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -1178,8 +1197,9 @@
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
 gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
 gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
diff --git a/core/installer/values-tmpl/core-auth.jsonschema b/core/installer/values-tmpl/core-auth.jsonschema
new file mode 100644
index 0000000..ec6a2c5
--- /dev/null
+++ b/core/installer/values-tmpl/core-auth.jsonschema
@@ -0,0 +1,15 @@
+{
+  "type": "object",
+  "properties": {
+    "Values": {
+      "type": "object",
+      "properties": {
+        "NamespacePrefix": { "type": "string" },
+        "Id": { "type": "string" },
+        "Domain": { "type": "string" }
+      },
+      "additionalProperties": false
+    }
+  },
+  "additionalProperties": false
+}
diff --git a/core/installer/values-tmpl/headscale.jsonschema b/core/installer/values-tmpl/headscale.jsonschema
new file mode 100644
index 0000000..ec6a2c5
--- /dev/null
+++ b/core/installer/values-tmpl/headscale.jsonschema
@@ -0,0 +1,15 @@
+{
+  "type": "object",
+  "properties": {
+    "Values": {
+      "type": "object",
+      "properties": {
+        "NamespacePrefix": { "type": "string" },
+        "Id": { "type": "string" },
+        "Domain": { "type": "string" }
+      },
+      "additionalProperties": false
+    }
+  },
+  "additionalProperties": false
+}
diff --git a/core/installer/values-tmpl/ingress-private.jsonschema b/core/installer/values-tmpl/ingress-private.jsonschema
new file mode 100644
index 0000000..ec6a2c5
--- /dev/null
+++ b/core/installer/values-tmpl/ingress-private.jsonschema
@@ -0,0 +1,15 @@
+{
+  "type": "object",
+  "properties": {
+    "Values": {
+      "type": "object",
+      "properties": {
+        "NamespacePrefix": { "type": "string" },
+        "Id": { "type": "string" },
+        "Domain": { "type": "string" }
+      },
+      "additionalProperties": false
+    }
+  },
+  "additionalProperties": false
+}
diff --git a/core/installer/values-tmpl/ingress-public.jsonschema b/core/installer/values-tmpl/ingress-public.jsonschema
new file mode 100644
index 0000000..ec6a2c5
--- /dev/null
+++ b/core/installer/values-tmpl/ingress-public.jsonschema
@@ -0,0 +1,15 @@
+{
+  "type": "object",
+  "properties": {
+    "Values": {
+      "type": "object",
+      "properties": {
+        "NamespacePrefix": { "type": "string" },
+        "Id": { "type": "string" },
+        "Domain": { "type": "string" }
+      },
+      "additionalProperties": false
+    }
+  },
+  "additionalProperties": false
+}
diff --git a/core/installer/values-tmpl/jellyfin.jsonschema b/core/installer/values-tmpl/jellyfin.jsonschema
new file mode 100644
index 0000000..ec6a2c5
--- /dev/null
+++ b/core/installer/values-tmpl/jellyfin.jsonschema
@@ -0,0 +1,15 @@
+{
+  "type": "object",
+  "properties": {
+    "Values": {
+      "type": "object",
+      "properties": {
+        "NamespacePrefix": { "type": "string" },
+        "Id": { "type": "string" },
+        "Domain": { "type": "string" }
+      },
+      "additionalProperties": false
+    }
+  },
+  "additionalProperties": false
+}
diff --git a/core/installer/values-tmpl/maddy.jsonschema b/core/installer/values-tmpl/maddy.jsonschema
new file mode 100644
index 0000000..ec6a2c5
--- /dev/null
+++ b/core/installer/values-tmpl/maddy.jsonschema
@@ -0,0 +1,15 @@
+{
+  "type": "object",
+  "properties": {
+    "Values": {
+      "type": "object",
+      "properties": {
+        "NamespacePrefix": { "type": "string" },
+        "Id": { "type": "string" },
+        "Domain": { "type": "string" }
+      },
+      "additionalProperties": false
+    }
+  },
+  "additionalProperties": false
+}
diff --git a/core/installer/values-tmpl/matrix.jsonschema b/core/installer/values-tmpl/matrix.jsonschema
new file mode 100644
index 0000000..ec6a2c5
--- /dev/null
+++ b/core/installer/values-tmpl/matrix.jsonschema
@@ -0,0 +1,15 @@
+{
+  "type": "object",
+  "properties": {
+    "Values": {
+      "type": "object",
+      "properties": {
+        "NamespacePrefix": { "type": "string" },
+        "Id": { "type": "string" },
+        "Domain": { "type": "string" }
+      },
+      "additionalProperties": false
+    }
+  },
+  "additionalProperties": false
+}
diff --git a/core/installer/values-tmpl/pihole.jsonschema b/core/installer/values-tmpl/pihole.jsonschema
new file mode 100644
index 0000000..ec6a2c5
--- /dev/null
+++ b/core/installer/values-tmpl/pihole.jsonschema
@@ -0,0 +1,15 @@
+{
+  "type": "object",
+  "properties": {
+    "Values": {
+      "type": "object",
+      "properties": {
+        "NamespacePrefix": { "type": "string" },
+        "Id": { "type": "string" },
+        "Domain": { "type": "string" }
+      },
+      "additionalProperties": false
+    }
+  },
+  "additionalProperties": false
+}
diff --git a/core/installer/values-tmpl/qbittorrent.jsonschema b/core/installer/values-tmpl/qbittorrent.jsonschema
new file mode 100644
index 0000000..ec6a2c5
--- /dev/null
+++ b/core/installer/values-tmpl/qbittorrent.jsonschema
@@ -0,0 +1,15 @@
+{
+  "type": "object",
+  "properties": {
+    "Values": {
+      "type": "object",
+      "properties": {
+        "NamespacePrefix": { "type": "string" },
+        "Id": { "type": "string" },
+        "Domain": { "type": "string" }
+      },
+      "additionalProperties": false
+    }
+  },
+  "additionalProperties": false
+}
diff --git a/core/installer/values-tmpl/rpuppy.jsonschema b/core/installer/values-tmpl/rpuppy.jsonschema
new file mode 100644
index 0000000..36619aa
--- /dev/null
+++ b/core/installer/values-tmpl/rpuppy.jsonschema
@@ -0,0 +1,7 @@
+{
+  "type": "object",
+  "properties": {
+    "Subdomain": { "type": "string", "default": "woof" }
+  },
+  "additionalProperties": false
+}
diff --git a/core/installer/values-tmpl/rpuppy.md b/core/installer/values-tmpl/rpuppy.md
new file mode 100644
index 0000000..a7d4177
--- /dev/null
+++ b/core/installer/values-tmpl/rpuppy.md
@@ -0,0 +1 @@
+rpuppy application will be installed on public network and be accessible to any user on https://{{ .Values.Subdomain }}.{{ .Global.Domain }}
diff --git a/core/installer/values-tmpl/rpuppy.yaml b/core/installer/values-tmpl/rpuppy.yaml
index 05298b8..e6014a3 100644
--- a/core/installer/values-tmpl/rpuppy.yaml
+++ b/core/installer/values-tmpl/rpuppy.yaml
@@ -2,7 +2,7 @@
 kind: HelmRelease
 metadata:
   name: rpuppy
-  namespace: {{ .Values.NamespacePrefix }}app-rpuppy
+  namespace: {{ .Global.NamespacePrefix }}app-rpuppy
 spec:
   chart:
     spec:
@@ -10,9 +10,9 @@
       sourceRef:
         kind: GitRepository
         name: pcloud
-        namespace: {{ .Values.Id }}
+        namespace: {{ .Global.Id }}
   interval: 1m0s
   values:
     ingressClassName: pcloud-ingress-public
-    certificateIssuer: lekva-public
-    domain: woof.{{ .Values.Domain }}
+    certificateIssuer: {{ .Global.Id }}-public
+    domain: {{ .Values.Subdomain }}.{{ .Global.Domain }}
diff --git a/core/installer/values-tmpl/vaultwarden.jsonschema b/core/installer/values-tmpl/vaultwarden.jsonschema
new file mode 100644
index 0000000..ec6a2c5
--- /dev/null
+++ b/core/installer/values-tmpl/vaultwarden.jsonschema
@@ -0,0 +1,15 @@
+{
+  "type": "object",
+  "properties": {
+    "Values": {
+      "type": "object",
+      "properties": {
+        "NamespacePrefix": { "type": "string" },
+        "Id": { "type": "string" },
+        "Domain": { "type": "string" }
+      },
+      "additionalProperties": false
+    }
+  },
+  "additionalProperties": false
+}