DodoApp: Support dev virtual machines

Change-Id: Ib7641adb5be477bdde7cd9a06df4b45aa65a1c01
diff --git a/core/auth/memberships/main.go b/core/auth/memberships/main.go
index ba5db7c..e72a163 100644
--- a/core/auth/memberships/main.go
+++ b/core/auth/memberships/main.go
@@ -691,6 +691,8 @@
 	go func() {
 		r := mux.NewRouter()
 		r.HandleFunc("/api/init", s.apiInitHandler)
+		// TODO(gio): change to /api/users/{username}
+		r.HandleFunc("/api/users/{username}/keys", s.apiAddUserKey).Methods(http.MethodPost)
 		r.HandleFunc("/api/user/{username}", s.apiMemberOfHandler)
 		r.HandleFunc("/api/users", s.apiGetAllUsers).Methods(http.MethodGet)
 		r.HandleFunc("/api/users", s.apiCreateUser).Methods(http.MethodPost)
@@ -1153,7 +1155,7 @@
 		http.Error(w, "SSH key not present", http.StatusBadRequest)
 		return
 	}
-	if err := s.store.AddSSHKeyForUser(username, sshKey); err != nil {
+	if err := s.store.AddSSHKeyForUser(strings.ToLower(username), sshKey); err != nil {
 		redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
 		http.Redirect(w, r, redirectURL, http.StatusFound)
 		return
@@ -1238,11 +1240,7 @@
 }
 
 func (s *Server) apiGetAllUsers(w http.ResponseWriter, r *http.Request) {
-	defer s.pingAllSyncAddresses()
-	selfAddress := r.FormValue("selfAddress")
-	if selfAddress != "" {
-		s.addSyncAddress(selfAddress)
-	}
+	s.addSyncAddress(r.FormValue("selfAddress"))
 	var users []User
 	var err error
 	groups := r.FormValue("groups")
@@ -1316,23 +1314,58 @@
 	}
 }
 
+type addUserKeyRequest struct {
+	User      string `json:"user"`
+	PublicKey string `json:"publicKey"`
+}
+
+func (s *Server) apiAddUserKey(w http.ResponseWriter, r *http.Request) {
+	var req addUserKeyRequest
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		http.Error(w, "Invalid request body", http.StatusBadRequest)
+		return
+	}
+	if req.User == "" {
+		http.Error(w, "Username cannot be empty", http.StatusBadRequest)
+		return
+	}
+	if req.PublicKey == "" {
+		http.Error(w, "PublicKey cannot be empty", http.StatusBadRequest)
+		return
+	}
+	if err := s.store.AddSSHKeyForUser(strings.ToLower(req.User), req.PublicKey); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
+// TODO(gio): enque sync event instead of directly reaching out to clients.
+// This will allow to deduplicate sync events and save resources.
 func (s *Server) pingAllSyncAddresses() {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 	for address := range s.syncAddresses {
-		resp, err := http.Get(address)
-		if err != nil {
-			log.Printf("Failed to ping %s: %v", address, err)
-			continue
-		}
-		resp.Body.Close()
-		if resp.StatusCode != http.StatusOK {
-			log.Printf("Ping to %s returned status %d", address, resp.StatusCode)
-		}
+		go func(address string) {
+			log.Printf("Pinging %s", address)
+			resp, err := http.Get(address)
+			if err != nil {
+				// TODO(gio): remove sync address after N number of failures.
+				log.Printf("Failed to ping %s: %v", address, err)
+				return
+			}
+			defer resp.Body.Close()
+			if resp.StatusCode != http.StatusOK {
+				log.Printf("Ping to %s returned status %d", address, resp.StatusCode)
+			}
+		}(address)
 	}
 }
 
 func (s *Server) addSyncAddress(address string) {
+	if address == "" {
+		return
+	}
+	fmt.Printf("Adding sync address: %s\n", address)
 	s.mu.Lock()
 	defer s.mu.Unlock()
 	s.syncAddresses[address] = struct{}{}
diff --git a/core/installer/app.go b/core/installer/app.go
index cee8a5f..3a16abe 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -346,7 +346,7 @@
 		return rendered{}, err
 	}
 	{
-		charts := res.LookupPath(cue.ParsePath("charts"))
+		charts := res.LookupPath(cue.ParsePath("output.charts"))
 		i, err := charts.Fields()
 		if err != nil {
 			return rendered{}, err
@@ -366,7 +366,7 @@
 		}
 	}
 	{
-		images := res.LookupPath(cue.ParsePath("images"))
+		images := res.LookupPath(cue.ParsePath("output.images"))
 		i, err := images.Fields()
 		if err != nil {
 			return rendered{}, err
@@ -380,8 +380,8 @@
 		}
 	}
 	{
-		output := res.LookupPath(cue.ParsePath("output"))
-		i, err := output.Fields()
+		helm := res.LookupPath(cue.ParsePath("output.helm"))
+		i, err := helm.Fields()
 		if err != nil {
 			return rendered{}, err
 		}
diff --git a/core/installer/app_configs/app_base.cue b/core/installer/app_configs/app_base.cue
index 9ca1eec..f058c96 100644
--- a/core/installer/app_configs/app_base.cue
+++ b/core/installer/app_configs/app_base.cue
@@ -1,5 +1,8 @@
 import (
-  "net"
+	"encoding/base64"
+	"encoding/yaml"
+	"list"
+	"net"
 )
 
 name: string | *""
@@ -47,22 +50,6 @@
 	name: string
 }
 
-volumes: {}
-volumes: {
-	for _, p in _postgresql {
-		for k, v in p.out.volumes {
-			"\(k)": v
-		}
-	}
-}
-volumes: {
-	for key, value in volumes {
-		"\(key)": #volume & value & {
-			name: key
-		}
-	}
-}
-
 #Chart: #GitRepositoryRef | #HelmRepositoryRef
 
 #GitRepositoryRef: {
@@ -114,56 +101,177 @@
 global: #Global
 release: #Release
 
-images: {}
-
-images: {
-	for key, value in images {
-		"\(key)": #Image & value
-	}
-    for _, value in _ingressValidate {
-        for name, image in value.out.images {
-            "\(name)": #Image & image
-        }
-    }
-    for _, value in _postgresql {
-        for name, image in value.out.images {
-            "\(name)": #Image & image
-        }
-    }
+#WriteFile: {
+	path: string
+	content: string
+	owner: string
+	permissions: string
 }
 
-charts: {}
-charts: {
-	for key, value in charts {
-		"\(key)": #Chart & value & {
-            name: key
-        }
-	}
-    for _, value in _ingressValidate {
-        for name, chart in value.out.charts {
-            "\(name)": #Chart & chart & {
-                name: name
-            }
-        }
-    }
-    for _, value in _postgresql {
-        for name, chart in value.out.charts {
-            "\(name)": #Chart & chart & {
-                name: name
-            }
-        }
-    }
+#CloudInit: {
+	runCmd: [...[...string]] | *[]
+	writeFiles: [...#WriteFile] | *[]
 }
-charts: {
-	volume: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/volumes"
+
+#VPNDisabled: {
+	enabled: false
+}
+
+#VPNEnabled: {
+	enabled: true
+	loginServer: string
+	authKey: string
+}
+
+#VPN: #VPNEnabled | #VPNDisabled
+
+#VirtualMachine: #WithOut & {
+	name: string
+	username: string
+	domain: string
+	vpn: #VPN | *{ enabled: false }
+	cpuCores: int
+	memory: string
+	sshKnownHosts: [...string] | *[]
+	sshAuthorizedKeys: [...string] | *[]
+	cloudInit: #CloudInit
+
+	_name: name
+	_cpuCores: cpuCores
+	_memory: memory
+
+	_codeServerPort: 9090
+
+	images: {}
+	charts: {
+		virtualMachine: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/virtual-machine"
+		}
+	}
+	charts: {
+		for key, value in charts {
+			"\(key)": value & {
+				name: key
+			}
+		}
+	}
+	helm: {
+		"\(_name)-virtual-machine": {
+			chart: charts.virtualMachine
+			info: "Creating \(_name) virtual machine"
+			annotations: {
+				"dodo.cloud/resource-type": "virtual-machine"
+				"dodo.cloud/resource.virtual-machine.name": _name
+				"dodo.cloud/resource.virtual-machine.user": username
+				"dodo.cloud/resource.virtual-machine.cpu-cores": "\(_cpuCores)"
+				"dodo.cloud/resource.virtual-machine.memory": _memory
+			}
+			values: {
+				name: _name
+				cpuCores: _cpuCores
+				memory: _memory
+				disk: {
+					source: "https://cloud.debian.org/images/cloud/bookworm-backports/latest/debian-12-backports-generic-amd64.qcow2"
+					size: "64Gi"
+				}
+				ports: [22, 8080, _codeServerPort]
+				servicePorts: [{
+					name: "ssh"
+					port: 22
+					targetPort: 22
+					protocol: "TCP"
+				}, {
+					name: "web"
+					port: 80
+					targetPort: 8080
+					protocol: "TCP"
+				}, {
+					name: _codeServerPortName
+					port: _codeServerPort
+					targetPort: _codeServerPort
+					protocol: "TCP"
+				}]
+				cloudInit: {
+					userData: base64.Encode(null, "#cloud-config\n\(yaml.Marshal(_cloudInitUserData))")
+					networkData: base64.Encode(null, yaml.Marshal({
+						version: 2
+						ethernets: {
+							enp1s0: {
+								dhcp4: true
+							}
+						}
+					}))
+				}
+			}
+			_cloudInitUserData: {
+				system_info: {
+					default_user: {
+						name: username
+						home: "/home/\(username)"
+					}
+				}
+				password: "dodo" // TODO(gio): remove if possible
+				chpasswd: {
+					expire: false
+				}
+				hostname: _name
+				ssh_pwauth: true
+				disable_root: false
+				ssh_authorized_keys: list.Concat([[
+					"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOa7FUrmXzdY3no8qNGUk7OPaRcIUi8G7MVbLlff9eB/ lekva@gl-mbp-m1-max.local"
+				], sshAuthorizedKeys])
+				packages: [
+					"curl",
+					// "emacs",
+					"git",
+					"openssh-client",
+				]
+				write_files: list.Concat([[{
+					path: "/home/\(username)/.gitconfig"
+					content: """
+					[user]
+						name = \(username)
+						email = \(username)@.\(domain)
+
+					"""
+					owner: "\(username):\(username)"
+					permissions: "0644"
+				}], cloudInit.writeFiles])
+				runcmd: list.Concat([[
+					["sh", "-c", "chown -R \(username):\(username) /home/\(username)"],
+					["sh", "-c", "ssh-keygen -t ed25519 -f /home/\(username)/.ssh/id_ed25519 -q -N ''"],
+					["sh", "-c", "chown \(username):\(username) /home/\(username)/.ssh/id_ed25519*"],
+					["sh", "-c", "chmod 0600 /home/\(username)/.ssh/id_ed25519*"],
+					// TODO(gio): implement post app delete webhook to remove ssh key from memberships
+					// TODO(gio): make memberships-api addr configurable
+					["sh", "-c", "PUBKEY=$(cat /home/\(username)/.ssh/id_ed25519.pub) && curl --request POST --data \"{\\\"user\\\":\\\"\(username)\\\",\\\"publicKey\\\":\\\"${PUBKEY}\\\"}\" http://memberships-api.\(global.namespacePrefix)core-auth-memberships.svc.cluster.local/api/users/\(username)/keys"],
+					// TODO(gio): this waits for user keys are synced from memberships service back to the dodo-app.
+					// We should inject this key into the dodo-app directly as well.
+					["sh", "-c", "sleep 20"],
+					if vpn.enabled {
+						["sh", "-c", "curl -fsSL https://tailscale.com/install.sh | sh"],
+					}
+					if vpn.enabled {
+						// TODO(gio): (maybe) enable tailscale ssh
+						["sh", "-c", "tailscale up --login-server=\(vpn.loginServer) --auth-key=\(vpn.authKey)"],
+					}
+					["sh", "-c", "curl -fsSL https://code-server.dev/install.sh | HOME=/home/\(username) sh"],
+					["sh", "-c", "systemctl enable --now code-server@\(username)"],
+					["sh", "-c", "sleep 10"],
+					// TODO(gio): (maybe) listen only on tailscale interface
+					["sh", "-c", "sed -i -e 's/127.0.0.1:8080/0.0.0.0:\(_codeServerPort)/g' /home/\(username)/.config/code-server/config.yaml"],
+					["sh", "-c", "sed -i -e 's/auth: password/auth: none/g' /home/\(username)/.config/code-server/config.yaml"],
+					["sh", "-c", "systemctl restart --now code-server@\(username)"],
+				], cloudInit.runCmd])
+			}
+		}
 	}
 }
 
-#PostgreSQL: {
+#PostgreSQL: #WithOut & {
 	name: string
 	version: "15.3"
 	initSQL: string | *""
@@ -172,108 +280,84 @@
 	_size: size
 	_volumeClaimName: "\(name)-postgresql"
 
-	out: {
-		images: {
-			postgres: #Image & {
-				repository: "library"
-				name: "postgres"
-				tag: version
-				pullPolicy: "IfNotPresent"
+	images: {
+		postgres: {
+			repository: "library"
+			name: "postgres"
+			tag: version
+			pullPolicy: "IfNotPresent"
+		}
+	}
+	charts: {
+		postgres: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/postgresql"
+		}
+	}
+	volumes: {
+		"\(_volumeClaimName)": size: _size
+	}
+	helm: {
+		postgres: {
+			chart: charts.postgres
+			annotations: {
+				"dodo.cloud/resource-type": "postgresql"
+				"dodo.cloud/resource.postgresql.name": name
+				"dodo.cloud/resource.postgresql.version": version
+				"dodo.cloud/resource.postgresql.volume": _volumeClaimName
 			}
-		}
-		charts: {
-			postgres: #Chart & {
-				kind: "GitRepository"
-				address: "https://code.v1.dodo.cloud/helm-charts"
-				branch: "main"
-				path: "charts/postgresql"
-			}
-		}
-		volumes: {
-			"\(_volumeClaimName)": size: _size
-		}
-		charts: {
-			for key, value in charts {
-				"\(key)": #Chart & value & {
-					name: key
+			values: {
+				fullnameOverride: "postgres-\(name)"
+				image: {
+					registry: images.postgres.registry
+					repository: images.postgres.imageName
+					tag: images.postgres.tag
+					pullPolicy: images.postgres.pullPolicy
 				}
-			}
-		}
-		helm: {
-			postgres: {
-				chart: charts.postgres
-				annotations: {
-					"dodo.cloud/resource-type": "postgresql"
-					"dodo.cloud/resource.postgresql.name": name
-					"dodo.cloud/resource.postgresql.version": version
-					"dodo.cloud/resource.postgresql.volume": _volumeClaimName
+				auth: {
+					postgresPassword: "postgres"
+					username: "postgres"
+					password: "postgres"
+					database: "postgres"
 				}
-				values: {
-					fullnameOverride: "postgres-\(name)"
-					image: {
-						registry: images.postgres.registry
-						repository: images.postgres.imageName
-						tag: images.postgres.tag
-						pullPolicy: images.postgres.pullPolicy
-					}
-					auth: {
-						postgresPassword: "postgres"
-						username: "postgres"
-						password: "postgres"
-						database: "postgres"
-					}
-					service: {
-						type: "ClusterIP"
-						port: 5432
-					}
-					global: {
-						postgresql: {
-							auth: {
-								postgresPassword: "postgres"
-								username: "postgres"
-								password: "postgres"
-								database: "postgres"
-							}
-						}
-					}
-					primary: {
-						persistence: existingClaim: _volumeClaimName
-						if initSQL != "" {
-							initdb: scripts: "init.sql": initSQL
-						}
-						securityContext: {
-							enabled: true
-							fsGroup: 0
-						}
-						containerSecurityContext: {
-							enabled: true
-							runAsUser: 0
-						}
-					}
-					volumePermissions: securityContext: runAsUser: 0
+				service: {
+					type: "ClusterIP"
+					port: 5432
 				}
+				global: {
+					postgresql: {
+						auth: {
+							postgresPassword: "postgres"
+							username: "postgres"
+							password: "postgres"
+							database: "postgres"
+						}
+					}
+				}
+				primary: {
+					persistence: existingClaim: _volumeClaimName
+					if initSQL != "" {
+						initdb: scripts: "init.sql": initSQL
+					}
+					securityContext: {
+						enabled: true
+						fsGroup: 0
+					}
+					containerSecurityContext: {
+						enabled: true
+						runAsUser: 0
+					}
+				}
+				volumePermissions: securityContext: runAsUser: 0
 			}
 		}
 	}
 }
 
-_ingressValidate: {}
-
-postgresql: {}
-_postgresql: {
-	for key, value in postgresql {
-		"\(key)": #PostgreSQL & value & {
-			name: key
-		}
-	}
-}
-
-localCharts: {
-	for key, _ in charts {
-		"\(key)": {
-        }
-    }
-}
+localCharts: {}
+_localCharts: localCharts
 
 #ResourceReference: {
     name: string
@@ -288,41 +372,6 @@
 	...
 }
 
-helm: {}
-_helmValidate: {
-	for key, value in helm {
-		"\(key)": #Helm & value & {
-			name: key
-		}
-	}
-	for key, value in volumes {
-		"\(key)-volume": #Helm & {
-			chart: charts.volume
-			info: "Creating disk for \(key)"
-			annotations: {
-				"dodo.cloud/resource-type": "volume"
-				"dodo.cloud/resource.volume.name": value.name
-				"dodo.cloud/resource.volume.size": value.size
-			}
-			values: value
-		}
-	}
-	for key, value in _ingressValidate {
-		for ing, ingValue in value.out.helm {
-			"\(key)-\(ing)": #Helm & ingValue & {
-				name: "\(key)-\(ing)"
-			}
-		}
-	}
-	for key, value in _postgresql {
-		for post, postValue in value.out.helm {
-			"\(key)-\(post)": #Helm & postValue & {
-				name: "\(key)-\(post)"
-			}
-		}
-	}
-}
-
 #HelmRelease: {
 	_name: string
 	_chart: _
@@ -349,14 +398,25 @@
 }
 
 output: {
-	for name, r in _helmValidate {
-		"\(name)": #HelmRelease & {
-			_name: name
-            _chart: localCharts[r.chart.name]
-			_values: r.values
-			_dependencies: r.dependsOn
-			_info: r.info
-			_annotations: r.annotations
+	images: out.images
+	charts: out.charts
+	_lc: _localCharts & {
+		for k, v in out.charts {
+			"\(k)": {
+				...
+			}
+		}
+	}
+	helm: {
+		for name, r in out.helmR {
+			"\(name)": #HelmRelease & {
+				_name: name
+				_chart: _lc[r.chart.name]
+				_values: r.values
+				_dependencies: r.dependsOn
+				_info: r.info
+				_annotations: r.annotations
+			}
 		}
 	}
 }
@@ -375,3 +435,138 @@
 help: [...#HelpDocument] | *[]
 
 url: string | *""
+
+#WithOut: {
+	images: {...}
+	charts: {...}
+	helm: {...}
+	images: {
+		for key, value in images {
+			"\(key)": #Image & value
+		}
+	}
+	charts: {
+		for k, v in charts {
+			"\(k)": #Chart & v & {
+				name: k
+			}
+		}
+	}
+	helmR: {
+		for key, value in helm {
+			"\(key)": #Helm & value & {
+				name: key
+			}
+		}
+		...
+	}
+	...
+}
+
+#WithOut: {
+	charts: {
+		volume: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/volumes"
+		}
+		...
+	}
+	volumes: {...}
+	volumes: {
+		for k, v in volumes {
+			"\(k)": #volume & v & {
+				name: k
+			}
+		}
+	}
+	helmR: {
+		for key, value in volumes {
+			"\(key)-volume": #Helm & {
+				name: key
+				chart: charts.volume
+				info: "Creating disk for \(key)"
+				annotations: {
+					"dodo.cloud/resource-type": "volume"
+					"dodo.cloud/resource.volume.name": value.name
+					"dodo.cloud/resource.volume.size": value.size
+				}
+				values: value
+			}
+		}
+	}
+}
+
+#WithOut: {
+	postgresql: {...}
+	postgresql: {
+		for k, v in postgresql {
+			"\(k)": #PostgreSQL & v
+		}
+		...
+	}
+	images: {
+		for k, v in postgresql {
+			for x, y in v.images {
+				"\(x)": y
+			}
+		}
+	}
+	charts: {
+		for k, v in postgresql {
+			for x, y in v.charts {
+				"\(x)": y
+			}
+		}
+	}
+	helmR: {
+		for k, v in postgresql {
+			for x, y in v.helmR {
+				"\(x)": y
+			}
+		}
+		...
+	}
+	...
+}
+
+#WithOut: {
+	vm: {...}
+	_vm: {...}
+	_vm: {
+		for k, v in vm if len(v) > 0 {
+			"\(k)": #VirtualMachine & v & {
+				name: k
+			}
+		}
+	}
+	images: {
+		for k, v in _vm {
+			for x, y in v.images {
+				"\(x)": y
+			}
+		}
+	}
+	charts: {
+		for k, v in _vm {
+			for x, y in v.charts {
+				"\(x)": y
+			}
+		}
+	}
+	helmR: {
+		for k, v in _vm {
+			for x, y in v.helmR {
+				"\(x)": y
+			}
+		}
+		...
+	}
+	...
+}
+
+out: #WithOut
+out: {}
+
+_codeServerPortName: "code-server"
diff --git a/core/installer/app_configs/app_global_env.cue b/core/installer/app_configs/app_global_env.cue
index 1681a1b..e8c8f57 100644
--- a/core/installer/app_configs/app_global_env.cue
+++ b/core/installer/app_configs/app_global_env.cue
@@ -27,7 +27,8 @@
 
 networks: #Networks
 
-#Ingress: {
+#Ingress: #WithOut & {
+	name: string
 	auth: #Auth
 	network: #Network
 	subdomain: string
@@ -36,93 +37,88 @@
 		name: string
 		port: close({ name: string }) | close({ number: int & > 0 })
 	})
+	g?: #Global
 
 	_domain: "\(subdomain).\(network.domain)"
 	_appRoot: appRoot
+	_authProxyName: "\(name)-auth-proxy"
     _authProxyHTTPPortName: "http"
 
-	out: {
-		images: {
-			authProxy: #Image & {
-				repository: "giolekva"
-				name: "auth-proxy"
-				tag: "latest"
-				pullPolicy: "Always"
-			}
+	images: {
+		authProxy: {
+			repository: "giolekva"
+			name: "auth-proxy"
+			tag: "latest"
+			pullPolicy: "Always"
 		}
-		charts: {
-			ingress: #Chart & {
-				kind: "GitRepository"
-				address: "https://code.v1.dodo.cloud/helm-charts"
-				branch: "main"
-				path: "charts/ingress"
-			}
-			authProxy: #Chart & {
-				kind: "GitRepository"
-				address: "https://code.v1.dodo.cloud/helm-charts"
-				branch: "main"
-				path: "charts/auth-proxy"
-			}
+	}
+	charts: {
+		ingress: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/ingress"
 		}
-		charts: {
-			for key, value in charts {
-				"\(key)": #Chart & value & {
-					name: key
-				}
-			}
+		authProxy: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/auth-proxy"
 		}
-		helm: {
-			if auth.enabled {
-				"auth-proxy": {
-					chart: charts.authProxy
-					info: "Installing authentication proxy"
-					values: {
-						image: {
-							repository: images.authProxy.fullName
-							tag: images.authProxy.tag
-							pullPolicy: images.authProxy.pullPolicy
-						}
-						upstream: "\(service.name).\(release.namespace).svc.cluster.local"
-						whoAmIAddr: "https://accounts.\(global.domain)/sessions/whoami"
-						loginAddr: "https://accounts-ui.\(global.domain)/login"
-						membershipAddr: "http://memberships-api.\(global.id)-core-auth-memberships.svc.cluster.local/api/user"
-						if global.privateDomain == "" {
-							membershipPublicAddr: "https://memberships.\(global.domain)"
-						}
-						if global.privateDomain != "" {
-							membershipPublicAddr: "https://memberships.\(global.privateDomain)"
-						}
-						groups: auth.groups
-						portName: _authProxyHTTPPortName
-					}
-				}
-			}
-			"\(_domain)": {
-				chart: charts.ingress
-				_service: service
-                info: "Generating TLS certificate for https://\(_domain)"
-				annotations: {
-					"dodo.cloud/resource-type": "ingress"
-					"dodo.cloud/resource.ingress.host": "https://\(_domain)"
-				}
+	}
+	helm: {
+		if auth.enabled {
+			"\(name)-auth-proxy": {
+				chart: charts.authProxy
+				info: "Installing authentication proxy"
+				_name: name
 				values: {
-					domain: _domain
-					appRoot: _appRoot
-					ingressClassName: network.ingressClass
-					certificateIssuer: network.certificateIssuer
-					service: {
-						if auth.enabled {
-							name: "auth-proxy"
-                            port: name: _authProxyHTTPPortName
+					name: _authProxyName
+					image: {
+						repository: images.authProxy.fullName
+						tag: images.authProxy.tag
+						pullPolicy: images.authProxy.pullPolicy
+					}
+					upstream: "\(service.name).\(release.namespace).svc.cluster.local"
+					whoAmIAddr: "https://accounts.\(g.domain)/sessions/whoami"
+					loginAddr: "https://accounts-ui.\(g.domain)/login"
+					membershipAddr: "http://memberships-api.\(g.namespacePrefix)core-auth-memberships.svc.cluster.local/api/user"
+					if g.privateDomain == "" {
+						membershipPublicAddr: "https://memberships.\(g.domain)"
+					}
+					if g.privateDomain != "" {
+						membershipPublicAddr: "https://memberships.\(g.privateDomain)"
+					}
+					groups: auth.groups
+					portName: _authProxyHTTPPortName
+				}
+			}
+		}
+		"\(name)-ingress": {
+			chart: charts.ingress
+			_service: service
+			info: "Generating TLS certificate for https://\(_domain)"
+			annotations: {
+				"dodo.cloud/resource-type": "ingress"
+				"dodo.cloud/resource.ingress.host": "https://\(_domain)"
+			}
+			values: {
+				domain: _domain
+				appRoot: _appRoot
+				ingressClassName: network.ingressClass
+				certificateIssuer: network.certificateIssuer
+				service: {
+					if auth.enabled {
+						name: _authProxyName
+						port: name: _authProxyHTTPPortName
+					}
+					if !auth.enabled {
+						name: _service.name
+						if _service.port.name != _|_ {
+							port: name: _service.port.name
 						}
-						if !auth.enabled {
-							name: _service.name
-							if _service.port.name != _|_ {
-								port: name: _service.port.name
-							}
-							if _service.port.number != _|_ {
-								port: number: _service.port.number
-							}
+						if _service.port.number != _|_ {
+							port: number: _service.port.number
 						}
 					}
 				}
@@ -131,9 +127,38 @@
 	}
 }
 
-ingress: {}
-_ingressValidate: {
-	for key, value in ingress {
-		"\(key)": #Ingress & value
+#WithOut: {
+	ingress: {...}
+	ingress: {
+		for k, v in ingress {
+			"\(k)": #Ingress & v & {
+				name: k
+				g: global
+			}
+		}
+		...
 	}
+	images: {
+		for k, v in ingress {
+			for x, y in v.images {
+				"\(x)": y
+			}
+		}
+	}
+	charts: {
+		for k, v in ingress {
+			for x, y in v.charts {
+				"\(x)": y
+			}
+		}
+	}
+	helmR: {
+		for k, v in ingress {
+			for x, y in v.helmR {
+				"\(x)": y
+			}
+		}
+		...
+	}
+	...
 }
diff --git a/core/installer/app_configs/dodo_app.cue b/core/installer/app_configs/dodo_app.cue
index a6fc803..84f236b 100644
--- a/core/installer/app_configs/dodo_app.cue
+++ b/core/installer/app_configs/dodo_app.cue
@@ -1,14 +1,56 @@
 import (
 	"encoding/base64"
 	"encoding/json"
+	"list"
 	"strings"
 )
 
 input: {
 	repoAddr: string
+	repoPublicAddr: string
 	managerAddr: string
 	appId: string
+	branch: string
 	sshPrivateKey: string
+	// TODO(gio): this should not be necessary as app.dev.username is autogenerated
+	username?: string
+}
+
+_devVM: {}
+
+if app.dev.enabled {
+	input: {
+		username?: string | *app.dev.username
+		vpnAuthKey: string @role(VPNAuthKey) @usernameField(username)
+	}
+
+	_devVM: {
+		username: app.dev.username
+		domain: global.domain
+		vpn: {
+			enabled: true
+			loginServer: "https://headscale.\(global.domain)"
+			authKey: input.vpnAuthKey
+		}
+		cpuCores: 1
+		memory: "1Gi"
+		cloudInit: {
+			runCmd: list.Concat([[
+				["sh", "-c", "chown \(username):\(username) /home/\(username)/.cache"],
+				["sh", "-c", "GIT_SSH_COMMAND='ssh -i /home/\(username)/.ssh/id_ed25519 -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new' git clone --branch \(input.branch) \(input.repoPublicAddr)/\(input.appId) /home/\(username)/code"],
+				["sh", "-c", "chown -R \(username):\(username) /home/\(username)/code"],
+				["sh", "-c", "chown \(username):\(username) /home/\(username)/.gitconfig"],
+	        ], app.vm.cloudInit.runCmd])
+		}
+	}
+}
+
+_vmName: "\(input.appId)-\(input.branch)"
+
+out: {
+	vm: {
+		"\(_vmName)": _devVM
+	}
 }
 
 #AppIngress: {
@@ -51,12 +93,39 @@
 	env: [...string] | *[]
 }
 
+#DevDisabled: {
+	enabled: false
+}
+
+#DevEnabled: {
+	enabled: true
+	username: string
+}
+
+#Dev: #DevEnabled | #DevDisabled
+
+#VMCustomization: {
+	cloudInit: #CloudInit
+}
+
+#AppTmpl: {
+	type: string
+	ingress: #AppIngress
+	volumes: #Volumes
+	postgresql: #PostgreSQLs
+	rootDir: string
+	runConfiguration: [...#Command]
+	dev: #Dev | *{ enabled: false }
+	vm: #VMCustomization
+	...
+}
+
 // Go app
 
 _goVer1220: "golang:1.22.0"
 _goVer1200: "golang:1.20.0"
 
-#GoAppTmpl: {
+#GoAppTmpl: #AppTmpl & {
 	type: _goVer1220 | _goVer1200
 	run: string | *"main.go"
 	ingress: #AppIngress
@@ -73,7 +142,6 @@
 		args: ["build", "-o", ".app", run]
 	}, {
 		bin: ".app",
-		args: [],
 		env: [
 			for k, v in volumes {
 				"DODO_VOLUME_\(strings.ToUpper(k))=/dodo-volume/\(v.name)"
@@ -96,6 +164,12 @@
 
 #GoApp1200: #GoAppTmpl & {
 	type: _goVer1200
+	vm: cloudInit: runCmd: [
+		["sh", "-c", "wget https://go.dev/dl/go1.20.linux-amd64.tar.gz -O /tmp/go.tar.gz"],
+		["sh", "-c", "rm -rf /usr/local/go && tar -C /usr/local -xzf /tmp/go.tar.gz"],
+		["sh", "-c", "echo \"export PATH=$PATH:/usr/local/go/bin\\n\" > /etc/environment"],
+		["sh", "-c", "rm /tmp/go.tar.gz"],
+    ]
 }
 
 #GoApp1220: #GoAppTmpl & {
@@ -108,7 +182,7 @@
 
 _hugoLatest: "hugo:latest"
 
-#HugoAppTmpl: {
+#HugoAppTmpl: #AppTmpl & {
 	type: _hugoLatest
 	ingress: #AppIngress
 	volumes: {}
@@ -118,7 +192,6 @@
 
 	runConfiguration: [{
 		bin: "/usr/bin/hugo",
-		args: []
 	}, {
 		bin: "/usr/bin/hugo",
 		args: [
@@ -136,7 +209,7 @@
 
 // PHP app
 
-#PHPAppTmpl: {
+#PHPAppTmpl: #AppTmpl & {
 	type: "php:8.2-apache"
 	ingress: #AppIngress
 	volumes: {}
@@ -160,65 +233,91 @@
 
 app: #App
 
-// output
-
 _app: app
-ingress: {
-	app: {
-		network: networks[strings.ToLower(_app.ingress.network)]
-		subdomain: _app.ingress.subdomain
-		auth: _app.ingress.auth
-		service: {
-			name: "app-app"
-			port: name: "app"
+
+if !_app.dev.enabled {
+	{
+	out: {
+		ingress: {
+			app: {
+				network: networks[strings.ToLower(_app.ingress.network)]
+				subdomain: _app.ingress.subdomain
+				auth: _app.ingress.auth
+				service: {
+					name: "app-app"
+					port: name: "app"
+				}
+			}
+		}
+		images: {
+			app: {
+				repository: "giolekva"
+				name: "app-runner"
+				tag: strings.Replace(_app.type, ":", "-", -1)
+				pullPolicy: "Always"
+			}
+		}
+		charts: {
+			app: {
+				kind: "GitRepository"
+				address: "https://code.v1.dodo.cloud/helm-charts"
+				branch: "main"
+				path: "charts/app-runner"
+			}
+		}
+		helm: {
+			app: {
+				chart: charts.app
+				values: {
+					image: {
+						repository: images.app.fullName
+						tag: images.app.tag
+						pullPolicy: images.app.pullPolicy
+					}
+					runtimeClassName: "untrusted-external" // TODO(gio): make this part of the infra config
+					appPort: _app.port
+					appDir: _app.rootDir
+					appId: input.appId
+					repoAddr: input.repoAddr
+					sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
+					runCfg: base64.Encode(null, json.Marshal(_app.runConfiguration))
+					managerAddr: input.managerAddr
+					volumes: [
+						for key, value in _app.volumes {
+							name: value.name
+							mountPath: "/dodo-volume/\(key)"
+						}
+                ]
+				}
+			}
 		}
 	}
+		}
 }
 
-images: {
-	app: {
-		repository: "giolekva"
-		name: "app-runner"
-		tag: strings.Replace(_app.type, ":", "-", -1)
-		pullPolicy: "Always"
-	}
-}
-
-charts: {
-	app: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/app-runner"
-	}
-}
-
-volumes: app.volumes
-postgresql: app.postgresql
-
-helm: {
-	app: {
-		chart: charts.app
-		values: {
-			image: {
-				repository: images.app.fullName
-				tag: images.app.tag
-				pullPolicy: images.app.pullPolicy
-			}
-			runtimeClassName: "untrusted-external" // TODO(gio): make this part of the infra config
-			appPort: _app.port
-			appDir: _app.rootDir
-			appId: input.appId
-			repoAddr: input.repoAddr
-			sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
-			runCfg: base64.Encode(null, json.Marshal(_app.runConfiguration))
-			managerAddr: input.managerAddr
-			volumes: [
-				for key, value in _app.volumes {
-					name: value.name
-					mountPath: "/dodo-volume/\(key)"
+if _app.dev.enabled {
+	{
+		out: {
+			ingress: {
+				app: {
+					network: networks[strings.ToLower(_app.ingress.network)]
+					subdomain: _app.ingress.subdomain
+					auth: _app.ingress.auth
+					service: {
+						name: _vmName
+						port: name: "web"
+					}
 				}
-            ]
+				code: {
+					network: networks[strings.ToLower(_app.ingress.network)]
+					subdomain: "code-\(_app.ingress.subdomain)"
+					auth: enabled: false
+					service: {
+						name: _vmName
+						port: name: _codeServerPortName
+					}
+				}
+			}
 		}
 	}
 }
diff --git a/core/installer/app_configs/testapp.cue b/core/installer/app_configs/testapp.cue
deleted file mode 100644
index 7ab6829..0000000
--- a/core/installer/app_configs/testapp.cue
+++ /dev/null
@@ -1,22 +0,0 @@
-app: {
-	type: "golang:1.22.0"
-	run: "main.go"
-	ingress: {
-		network: "private"
-		subdomain: "testapp"
-		auth: enabled: false
-	}
-	volumes: {
-		data: {
-			size: "1Gi"
-		}
-	}
-	postgresql: {
-		foo: {
-			size: "2Gi"
-		}
-	}
-}
-
-// do create app --type=go[1.22.0] [--run-cmd=(*default main.go)]
-// do create ingress --subdomain=testapp [--network=public (*default private)] [--auth] [--auth-groups="admin" (*default empty)] TODO(gio): port
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index 3157a45..013c05d 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -76,7 +76,7 @@
 	}
 }
 
-func (m *AppManager) FindAllInstances() ([]AppInstanceConfig, error) {
+func (m *AppManager) GetAllInstances() ([]AppInstanceConfig, error) {
 	m.repoIO.Pull()
 	kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
 	if err != nil {
@@ -94,7 +94,7 @@
 	return ret, nil
 }
 
-func (m *AppManager) FindAllAppInstances(name string) ([]AppInstanceConfig, error) {
+func (m *AppManager) GetAllAppInstances(name string) ([]AppInstanceConfig, error) {
 	kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
 	if err != nil {
 		if errors.Is(err, fs.ErrNotExist) {
@@ -117,22 +117,30 @@
 	return ret, nil
 }
 
-func (m *AppManager) FindInstance(id string) (*AppInstanceConfig, error) {
-	kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
+func (m *AppManager) GetInstance(id string) (*AppInstanceConfig, error) {
+	appDir := filepath.Clean(filepath.Join(m.appDirRoot, id))
+	cfgPath := filepath.Join(appDir, "config.json")
+	// kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
+	// if err != nil {
+	// 	return nil, err
+	// }
+	// for _, app := range kust.Resources {
+	// 	if app == id {
+	// cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
+	cfg, err := m.appConfig(cfgPath)
 	if err != nil {
 		return nil, err
 	}
-	for _, app := range kust.Resources {
-		if app == id {
-			cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
-			if err != nil {
-				return nil, err
-			}
-			cfg.Id = id
-			return &cfg, nil
-		}
-	}
-	return nil, ErrorNotFound
+	cfg.Id = id
+	return &cfg, err
+	// if err != nil {
+	// 	return nil, err
+	// }
+	// 		cfg.Id = id
+	// 		return &cfg, nil
+	// 	}
+	// }
+	// return nil, ErrorNotFound
 }
 
 func GetCueAppData(fs soft.RepoFS, dir string) (CueAppData, error) {
@@ -653,7 +661,7 @@
 			DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/remove", env.Id),
 		})
 	}
-	n, err := m.FindAllAppInstances("network")
+	n, err := m.GetAllAppInstances("network")
 	if err != nil {
 		return nil, err
 	}
diff --git a/core/installer/app_test.go b/core/installer/app_test.go
index f0f0e64..2987c80 100644
--- a/core/installer/app_test.go
+++ b/core/installer/app_test.go
@@ -63,7 +63,7 @@
 
 func TestAuthProxyEnabled(t *testing.T) {
 	r := NewInMemoryAppRepository(CreateAllApps())
-	for _, app := range []string{"rpuppy", "pi-hole", "url-shortener"} {
+	for _, app := range []string{"rpuppy"} {
 		a, err := FindEnvApp(r, app)
 		if err != nil {
 			t.Fatal(err)
@@ -94,7 +94,7 @@
 
 func TestAuthProxyDisabled(t *testing.T) {
 	r := NewInMemoryAppRepository(CreateAllApps())
-	for _, app := range []string{"rpuppy", "pi-hole", "url-shortener"} {
+	for _, app := range []string{"rpuppy"} {
 		a, err := FindEnvApp(r, app)
 		if err != nil {
 			t.Fatal(err)
@@ -337,11 +337,37 @@
 	}
 }
 
-//go:embed app_configs/testapp.cue
-var testAppCue []byte
+var dodoAppDevDisabledCue = `
+app: {
+	type: "golang:1.22.0"
+	run: "main.go"
+	ingress: {
+		network: "private"
+		subdomain: "testapp"
+		auth: enabled: false
+	}
+	dev: {
+		enabled: false
+	}
+}`
 
-func TestPCloudApp(t *testing.T) {
-	app, err := NewDodoApp(testAppCue)
+var dodoAppDevEnabledCue = `
+app: {
+	type: "golang:1.22.0"
+	run: "main.go"
+	ingress: {
+		network: "private"
+		subdomain: "testapp"
+		auth: enabled: false
+	}
+	dev: {
+		enabled: true
+		username: "gio"
+	}
+}`
+
+func TestDodoAppDevDisabled(t *testing.T) {
+	app, err := NewDodoApp([]byte(dodoAppDevDisabledCue))
 	if err != nil {
 		for _, e := range errors.Errors(err) {
 			t.Log(e)
@@ -355,15 +381,60 @@
 		RepoAddr:      "ssh://192.168.100.210:22/config",
 		AppDir:        "/foo/bar",
 	}
-	_, err = app.Render(release, env, networks, map[string]any{
-		"repoAddr":      "",
-		"managerAddr":   "",
-		"appId":         "",
-		"sshPrivateKey": "",
-	}, nil, nil)
+	keyGen := testKeyGen{}
+	r, err := app.Render(release, env, networks, map[string]any{
+		"repoAddr":       "",
+		"repoPublicAddr": "",
+		"managerAddr":    "",
+		"appId":          "",
+		"branch":         "",
+		"sshPrivateKey":  "",
+	}, nil, keyGen)
 	if err != nil {
+		for _, e := range errors.Errors(err) {
+			for _, f := range errors.Errors(e) {
+				for _, g := range errors.Errors(f) {
+					t.Log(g)
+				}
+			}
+		}
 		t.Fatal(err)
 	}
+	t.Log(string(r.Raw))
+}
+
+func TestDodoAppDevEnabled(t *testing.T) {
+	app, err := NewDodoApp([]byte(dodoAppDevEnabledCue))
+	if err != nil {
+		for _, e := range errors.Errors(err) {
+			t.Log(e)
+		}
+		t.Fatal(err)
+	}
+
+	release := Release{
+		Namespace:     "foo",
+		AppInstanceId: "foo-bar",
+		RepoAddr:      "ssh://192.168.100.210:22/config",
+		AppDir:        "/foo/bar",
+	}
+	keyGen := testKeyGen{}
+	r, err := app.Render(release, env, networks, map[string]any{
+		"repoAddr":       "",
+		"repoPublicAddr": "",
+		"managerAddr":    "",
+		"appId":          "",
+		"branch":         "",
+		"sshPrivateKey":  "",
+		"username":       "",
+	}, nil, keyGen)
+	if err != nil {
+		for _, e := range errors.Errors(err) {
+			t.Log(e)
+		}
+		t.Fatal(err)
+	}
+	t.Log(string(r.Raw))
 }
 
 func TestDodoAppInstance(t *testing.T) {
@@ -380,8 +451,11 @@
 	}
 	values := map[string]any{
 		"repoAddr":         "",
+		"repoPublicAddr":   "",
 		"repoHost":         "",
+		"branch":           "",
 		"gitRepoPublicKey": "",
+		"username":         "",
 	}
 	rendered, err := a.Render(release, env, networks, values, nil, nil)
 	if err != nil {
diff --git a/core/installer/cmd/rewrite.go b/core/installer/cmd/rewrite.go
index 0562c0e..3e2961b 100644
--- a/core/installer/cmd/rewrite.go
+++ b/core/installer/cmd/rewrite.go
@@ -75,7 +75,7 @@
 	if err != nil {
 		return err
 	}
-	all, err := mgr.FindAllInstances()
+	all, err := mgr.GetAllInstances()
 	if err != nil {
 		return err
 	}
diff --git a/core/installer/derived.go b/core/installer/derived.go
index aabda98..508cd3e 100644
--- a/core/installer/derived.go
+++ b/core/installer/derived.go
@@ -90,10 +90,17 @@
 				}
 			}
 			if def.Kind() == KindVPNAuthKey {
-				usernameField := def.Meta()["usernameField"]
-				// TODO(gio): Improve getField
-				username := getField(root, usernameField)
-				authKey, err := vpnKeyGen.Generate(username.(string))
+				var username string
+				if v, ok := def.Meta()["username"]; ok {
+					username = v
+				} else if v, ok := def.Meta()["usernameField"]; ok {
+					// TODO(gio): Improve getField
+					username, ok = getField(root, v).(string)
+					if !ok {
+						return nil, fmt.Errorf("could not resolve username: %+v %s %+v", def.Meta(), v, root)
+					}
+				}
+				authKey, err := vpnKeyGen.Generate(username)
 				if err != nil {
 					return nil, err
 				}
@@ -147,19 +154,19 @@
 			}
 			ret[k] = picked
 		case KindAuth:
-			r, err := deriveValues(v, v, AuthSchema, networks, vpnKeyGen)
+			r, err := deriveValues(root, v, AuthSchema, networks, vpnKeyGen)
 			if err != nil {
 				return nil, err
 			}
 			ret[k] = r
 		case KindSSHKey:
-			r, err := deriveValues(v, v, SSHKeySchema, networks, vpnKeyGen)
+			r, err := deriveValues(root, v, SSHKeySchema, networks, vpnKeyGen)
 			if err != nil {
 				return nil, err
 			}
 			ret[k] = r
 		case KindStruct:
-			r, err := deriveValues(v, v, def, networks, vpnKeyGen)
+			r, err := deriveValues(root, v, def, networks, vpnKeyGen)
 			if err != nil {
 				return nil, err
 			}
diff --git a/core/installer/helm.go b/core/installer/helm.go
index 8370b4e..7d3c334 100644
--- a/core/installer/helm.go
+++ b/core/installer/helm.go
@@ -42,6 +42,7 @@
 }
 
 type HelmFetcher interface {
+	// TODO(gio): implement integrity check
 	Pull(chart HelmChartGitRepo, rfs soft.RepoFS, root string) error
 }
 
@@ -99,6 +100,9 @@
 	if err != nil {
 		return err
 	}
+	if err := rfs.RemoveDir(root); err != nil {
+		return err
+	}
 	return util.Walk(wtFS, "/", func(path string, info fs.FileInfo, err error) error {
 		if info.IsDir() {
 			return nil
diff --git a/core/installer/schema.go b/core/installer/schema.go
index b02f3b7..04955b1 100644
--- a/core/installer/schema.go
+++ b/core/installer/schema.go
@@ -231,8 +231,17 @@
 	case cue.StringKind:
 		if role == "vpnauthkey" {
 			meta := map[string]string{}
-			usernameAttr := v.Attribute("usernameField")
-			meta["usernameField"] = strings.ToLower(usernameAttr.Contents())
+			usernameFieldAttr := v.Attribute("usernameField")
+			if usernameFieldAttr.Err() == nil {
+				meta["usernameField"] = strings.ToLower(usernameFieldAttr.Contents())
+			}
+			usernameAttr := v.Attribute("username")
+			if usernameAttr.Err() == nil {
+				meta["username"] = strings.ToLower(usernameAttr.Contents())
+			}
+			if len(meta) != 1 {
+				return nil, fmt.Errorf("invalid vpn auth key field meta: %+v", meta)
+			}
 			return basicSchema{name, KindVPNAuthKey, true, meta}, nil
 		} else {
 			return basicSchema{name, KindString, false, nil}, nil
diff --git a/core/installer/soft/client.go b/core/installer/soft/client.go
index dd5bbfe..5163f23 100644
--- a/core/installer/soft/client.go
+++ b/core/installer/soft/client.go
@@ -16,6 +16,7 @@
 	"github.com/cenkalti/backoff/v4"
 	"github.com/go-git/go-billy/v5/memfs"
 	"github.com/go-git/go-git/v5"
+	"github.com/go-git/go-git/v5/plumbing"
 	"github.com/go-git/go-git/v5/plumbing/transport"
 	gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
 	"github.com/go-git/go-git/v5/storage/memory"
@@ -29,6 +30,7 @@
 	GetPublicKeys() ([]string, error)
 	RepoExists(name string) (bool, error)
 	GetRepo(name string) (RepoIO, error)
+	GetRepoBranch(name, branch string) (RepoIO, error)
 	GetAllRepos() ([]string, error)
 	GetRepoAddress(name string) string
 	AddRepository(name string) error
@@ -44,6 +46,8 @@
 	AddReadWriteCollaborator(repo, user string) error
 	AddReadOnlyCollaborator(repo, user string) error
 	AddWebhook(repo, url string, opts ...string) error
+	DisableAnonAccess() error
+	DisableKeyless() error
 }
 
 type realClient struct {
@@ -265,13 +269,30 @@
 	return err
 }
 
+func (ss *realClient) DisableAnonAccess() error {
+	log.Printf("Disabling anon access")
+	_, err := ss.RunCommand("settings", "anon-access", "no-access")
+	return err
+}
+
+func (ss *realClient) DisableKeyless() error {
+	log.Printf("Disabling anon access")
+	_, err := ss.RunCommand("settings", "allow-keyless", "false")
+	return err
+}
+
 type Repository struct {
 	*git.Repository
 	Addr RepositoryAddress
+	Ref  string
 }
 
 func (ss *realClient) GetRepo(name string) (RepoIO, error) {
-	r, err := CloneRepository(RepositoryAddress{ss.addr, name}, ss.signer)
+	return ss.GetRepoBranch(name, "master")
+}
+
+func (ss *realClient) GetRepoBranch(name, branch string) (RepoIO, error) {
+	r, err := CloneRepositoryBranch(RepositoryAddress{ss.addr, name}, branch, ss.signer)
 	if err != nil {
 		return nil, err
 	}
@@ -305,7 +326,12 @@
 }
 
 func CloneRepository(addr RepositoryAddress, signer ssh.Signer) (*Repository, error) {
-	fmt.Printf("Cloning repository: %s %s\n", addr.Addr, addr.Name)
+	return CloneRepositoryBranch(addr, "master", signer)
+}
+
+func CloneRepositoryBranch(addr RepositoryAddress, branch string, signer ssh.Signer) (*Repository, error) {
+	fmt.Printf("Cloning repository: %s %s %s\n", addr.Addr, addr.Name, branch)
+	ref := fmt.Sprintf("refs/heads/%s", branch)
 	c, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
 		URL: addr.FullAddress(),
 		Auth: &gitssh.PublicKeys{
@@ -320,7 +346,7 @@
 			},
 		},
 		RemoteName:      "origin",
-		ReferenceName:   "refs/heads/master",
+		ReferenceName:   plumbing.ReferenceName(ref),
 		SingleBranch:    true,
 		Depth:           1,
 		InsecureSkipTLS: true,
@@ -348,6 +374,7 @@
 	return &Repository{
 		Repository: c,
 		Addr:       addr,
+		Ref:        ref,
 	}, nil
 }
 
diff --git a/core/installer/soft/repoio.go b/core/installer/soft/repoio.go
index 191b291..458f688 100644
--- a/core/installer/soft/repoio.go
+++ b/core/installer/soft/repoio.go
@@ -209,6 +209,7 @@
 	if len(st) == 0 {
 		return "", nil // TODO(gio): maybe return ErrorNothingToCommit
 	}
+	fmt.Printf("@@@ %+v\n", st)
 	hash, err := wt.Commit(message, &git.CommitOptions{
 		Author: &object.Signature{
 			Name: "pcloud-installer",
@@ -223,11 +224,12 @@
 		Auth:       auth(r.signer),
 	}
 	if o.ToBranch != "" {
-		gopts.RefSpecs = []config.RefSpec{config.RefSpec(fmt.Sprintf("refs/heads/master:refs/heads/%s", o.ToBranch))}
+		gopts.RefSpecs = []config.RefSpec{config.RefSpec(fmt.Sprintf("%s:refs/heads/%s", r.repo.Ref, o.ToBranch))}
 	}
 	if o.Force {
 		gopts.Force = true
 	}
+	fmt.Println(3333)
 	return hash.String(), r.repo.Push(gopts)
 }
 
@@ -259,6 +261,7 @@
 	if o.ToBranch != "" {
 		popts = append(popts, WithToBranch(o.ToBranch))
 	}
+	fmt.Println(2222)
 	return r.CommitAndPush(msg, popts...)
 }
 
diff --git a/core/installer/values-tmpl/appmanager.cue b/core/installer/values-tmpl/appmanager.cue
index aad8f52..24d0d8d 100644
--- a/core/installer/values-tmpl/appmanager.cue
+++ b/core/installer/values-tmpl/appmanager.cue
@@ -29,57 +29,59 @@
 _domain: "\(_subdomain).\(input.network.domain)"
 url: "https://\(_domain)"
 
-ingress: {
-	appmanager: {
-		auth: {
-			enabled: true
-			groups: input.authGroups
-		}
-		network: input.network
-		subdomain: _subdomain
-		service: {
-			name: "appmanager"
-			port: name: _httpPortName
-		}
-	}
-}
-
-images: {
-	appmanager: {
-		repository: "giolekva"
-		name: "pcloud-installer"
-		tag: "latest"
-		pullPolicy: "Always"
-	}
-}
-
-charts: {
-	appmanager: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/appmanager"
-	}
-}
-
-helm: {
-	appmanager: {
-		chart: charts.appmanager
-		values: {
-			repoAddr: input.repoAddr
-			sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
-			headscaleAPIAddr: "http://headscale-api.\(global.namespacePrefix)app-headscale.svc.cluster.local"
-			ingress: {
-				className: input.network.ingressClass
-				domain: _domain
-				certificateIssuer: ""
+out: {
+	ingress: {
+		appmanager: {
+			auth: {
+				enabled: true
+				groups: input.authGroups
 			}
-			clusterRoleName: "\(global.id)-appmanager"
-			portName: _httpPortName
-			image: {
-				repository: images.appmanager.fullName
-				tag: images.appmanager.tag
-				pullPolicy: images.appmanager.pullPolicy
+			network: input.network
+			subdomain: _subdomain
+			service: {
+				name: "appmanager"
+				port: name: _httpPortName
+			}
+		}
+	}
+
+	images: {
+		appmanager: {
+			repository: "giolekva"
+			name: "pcloud-installer"
+			tag: "latest"
+			pullPolicy: "Always"
+		}
+	}
+
+	charts: {
+		appmanager: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/appmanager"
+		}
+	}
+
+	helm: {
+		appmanager: {
+			chart: charts.appmanager
+			values: {
+				repoAddr: input.repoAddr
+				sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
+				headscaleAPIAddr: "http://headscale-api.\(global.namespacePrefix)app-headscale.svc.cluster.local"
+				ingress: {
+					className: input.network.ingressClass
+					domain: _domain
+					certificateIssuer: ""
+				}
+				clusterRoleName: "\(global.id)-appmanager"
+				portName: _httpPortName
+				image: {
+					repository: images.appmanager.fullName
+					tag: images.appmanager.tag
+					pullPolicy: images.appmanager.pullPolicy
+				}
 			}
 		}
 	}
diff --git a/core/installer/values-tmpl/cert-manager.cue b/core/installer/values-tmpl/cert-manager.cue
index 0a89102..66e21af 100644
--- a/core/installer/values-tmpl/cert-manager.cue
+++ b/core/installer/values-tmpl/cert-manager.cue
@@ -3,104 +3,106 @@
 name: "cert-manager"
 namespace: "cert-manager"
 
-images: {
-	certManager: {
-		registry: "quay.io"
-		repository: "jetstack"
-		name: "cert-manager-controller"
-		tag: "v1.12.2"
-		pullPolicy: "IfNotPresent"
+out: {
+	images: {
+		certManager: {
+			registry: "quay.io"
+			repository: "jetstack"
+			name: "cert-manager-controller"
+			tag: "v1.12.2"
+			pullPolicy: "IfNotPresent"
+		}
+		cainjector: {
+			registry: "quay.io"
+			repository: "jetstack"
+			name: "cert-manager-cainjector"
+			tag: "v1.12.2"
+			pullPolicy: "IfNotPresent"
+		}
+		webhook: {
+			registry: "quay.io"
+			repository: "jetstack"
+			name: "cert-manager-webhook"
+			tag: "v1.12.2"
+			pullPolicy: "IfNotPresent"
+		}
+		dnsChallengeSolver: {
+			repository: "giolekva"
+			name: "dns-challenge-solver"
+			tag: "latest"
+			pullPolicy: "Always"
+		}
 	}
-	cainjector: {
-		registry: "quay.io"
-		repository: "jetstack"
-		name: "cert-manager-cainjector"
-		tag: "v1.12.2"
-		pullPolicy: "IfNotPresent"
-	}
-	webhook: {
-		registry: "quay.io"
-		repository: "jetstack"
-		name: "cert-manager-webhook"
-		tag: "v1.12.2"
-		pullPolicy: "IfNotPresent"
-	}
-	dnsChallengeSolver: {
-		repository: "giolekva"
-		name: "dns-challenge-solver"
-		tag: "latest"
-		pullPolicy: "Always"
-	}
-}
 
-charts: {
-	certManager: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/cert-manager"
+	charts: {
+		certManager: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/cert-manager"
+		}
+		dnsChallengeSolver: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/cert-manager-webhook-pcloud"
+		}
 	}
-	dnsChallengeSolver: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/cert-manager-webhook-pcloud"
-	}
-}
 
-helm: {
-	"cert-manager": {
-		chart: charts.certManager
-		dependsOn: [{
-			name: "ingress-public"
-			namespace: "\(global.pcloudEnvName)-ingress-public"
-		}]
-		values: {
-			fullnameOverride: "\(global.pcloudEnvName)-cert-manager"
-			installCRDs: true
-			dns01RecursiveNameserversOnly: true
-			dns01RecursiveNameservers: "1.1.1.1:53,8.8.8.8:53"
-			image: {
-				repository: images.certManager.fullName
-				tag: images.certManager.tag
-				pullPolicy: images.certManager.pullPolicy
-			}
-			cainjector: {
+	helm: {
+		"cert-manager": {
+			chart: charts.certManager
+			dependsOn: [{
+				name: "ingress-public"
+				namespace: "\(global.pcloudEnvName)-ingress-public"
+			}]
+			values: {
+				fullnameOverride: "\(global.pcloudEnvName)-cert-manager"
+				installCRDs: true
+				dns01RecursiveNameserversOnly: true
+				dns01RecursiveNameservers: "1.1.1.1:53,8.8.8.8:53"
 				image: {
-					repository: images.cainjector.fullName
-					tag: images.cainjector.tag
-					pullPolicy: images.cainjector.pullPolicy
+					repository: images.certManager.fullName
+					tag: images.certManager.tag
+					pullPolicy: images.certManager.pullPolicy
 				}
-			}
-			webhook: {
-				image: {
-					repository: images.webhook.fullName
-					tag: images.webhook.tag
-					pullPolicy: images.webhook.pullPolicy
+				cainjector: {
+					image: {
+						repository: images.cainjector.fullName
+						tag: images.cainjector.tag
+						pullPolicy: images.cainjector.pullPolicy
+					}
+				}
+				webhook: {
+					image: {
+						repository: images.webhook.fullName
+						tag: images.webhook.tag
+						pullPolicy: images.webhook.pullPolicy
+					}
 				}
 			}
 		}
-	}
-	"cert-manager-webhook-pcloud": {
-		chart: charts.dnsChallengeSolver
-		dependsOn: [{
-			name: "cert-manager"
-			namespace: release.namespace
-		}]
-		values: {
-			fullnameOverride: "\(global.pcloudEnvName)-cert-manager-webhook-pcloud"
-			certManager: {
-				name: "\(global.pcloudEnvName)-cert-manager"
-				namespace: "\(global.pcloudEnvName)-cert-manager"
+		"cert-manager-webhook-pcloud": {
+			chart: charts.dnsChallengeSolver
+			dependsOn: [{
+				name: "cert-manager"
+				namespace: release.namespace
+			}]
+			values: {
+				fullnameOverride: "\(global.pcloudEnvName)-cert-manager-webhook-pcloud"
+				certManager: {
+					name: "\(global.pcloudEnvName)-cert-manager"
+					namespace: "\(global.pcloudEnvName)-cert-manager"
+				}
+				image: {
+					repository: images.dnsChallengeSolver.fullName
+					tag: images.dnsChallengeSolver.tag
+					pullPolicy: images.dnsChallengeSolver.pullPolicy
+				}
+				logLevel: 2
+				apiGroupName: "dodo.cloud"
+				resolverName: "dns-resolver-pcloud"
 			}
-			image: {
-				repository: images.dnsChallengeSolver.fullName
-				tag: images.dnsChallengeSolver.tag
-				pullPolicy: images.dnsChallengeSolver.pullPolicy
-			}
-			logLevel: 2
-			apiGroupName: "dodo.cloud"
-			resolverName: "dns-resolver-pcloud"
 		}
 	}
 }
diff --git a/core/installer/values-tmpl/certificate-issuer-custom.cue b/core/installer/values-tmpl/certificate-issuer-custom.cue
index 0b1bc6e..8721e7f 100644
--- a/core/installer/values-tmpl/certificate-issuer-custom.cue
+++ b/core/installer/values-tmpl/certificate-issuer-custom.cue
@@ -7,8 +7,6 @@
 	domain: string
 }
 
-images: {}
-
 name: "Network"
 namespace: "ingress-custom"
 readme: "Configure custom public domain"
@@ -36,29 +34,31 @@
   </g>
 </svg>"""
 
-charts: {
-	"certificate-issuer": {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/certificate-issuer-public"
+out: {
+	charts: {
+		"certificate-issuer": {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/certificate-issuer-public"
+		}
 	}
-}
 
-helm: {
-	"certificate-issuer": {
-		chart: charts["certificate-issuer"]
-		dependsOn: [{
-			name: "ingress-nginx"
-			namespace: "\(global.namespacePrefix)ingress-private"
-		}]
-		values: {
-			issuer: {
-				name: input.name
-				server: "https://acme-v02.api.letsencrypt.org/directory"
-				domain: input.domain
-				contactEmail: global.contactEmail
-				ingressClass: networks.public.ingressClass
+	helm: {
+		"certificate-issuer": {
+			chart: charts["certificate-issuer"]
+			dependsOn: [{
+				name: "ingress-nginx"
+				namespace: "\(global.namespacePrefix)ingress-private"
+			}]
+			values: {
+				issuer: {
+					name: input.name
+					server: "https://acme-v02.api.letsencrypt.org/directory"
+					domain: input.domain
+					contactEmail: global.contactEmail
+					ingressClass: networks.public.ingressClass
+				}
 			}
 		}
 	}
diff --git a/core/installer/values-tmpl/certificate-issuer-private.cue b/core/installer/values-tmpl/certificate-issuer-private.cue
index c67702b..66dc53c 100644
--- a/core/installer/values-tmpl/certificate-issuer-private.cue
+++ b/core/installer/values-tmpl/certificate-issuer-private.cue
@@ -3,34 +3,34 @@
 name: "certificate-issuer-private"
 namespace: "ingress-private"
 
-images: {}
-
-charts: {
-	"certificate-issuer-private": {
-		path: "charts/certificate-issuer-private"
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
+out: {
+	charts: {
+		"certificate-issuer-private": {
+			path: "charts/certificate-issuer-private"
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+		}
 	}
-}
 
-helm: {
-	"certificate-issuer-private": {
-		chart: charts["certificate-issuer-private"]
-		dependsOn: [{
-			name: "ingress-nginx"
-			namespace: "\(global.namespacePrefix)ingress-private"
-		}]
-		values: {
-			issuer: {
-				name: "\(global.id)-private"
-				server: "https://acme-v02.api.letsencrypt.org/directory"
-				domain: global.privateDomain
-				contactEmail: global.contactEmail
-			}
-			config: {
-				createTXTAddr: "http://dns-api.\(global.id)-dns.svc.cluster.local/create-txt-record"
-				deleteTXTAddr: "http://dns-api.\(global.id)-dns.svc.cluster.local/delete-txt-record"
+	helm: {
+		"certificate-issuer-private": {
+			chart: charts["certificate-issuer-private"]
+			dependsOn: [{
+				name: "ingress-nginx"
+				namespace: "\(global.namespacePrefix)ingress-private"
+			}]
+			values: {
+				issuer: {
+					name: "\(global.id)-private"
+					server: "https://acme-v02.api.letsencrypt.org/directory"
+					domain: global.privateDomain
+					contactEmail: global.contactEmail
+				}
+				config: {
+					createTXTAddr: "http://dns-api.\(global.id)-dns.svc.cluster.local/create-txt-record"
+					deleteTXTAddr: "http://dns-api.\(global.id)-dns.svc.cluster.local/delete-txt-record"
+				}
 			}
 		}
 	}
diff --git a/core/installer/values-tmpl/certificate-issuer-public.cue b/core/installer/values-tmpl/certificate-issuer-public.cue
index 15f2d11..04a6e50 100644
--- a/core/installer/values-tmpl/certificate-issuer-public.cue
+++ b/core/installer/values-tmpl/certificate-issuer-public.cue
@@ -2,30 +2,30 @@
 	network: #Network
 }
 
-images: {}
-
 name: "certificate-issuer-public"
 namespace: "ingress-private"
 
-charts: {
-	"certificate-issuer-public": {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/certificate-issuer-public"
+out: {
+	charts: {
+		"certificate-issuer-public": {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/certificate-issuer-public"
+		}
 	}
-}
 
-helm: {
-	"certificate-issuer-public": {
-		chart: charts["certificate-issuer-public"]
-		values: {
-			issuer: {
-				name: input.network.certificateIssuer
-				server: "https://acme-v02.api.letsencrypt.org/directory"
-				domain: input.network.domain
-				contactEmail: global.contactEmail
-				ingressClass: input.network.ingressClass
+	helm: {
+		"certificate-issuer-public": {
+			chart: charts["certificate-issuer-public"]
+			values: {
+				issuer: {
+					name: input.network.certificateIssuer
+					server: "https://acme-v02.api.letsencrypt.org/directory"
+					domain: input.network.domain
+					contactEmail: global.contactEmail
+					ingressClass: input.network.ingressClass
+				}
 			}
 		}
 	}
diff --git a/core/installer/values-tmpl/coder.cue b/core/installer/values-tmpl/coder.cue
index 7f98189..4365830 100644
--- a/core/installer/values-tmpl/coder.cue
+++ b/core/installer/values-tmpl/coder.cue
@@ -12,125 +12,127 @@
 description: readme
 icon: "<svg xmlns='http://www.w3.org/2000/svg' width='50px' height='50px' viewBox='0 0 16 16'><path fill='currentColor' fill-rule='evenodd' d='M11.573.275a1.203 1.203 0 0 0-.191.073c-.039.021-1.383 1.172-2.986 2.558C6.792 4.291 5.462 5.424 5.44 5.424c-.022 0-.664-.468-1.427-1.04c-.762-.571-1.428-1.057-1.48-1.078c-.15-.063-.468-.05-.613.024C1.754 3.416.189 4.975.094 5.15a.741.741 0 0 0 .04.766c.041.057.575.557 1.185 1.11c.611.553 1.107 1.015 1.102 1.026c-.004.012-.495.442-1.091.957c-.596.514-1.122.981-1.168 1.036a.746.746 0 0 0-.069.804c.096.175 1.659 1.734 1.827 1.821c.166.087.497.089.653.005c.059-.031.7-.502 1.424-1.046l1.318-.988l.109.1l2.73 2.473c1.846 1.671 2.666 2.396 2.772 2.453l.15.08h1.348l1.631-.814c1.5-.748 1.64-.823 1.748-.942c.213-.237.197.241.197-5.738c0-5.821.009-5.468-.151-5.699c-.058-.084-.41-.331-1.634-1.148c-.857-.572-1.613-1.065-1.68-1.095c-.1-.045-.187-.056-.482-.063c-.237-.005-.401.004-.48.027m1.699 2.305l1.233.82l.001 4.82l.001 4.82l-1.205.6l-1.204.6h-.569L8.66 11.644c-1.578-1.428-2.912-2.616-2.963-2.641c-.199-.094-.5-.101-.661-.014c-.034.018-.651.475-1.372 1.015c-.721.541-1.322.983-1.335.983c-.03 0-.477-.448-.461-.462c.673-.577 2.078-1.794 2.182-1.891c.086-.081.169-.192.21-.28c.057-.127.065-.174.054-.343c-.01-.158-.028-.223-.091-.324c-.053-.086-.454-.466-1.229-1.167l-1.15-1.04l.231-.233a1.83 1.83 0 0 1 .256-.234c.013 0 .644.465 1.4 1.033c1.496 1.123 1.537 1.148 1.81 1.116a.968.968 0 0 0 .253-.069c.062-.029.503-.39.979-.802L7.96 5.265a5929.2 5929.2 0 0 0 2.187-1.89a191.687 191.687 0 0 1 1.879-1.614c.008-.001.568.368 1.246.819M11.64 4.257a1.5 1.5 0 0 0-.16.051c-.059.021-1.079.738-2.267 1.593C6.867 7.59 6.92 7.547 6.851 7.854a.556.556 0 0 0 0 .292c.068.307.017.264 2.362 1.953c1.188.855 2.214 1.576 2.28 1.601c.347.133.743-.029.929-.38l.071-.133V4.813l-.071-.133a.76.76 0 0 0-.369-.356c-.127-.056-.324-.088-.413-.067m-.66 4.5l-.007.757l-1.046-.75A41.313 41.313 0 0 1 8.881 8c0-.007.471-.351 1.046-.764l1.046-.75l.007.757a95.51 95.51 0 0 1 0 1.514'/></svg>"
 
-ingress: {
-	coder: {
-		auth: enabled: false
-		network: input.network
-		subdomain: input.subdomain
-		service: {
+out: {
+	ingress: {
+		coder: {
+			auth: enabled: false
+			network: input.network
+			subdomain: input.subdomain
+			service: {
+				name: "coder"
+				port: name: "http"
+			}
+		}
+	}
+
+	images: {
+		postgres: {
+			repository: "library"
+			name: "postgres"
+			tag: "15.3"
+			pullPolicy: "IfNotPresent"
+		}
+		coder: {
+			registry: "ghcr.io"
+			repository: "coder"
 			name: "coder"
-			port: name: "http"
+			tag: "latest"
+			pullPolicy: "IfNotPresent"
 		}
 	}
-}
 
-images: {
-	postgres: {
-		repository: "library"
-		name: "postgres"
-		tag: "15.3"
-		pullPolicy: "IfNotPresent"
-	}
-	coder: {
-		registry: "ghcr.io"
-		repository: "coder"
-		name: "coder"
-		tag: "latest"
-		pullPolicy: "IfNotPresent"
-	}
-}
-
-charts: {
-	postgres: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/postgresql"
-	}
-	coder: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/coder"
-	}
-	oauth2Client: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/oauth2-client"
-	}
-}
-
-_oauth2ClientSecretName: "oauth2-credentials"
-
-helm: {
-	"oauth2-client": {
-		chart: charts.oauth2Client
-		values: {
-			name: "\(release.namespace)-coder"
-			secretName: _oauth2ClientSecretName
-			grantTypes: ["authorization_code"]
-			responseTypes: ["code"]
-			scope: "openid profile email"
-			redirectUris: ["\(url)/api/v2/users/oidc/callbackzot"]
-			hydraAdmin: "http://hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
+	charts: {
+		postgres: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/postgresql"
+		}
+		coder: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/coder"
+		}
+		oauth2Client: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/oauth2-client"
 		}
 	}
-	postgres: {
-		chart: charts.postgres
-		values: {
-			fullnameOverride: "postgres"
-			image: {
-				registry: images.postgres.registry
-				repository: images.postgres.imageName
-				tag: images.postgres.tag
-				pullPolicy: images.postgres.pullPolicy
-			}
-			auth: {
-				username: "coder"
-				password: "coder"
-				database: "coder"
+
+	_oauth2ClientSecretName: "oauth2-credentials"
+
+	helm: {
+		"oauth2-client": {
+			chart: charts.oauth2Client
+			values: {
+				name: "\(release.namespace)-coder"
+				secretName: _oauth2ClientSecretName
+				grantTypes: ["authorization_code"]
+				responseTypes: ["code"]
+				scope: "openid profile email"
+				redirectUris: ["\(url)/api/v2/users/oidc/callbackzot"]
+				hydraAdmin: "http://hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
 			}
 		}
-	}
-	coder: {
-		chart: charts.coder
-		values: coder: {
-			image: {
-				repo: images.coder.fullName
-				tag: images.coder.tag
-				pullPolicy: images.coder.pullPolicy
-			}
-			envUseClusterAccessURL: false
-			env: [{
-				name: "CODER_ACCESS_URL"
-				value: url
-			}, {
-				name: "CODER_PG_CONNECTION_URL"
-				value: "postgres://coder:coder@postgres.\(release.namespace).svc.cluster.local:5432/coder?sslmode=disable"
-			}, {
-				name: "CODER_OIDC_ISSUER_URL"
-				value: "https://hydra.\(networks.public.domain)"
-			}, {
-				name: "CODER_OIDC_EMAIL_DOMAIN"
-				value: networks.public.domain
-			}, {
-				name: "CODER_OIDC_CLIENT_ID"
-				valueFrom: {
-					secretKeyRef: {
-						name: _oauth2ClientSecretName
-						key: "client_id"
-					}
+		postgres: {
+			chart: charts.postgres
+			values: {
+				fullnameOverride: "postgres"
+				image: {
+					registry: images.postgres.registry
+					repository: images.postgres.imageName
+					tag: images.postgres.tag
+					pullPolicy: images.postgres.pullPolicy
 				}
-			}, {
-				name: "CODER_OIDC_CLIENT_SECRET"
-				valueFrom: {
-					secretKeyRef: {
-						name: _oauth2ClientSecretName
-						key: "client_secret"
-					}
+				auth: {
+					username: "coder"
+					password: "coder"
+					database: "coder"
 				}
-			}]
+			}
+		}
+		coder: {
+			chart: charts.coder
+			values: coder: {
+				image: {
+					repo: images.coder.fullName
+					tag: images.coder.tag
+					pullPolicy: images.coder.pullPolicy
+				}
+				envUseClusterAccessURL: false
+				env: [{
+					name: "CODER_ACCESS_URL"
+					value: url
+				}, {
+					name: "CODER_PG_CONNECTION_URL"
+					value: "postgres://coder:coder@postgres.\(release.namespace).svc.cluster.local:5432/coder?sslmode=disable"
+				}, {
+					name: "CODER_OIDC_ISSUER_URL"
+					value: "https://hydra.\(networks.public.domain)"
+				}, {
+					name: "CODER_OIDC_EMAIL_DOMAIN"
+					value: networks.public.domain
+				}, {
+					name: "CODER_OIDC_CLIENT_ID"
+					valueFrom: {
+						secretKeyRef: {
+							name: _oauth2ClientSecretName
+							key: "client_id"
+						}
+					}
+				}, {
+					name: "CODER_OIDC_CLIENT_SECRET"
+					valueFrom: {
+						secretKeyRef: {
+							name: _oauth2ClientSecretName
+							key: "client_secret"
+						}
+					}
+				}]
+			}
 		}
 	}
 }
diff --git a/core/installer/values-tmpl/config-repo.cue b/core/installer/values-tmpl/config-repo.cue
index f56da11..26ada75 100644
--- a/core/installer/values-tmpl/config-repo.cue
+++ b/core/installer/values-tmpl/config-repo.cue
@@ -7,41 +7,40 @@
 name: "config-repo"
 namespace: "config-repo"
 
-images: {
-	softserve: {
-		repository: "charmcli"
-		name: "soft-serve"
-		tag: "v0.7.1"
-		pullPolicy: "IfNotPresent"
+out: {
+	images: {
+		softserve: {
+			repository: "charmcli"
+			name: "soft-serve"
+			tag: "v0.7.1"
+			pullPolicy: "IfNotPresent"
+		}
 	}
-}
 
-charts: {
-	softserve: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/soft-serve"
+	charts: {
+		softserve: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/soft-serve"
+		}
 	}
-}
 
-helm: {
-	softserve: {
-		chart: charts.softserve
-		values: {
-			serviceType: "ClusterIP"
-			addressPool: ""
-			reservedIP: ""
-			adminKey: input.adminKey
-			privateKey: input.privateKey
-			publicKey: input.publicKey
-			ingress: {
-				enabled: false
-			}
-			image: {
-				repository: images.softserve.fullName
-				tag: images.softserve.tag
-				pullPolicy: images.softserve.pullPolicy
+	helm: {
+		softserve: {
+			chart: charts.softserve
+			values: {
+				serviceType: "ClusterIP"
+				addressPool: ""
+				reservedIP: ""
+				adminKey: input.adminKey
+				privateKey: input.privateKey
+				publicKey: input.publicKey
+				image: {
+					repository: images.softserve.fullName
+					tag: images.softserve.tag
+					pullPolicy: images.softserve.pullPolicy
+				}
 			}
 		}
 	}
diff --git a/core/installer/values-tmpl/core-auth.cue b/core/installer/values-tmpl/core-auth.cue
index 079da27..d535c97 100644
--- a/core/installer/values-tmpl/core-auth.cue
+++ b/core/installer/values-tmpl/core-auth.cue
@@ -36,344 +36,353 @@
 }
 """###
 
-images: {
-	kratos: {
-		repository: "oryd"
-		name: "kratos"
-		tag: "v1.1.0-distroless"
-		pullPolicy: "IfNotPresent"
+out: {
+	images: {
+		kratos: {
+			repository: "oryd"
+			name: "kratos"
+			tag: "v1.1.0-distroless"
+			pullPolicy: "IfNotPresent"
+		}
+		hydra: {
+			repository: "oryd"
+			name: "hydra"
+			tag: "v2.2.0-distroless"
+			pullPolicy: "IfNotPresent"
+		}
+		ui: {
+			repository: "giolekva"
+			name: "auth-ui"
+			tag: "latest"
+			pullPolicy: "Always"
+		}
+		postgres: {
+			repository: "library"
+			name: "postgres"
+			tag: "15.3"
+			pullPolicy: "IfNotPresent"
+		}
 	}
-	hydra: {
-		repository: "oryd"
-		name: "hydra"
-		tag: "v2.2.0-distroless"
-		pullPolicy: "IfNotPresent"
-	}
-	ui: {
-		repository: "giolekva"
-		name: "auth-ui"
-		tag: "latest"
-		pullPolicy: "Always"
-	}
-	postgres: {
-		repository: "library"
-		name: "postgres"
-		tag: "15.3"
-		pullPolicy: "IfNotPresent"
-	}
-}
 
-charts: {
-	auth: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/auth"
+	charts: {
+		auth: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/auth"
+		}
+		postgres: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/postgresql"
+		}
 	}
-	postgres: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/postgresql"
-	}
-}
 
-helm: {
-	postgres: {
-		chart: charts.postgres
-		values: {
-			fullnameOverride: "postgres"
-			image: {
-				registry: images.postgres.registry
-				repository: images.postgres.imageName
-				tag: images.postgres.tag
-				pullPolicy: images.postgres.pullPolicy
-			}
-			service: {
-				type: "ClusterIP"
-				port: 5432
-			}
-			primary: {
-				initdb: {
-					scripts: {
-						"init.sql": """
-						CREATE USER kratos WITH PASSWORD 'kratos';
-						CREATE USER hydra WITH PASSWORD 'hydra';
-						CREATE DATABASE kratos WITH OWNER = kratos;
-						CREATE DATABASE hydra WITH OWNER = hydra;
-						"""
+	helm: {
+		postgres: {
+			chart: charts.postgres
+			values: {
+				fullnameOverride: "postgres"
+				image: {
+					registry: images.postgres.registry
+					repository: images.postgres.imageName
+					tag: images.postgres.tag
+					pullPolicy: images.postgres.pullPolicy
+				}
+				service: {
+					type: "ClusterIP"
+					port: 5432
+				}
+				primary: {
+					initdb: {
+						scripts: {
+							"init.sql": """
+							CREATE USER kratos WITH PASSWORD 'kratos';
+							CREATE USER hydra WITH PASSWORD 'hydra';
+							CREATE DATABASE kratos WITH OWNER = kratos;
+							CREATE DATABASE hydra WITH OWNER = hydra;
+							"""
+						}
+					}
+					persistence: {
+						size: "1Gi"
+					}
+					securityContext: {
+						enabled: true
+						fsGroup: 0
+					}
+					containerSecurityContext: {
+						enabled: true
+						runAsUser: 0
 					}
 				}
-				persistence: {
-					size: "1Gi"
-				}
-				securityContext: {
-					enabled: true
-					fsGroup: 0
-				}
-				containerSecurityContext: {
-					enabled: true
-					runAsUser: 0
-				}
-			}
-			volumePermissions: {
-				securityContext: {
-					runAsUser: 0
+				volumePermissions: {
+					securityContext: {
+						runAsUser: 0
+					}
 				}
 			}
 		}
-	}
-	auth: {
-		chart: charts.auth
-		dependsOn: [{
-			name: "postgres"
-			namespace: release.namespace
-		}]
-		values: {
-			kratos: {
-				fullnameOverride: "kratos"
-				image: {
-					repository: images.kratos.fullName
-					tag: images.kratos.tag
-					pullPolicy: images.kratos.pullPolicy
-				}
-				service: {
-					admin: {
-						enabled: true
-						type: "ClusterIP"
-						port: 80
-						name: "http"
-					}
-					public: {
-						enabled: true
-						type: "ClusterIP"
-						port: 80
-						name: "http"
-					}
-				}
-				ingress: {
-					admin: enabled: false
-					public: {
-						enabled: true
-						className: input.network.ingressClass
-						annotations: {
-							"acme.cert-manager.io/http01-edit-in-place": "true"
-							"cert-manager.io/cluster-issuer": input.network.certificateIssuer
-						}
-						hosts: [{
-							host: "accounts.\(input.network.domain)"
-							paths: [{
-								path: "/"
-								pathType: "Prefix"
-							}]
-						}]
-						tls: [{
-							hosts: ["accounts.\(input.network.domain)"]
-							secretName: "cert-accounts.\(input.network.domain)"
-						}]
-					}
-				}
-				secret: {
-					enabled: true
-				}
+		auth: {
+			chart: charts.auth
+			dependsOn: [{
+				name: "postgres"
+				namespace: release.namespace
+			}]
+			values: {
 				kratos: {
-					automigration: {
+					fullnameOverride: "kratos"
+					image: {
+						repository: images.kratos.fullName
+						tag: images.kratos.tag
+						pullPolicy: images.kratos.pullPolicy
+					}
+					service: {
+						admin: {
+							enabled: true
+							type: "ClusterIP"
+							port: 80
+							name: "http"
+						}
+						public: {
+							enabled: true
+							type: "ClusterIP"
+							port: 80
+							name: "http"
+						}
+					}
+					ingress: {
+						admin: enabled: false
+						public: {
+							enabled: true
+							className: input.network.ingressClass
+							annotations: {
+								"acme.cert-manager.io/http01-edit-in-place": "true"
+								"cert-manager.io/cluster-issuer": input.network.certificateIssuer
+							}
+							hosts: [{
+								host: "accounts.\(input.network.domain)"
+								paths: [{
+									path: "/"
+									pathType: "Prefix"
+								}]
+							}]
+							tls: [{
+								hosts: ["accounts.\(input.network.domain)"]
+								secretName: "cert-accounts.\(input.network.domain)"
+							}]
+						}
+					}
+					secret: {
 						enabled: true
 					}
-					development: false
-					courier: {
-						enabled: false
-					}
-					config: {
-						version: "v0.7.1-alpha.1"
-						dsn: "postgres://kratos:kratos@postgres.\(global.namespacePrefix)core-auth.svc:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4"
-						serve: {
-							public: {
-								base_url: "https://accounts.\(input.network.domain)"
-								cors: {
-									enabled: true
-									debug: false
-									allow_credentials: true
-									allowed_origins: [
-										"https://\(input.network.domain)",
-										"https://*.\(input.network.domain)",
-								]
-								}
-							}
-							admin: {
-								base_url: "https://kratos-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
-							}
+					kratos: {
+						automigration: {
+							enabled: true
 						}
-						selfservice: {
-							default_browser_return_url: "https://accounts-ui.\(input.network.domain)"
-							allowed_return_urls: [
-								"https://*.\(input.network.domain)/",
-								// TODO(gio): replace with input.network.privateSubdomain
-								"https://*.\(global.privateDomain)",
-						    ]
-							methods: {
-								password: {
-									enabled: true
-								}
-							}
-							flows: {
-								error: {
-									ui_url: "https://accounts-ui.\(input.network.domain)/error"
-								}
-								settings: {
-									ui_url: "https://accounts-ui.\(input.network.domain)/settings"
-									privileged_session_max_age: "15m"
-								}
-								recovery: {
-									enabled: false
-								}
-								verification: {
-									enabled: false
-								}
-								logout: {
-									after: {
-										default_browser_return_url: "https://accounts-ui.\(input.network.domain)/login"
-									}
-								}
-								login: {
-									ui_url: "https://accounts-ui.\(input.network.domain)/login"
-									lifespan: "10m"
-									after: {
-										password: {
-											default_browser_return_url: "https://accounts-ui.\(input.network.domain)/"
-										}
-									}
-								}
-								registration: {
-									lifespan: "10m"
-									ui_url: "https://accounts-ui.\(input.network.domain)/register"
-									after: {
-										password: {
-											hooks: [{
-												hook: "session"
-											}]
-											default_browser_return_url: "https://accounts-ui.\(input.network.domain)/"
-										}
-									}
-								}
-							}
-						}
-						log: {
-							level: "debug"
-							format: "text"
-							leak_sensitive_values: true
-						}
-						cookies: {
-							path: "/"
-							same_site: "None"
-							domain: input.network.domain
-						}
-						secrets: {
-							cookie: ["PLEASE-CHANGE-ME-I-AM-VERY-INSECURE"]
-						}
-						hashers: {
-							argon2: {
-								parallelism: 1
-								memory: "128MB"
-								iterations: 2
-								salt_length: 16
-								key_length: 16
-								}
-						}
-						identity: {
-							schemas: [{
-								id: "user"
-								url: "file:///etc/config/identity.schema.json"
-							}]
-							default_schema_id: "user"
-						}
+						development: false
 						courier: {
-							smtp: {
-								connection_uri: "smtps://test-z1VmkYfYPjgdPRgPFgmeZ31esT9rUgS%40\(input.network.domain):iW%213Kk%5EPPLFrZa%24%21bbpTPN9Wv3b8mvwS6ZJvMLtce%23A2%2A4MotD@mx1.\(input.network.domain)"
+							enabled: false
+						}
+						config: {
+							version: "v0.7.1-alpha.1"
+							dsn: "postgres://kratos:kratos@postgres.\(global.namespacePrefix)core-auth.svc:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4"
+							serve: {
+								public: {
+									base_url: "https://accounts.\(input.network.domain)"
+									cors: {
+										enabled: true
+										debug: false
+										allow_credentials: true
+										allowed_origins: [
+											"https://\(input.network.domain)",
+											"https://*.\(input.network.domain)",
+									]
+									}
+								}
+								admin: {
+									base_url: "https://kratos-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
+								}
+							}
+							selfservice: {
+								default_browser_return_url: "https://accounts-ui.\(input.network.domain)"
+								allowed_return_urls: [
+									"https://*.\(input.network.domain)/",
+									// TODO(gio): replace with input.network.privateSubdomain
+									"https://*.\(global.privateDomain)",
+								]
+								methods: {
+									password: {
+										enabled: true
+									}
+								}
+								flows: {
+									error: {
+										ui_url: "https://accounts-ui.\(input.network.domain)/error"
+									}
+									settings: {
+										ui_url: "https://accounts-ui.\(input.network.domain)/settings"
+										privileged_session_max_age: "15m"
+									}
+									recovery: {
+										enabled: false
+									}
+									verification: {
+										enabled: false
+									}
+									logout: {
+										after: {
+											default_browser_return_url: "https://accounts-ui.\(input.network.domain)/login"
+										}
+									}
+									login: {
+										ui_url: "https://accounts-ui.\(input.network.domain)/login"
+										lifespan: "10m"
+										after: {
+											password: {
+												default_browser_return_url: "https://accounts-ui.\(input.network.domain)/"
+											}
+										}
+									}
+									registration: {
+										lifespan: "10m"
+										ui_url: "https://accounts-ui.\(input.network.domain)/register"
+										after: {
+											password: {
+												hooks: [{
+													hook: "session"
+												}]
+												default_browser_return_url: "https://accounts-ui.\(input.network.domain)/"
+											}
+										}
+									}
+								}
+							}
+							log: {
+								level: "debug"
+								format: "text"
+								leak_sensitive_values: true
+							}
+							cookies: {
+								path: "/"
+								same_site: "None"
+								domain: input.network.domain
+							}
+							secrets: {
+								cookie: ["PLEASE-CHANGE-ME-I-AM-VERY-INSECURE"]
+							}
+							hashers: {
+								argon2: {
+									parallelism: 1
+									memory: "128MB"
+									iterations: 2
+									salt_length: 16
+									key_length: 16
+									}
+							}
+							identity: {
+								schemas: [{
+									id: "user"
+									url: "file:///etc/config/identity.schema.json"
+								}]
+								default_schema_id: "user"
+							}
+							courier: {
+								smtp: {
+									connection_uri: "smtps://test-z1VmkYfYPjgdPRgPFgmeZ31esT9rUgS%40\(input.network.domain):iW%213Kk%5EPPLFrZa%24%21bbpTPN9Wv3b8mvwS6ZJvMLtce%23A2%2A4MotD@mx1.\(input.network.domain)"
+								}
 							}
 						}
-					}
-					identitySchemas: {
-                        "identity.schema.json": _userSchema
-					}
-				}
-			}
-			hydra: {
-				fullnameOverride: "hydra"
-				image: {
-					repository: images.hydra.fullName
-					tag: images.hydra.tag
-					pullPolicy: images.hydra.pullPolicy
-				}
-				service: {
-					admin: {
-						enabled: true
-						type: "ClusterIP"
-						port: 80
-						name: "http"
-					}
-					public: {
-						enabled: true
-						type: "ClusterIP"
-						port: 80
-						name: "http"
-					}
-				}
-				ingress: {
-					admin: enabled: false
-					public: {
-						enabled: true
-						className: input.network.ingressClass
-						annotations: {
-							"acme.cert-manager.io/http01-edit-in-place": "true"
-							"cert-manager.io/cluster-issuer": input.network.certificateIssuer
+						identitySchemas: {
+							"identity.schema.json": _userSchema
 						}
-						hosts: [{
-							host: "hydra.\(input.network.domain)"
-							paths: [{
-								path: "/"
-								pathType: "Prefix"
-							}]
-						}]
-						tls: [{
-							hosts: ["hydra.\(input.network.domain)"]
-							secretName: "cert-hydra.\(input.network.domain)"
-						}]
 					}
 				}
-				secret: {
-					enabled: true
-				}
-				maester: {
-					enabled: false
-				}
 				hydra: {
-					automigration: {
+					fullnameOverride: "hydra"
+					image: {
+						repository: images.hydra.fullName
+						tag: images.hydra.tag
+						pullPolicy: images.hydra.pullPolicy
+					}
+					service: {
+						admin: {
+							enabled: true
+							type: "ClusterIP"
+							port: 80
+							name: "http"
+						}
+						public: {
+							enabled: true
+							type: "ClusterIP"
+							port: 80
+							name: "http"
+						}
+					}
+					ingress: {
+						admin: enabled: false
+						public: {
+							enabled: true
+							className: input.network.ingressClass
+							annotations: {
+								"acme.cert-manager.io/http01-edit-in-place": "true"
+								"cert-manager.io/cluster-issuer": input.network.certificateIssuer
+							}
+							hosts: [{
+								host: "hydra.\(input.network.domain)"
+								paths: [{
+									path: "/"
+									pathType: "Prefix"
+								}]
+							}]
+							tls: [{
+								hosts: ["hydra.\(input.network.domain)"]
+								secretName: "cert-hydra.\(input.network.domain)"
+							}]
+						}
+					}
+					secret: {
 						enabled: true
 					}
-					config: {
-						version: "v1.10.6"
-						dsn: "postgres://hydra:hydra@postgres.\(global.namespacePrefix)core-auth.svc:5432/hydra?sslmode=disable&max_conns=20&max_idle_conns=4"
-						serve: {
-							cookies: {
-								same_site_mode: "None"
-							}
-							public: {
-								cors: {
-									enabled: true
-									debug: false
-									allow_credentials: true
-									allowed_origins: [
-										"https://\(input.network.domain)",
-										"https://*.\(input.network.domain)"
-								]
+					maester: {
+						enabled: false
+					}
+					hydra: {
+						automigration: {
+							enabled: true
+						}
+						config: {
+							version: "v1.10.6"
+							dsn: "postgres://hydra:hydra@postgres.\(global.namespacePrefix)core-auth.svc:5432/hydra?sslmode=disable&max_conns=20&max_idle_conns=4"
+							serve: {
+								cookies: {
+									same_site_mode: "None"
 								}
-							}
-							admin: {
-								cors: {
-									allowed_origins: [
-										"https://hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
-								]
+								public: {
+									cors: {
+										enabled: true
+										debug: false
+										allow_credentials: true
+										allowed_origins: [
+											"https://\(input.network.domain)",
+											"https://*.\(input.network.domain)"
+									]
+									}
+								}
+								admin: {
+									cors: {
+										allowed_origins: [
+											"https://hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
+									]
+									}
+									tls: {
+										allow_termination_from: [
+											"0.0.0.0/0",
+											"10.42.0.0/16",
+											"10.43.0.0/16",
+									]
+									}
 								}
 								tls: {
 									allow_termination_from: [
@@ -383,54 +392,47 @@
 								]
 								}
 							}
-							tls: {
-								allow_termination_from: [
-									"0.0.0.0/0",
-									"10.42.0.0/16",
-									"10.43.0.0/16",
-							]
+							urls: {
+								self: {
+									public: "https://hydra.\(input.network.domain)"
+									issuer: "https://hydra.\(input.network.domain)"
+								}
+								consent: "https://accounts-ui.\(input.network.domain)/consent"
+								login: "https://accounts-ui.\(input.network.domain)/login"
+								logout: "https://accounts-ui.\(input.network.domain)/logout"
 							}
-						}
-						urls: {
-							self: {
-								public: "https://hydra.\(input.network.domain)"
-								issuer: "https://hydra.\(input.network.domain)"
+							secrets: {
+								system: ["youReallyNeedToChangeThis"]
 							}
-							consent: "https://accounts-ui.\(input.network.domain)/consent"
-							login: "https://accounts-ui.\(input.network.domain)/login"
-							logout: "https://accounts-ui.\(input.network.domain)/logout"
-						}
-						secrets: {
-							system: ["youReallyNeedToChangeThis"]
-						}
-						oidc: {
-							subject_identifiers: {
-								supported_types: [
-									"pairwise",
-									"public",
-							]
-								pairwise: {
-									salt: "youReallyNeedToChangeThis"
+							oidc: {
+								subject_identifiers: {
+									supported_types: [
+										"pairwise",
+										"public",
+								]
+									pairwise: {
+										salt: "youReallyNeedToChangeThis"
+									}
 								}
 							}
-						}
-						log: {
-							level: "trace"
-							leak_sensitive_values: false
+							log: {
+								level: "trace"
+								leak_sensitive_values: false
+							}
 						}
 					}
 				}
-			}
-			ui: {
-				certificateIssuer: input.network.certificateIssuer
-				ingressClassName: input.network.ingressClass
-				domain: input.network.domain
-				hydra: "hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
-				enableRegistration: false
-				image: {
-					repository: images.ui.fullName
-					tag: images.ui.tag
-					pullPolicy: images.ui.pullPolicy
+				ui: {
+					certificateIssuer: input.network.certificateIssuer
+					ingressClassName: input.network.ingressClass
+					domain: input.network.domain
+					hydra: "hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
+					enableRegistration: false
+					image: {
+						repository: images.ui.fullName
+						tag: images.ui.tag
+						pullPolicy: images.ui.pullPolicy
+					}
 				}
 			}
 		}
diff --git a/core/installer/values-tmpl/csi-driver-smb.cue b/core/installer/values-tmpl/csi-driver-smb.cue
index d765f7d..4627b3b 100644
--- a/core/installer/values-tmpl/csi-driver-smb.cue
+++ b/core/installer/values-tmpl/csi-driver-smb.cue
@@ -9,58 +9,60 @@
 	pullPolicy: "IfNotPresent"
 }
 
-images: {
-	smb: _baseImage & {
-		name: "smbplugin"
-		tag: "v1.11.0"
+out: {
+	images: {
+		smb: _baseImage & {
+			name: "smbplugin"
+			tag: "v1.11.0"
+		}
+		csiProvisioner: _baseImage & {
+			name: "csi-provisioner"
+			tag: "v3.5.0"
+		}
+		livenessProbe: _baseImage & {
+			name: "livenessprobe"
+			tag: "v2.10.0"
+		}
+		nodeDriverRegistrar: _baseImage & {
+			name: "csi-node-driver-registrar"
+			tag: "v2.8.0"
+		}
 	}
-	csiProvisioner: _baseImage & {
-		name: "csi-provisioner"
-		tag: "v3.5.0"
-	}
-	livenessProbe: _baseImage & {
-		name: "livenessprobe"
-		tag: "v2.10.0"
-	}
-	nodeDriverRegistrar: _baseImage & {
-		name: "csi-node-driver-registrar"
-		tag: "v2.8.0"
-	}
-}
 
-charts: {
-	csiDriverSMB: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/csi-driver-smb"
+	charts: {
+		csiDriverSMB: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/csi-driver-smb"
+		}
 	}
-}
 
-helm: {
-	"csi-driver-smb": {
-		chart: charts.csiDriverSMB
-		values: {
-			image: {
-				smb: {
-					repository: images.smb.fullName
-					tag: images.smb.tag
-					pullPolicy: images.smb.pullPolicy
-				}
-				csiProvisioner: {
-					repository: images.csiProvisioner.fullName
-					tag: images.csiProvisioner.tag
-					pullPolicy: images.csiProvisioner.pullPolicy
-				}
-				livenessProbe: {
-					repository: images.livenessProbe.fullName
-					tag: images.livenessProbe.tag
-					pullPolicy: images.livenessProbe.pullPolicy
-				}
-				nodeDriverRegistrar: {
-					repository: images.nodeDriverRegistrar.fullName
-					tag: images.nodeDriverRegistrar.tag
-					pullPolicy: images.nodeDriverRegistrar.pullPolicy
+	helm: {
+		"csi-driver-smb": {
+			chart: charts.csiDriverSMB
+			values: {
+				image: {
+					smb: {
+						repository: images.smb.fullName
+						tag: images.smb.tag
+						pullPolicy: images.smb.pullPolicy
+					}
+					csiProvisioner: {
+						repository: images.csiProvisioner.fullName
+						tag: images.csiProvisioner.tag
+						pullPolicy: images.csiProvisioner.pullPolicy
+					}
+					livenessProbe: {
+						repository: images.livenessProbe.fullName
+						tag: images.livenessProbe.tag
+						pullPolicy: images.livenessProbe.pullPolicy
+					}
+					nodeDriverRegistrar: {
+						repository: images.nodeDriverRegistrar.fullName
+						tag: images.nodeDriverRegistrar.tag
+						pullPolicy: images.nodeDriverRegistrar.pullPolicy
+					}
 				}
 			}
 		}
diff --git a/core/installer/values-tmpl/dns-gateway.cue b/core/installer/values-tmpl/dns-gateway.cue
index 59d2c37..bca02df 100644
--- a/core/installer/values-tmpl/dns-gateway.cue
+++ b/core/installer/values-tmpl/dns-gateway.cue
@@ -10,109 +10,111 @@
 name: "dns-gateway"
 namespace: "dns-gateway"
 
-images: {
-	coredns: {
-		repository: "coredns"
-		name: "coredns"
-		tag: "1.11.1"
-		pullPolicy: "IfNotPresent"
+out: {
+	images: {
+		coredns: {
+			repository: "coredns"
+			name: "coredns"
+			tag: "1.11.1"
+			pullPolicy: "IfNotPresent"
+		}
 	}
-}
 
-charts: {
-	coredns: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/coredns"
+	charts: {
+		coredns: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/coredns"
+		}
 	}
-}
 
-helm: {
-	coredns: {
-		chart: charts.coredns
-		values: {
-			image: {
-				repository: images.coredns.fullName
-				tag: images.coredns.tag
-				pullPolicy: images.coredns.pullPolicy
-			}
-			replicaCount: 1
-			resources: {
-				limits: {
-					cpu: "100m"
-					memory: "128Mi"
+	helm: {
+		coredns: {
+			chart: charts.coredns
+			values: {
+				image: {
+					repository: images.coredns.fullName
+					tag: images.coredns.tag
+					pullPolicy: images.coredns.pullPolicy
 				}
-				requests: {
-					cpu: "100m"
-					memory: "128Mi"
+				replicaCount: 1
+				resources: {
+					limits: {
+						cpu: "100m"
+						memory: "128Mi"
+					}
+					requests: {
+						cpu: "100m"
+						memory: "128Mi"
+					}
 				}
-			}
-			rollingUpdate: {
-				maxUnavailable: 1
-				maxSurge: "25%"
-			}
-			terminationGracePeriodSeconds: 30
-			serviceType: "ClusterIP"
-			service: name: "coredns"
-			serviceAccount: create: false
-			rbac: {
-				create: false
-				pspEnable: false
-			}
-			isClusterService: false
-			if len(input.servers) > 0 {
-				servers: [
-					for s in input.servers {
+				rollingUpdate: {
+					maxUnavailable: 1
+					maxSurge: "25%"
+				}
+				terminationGracePeriodSeconds: 30
+				serviceType: "ClusterIP"
+				service: name: "coredns"
+				serviceAccount: create: false
+				rbac: {
+					create: false
+					pspEnable: false
+				}
+				isClusterService: false
+				if len(input.servers) > 0 {
+					servers: [
+						for s in input.servers {
+							zones: [{
+								zone: s.zone
+							}]
+							port: 53
+							plugins: [{
+								name: "log"
+							}, {
+								name: "forward"
+								parameters: ". \(s.address)"
+							}, {
+								name: "health"
+								configBlock: "lameduck 5s"
+							}, {
+								name: "ready"
+							}]
+						}
+					]
+				}
+				if len(input.servers) == 0 {
+					servers: [{
 						zones: [{
-							zone: s.zone
+							zone: "."
 						}]
 						port: 53
 						plugins: [{
-							name: "log"
-						}, {
-							name: "forward"
-							parameters: ". \(s.address)"
-						}, {
-							name: "health"
-							configBlock: "lameduck 5s"
-						}, {
 							name: "ready"
 						}]
-					}
-			    ]
-			}
-			if len(input.servers) == 0 {
-				servers: [{
-					zones: [{
-						zone: "."
 					}]
-					port: 53
-					plugins: [{
-						name: "ready"
-					}]
-				}]
+				}
+				livenessProbe: {
+					enabled: true
+					initialDelaySeconds: 60
+					periodSeconds: 10
+					timeoutSeconds: 5
+					failureThreshold: 5
+					successThreshold: 1
+				}
+				readinessProbe: {
+					enabled: true
+					initialDelaySeconds: 30
+					periodSeconds: 10
+					timeoutSeconds: 5
+					failureThreshold: 5
+					successThreshold: 1
+				}
+				zoneFiles: []
+				hpa: enabled: false
+				autoscaler: enabled: false
+				deployment: enabled: true
 			}
-			livenessProbe: {
-				enabled: true
-				initialDelaySeconds: 60
-				periodSeconds: 10
-				timeoutSeconds: 5
-				failureThreshold: 5
-				successThreshold: 1
-			}
-			readinessProbe: {
-				enabled: true
-				initialDelaySeconds: 30
-				periodSeconds: 10
-				timeoutSeconds: 5
-				failureThreshold: 5
-				successThreshold: 1
-			}
-			zoneFiles: []
-			hpa: enabled: false
-			autoscaler: enabled: false
-			deployment: enabled: true
 		}
 	}
 }
diff --git a/core/installer/values-tmpl/dodo-app-instance-status.cue b/core/installer/values-tmpl/dodo-app-instance-status.cue
index ad66153..b72a272 100644
--- a/core/installer/values-tmpl/dodo-app-instance-status.cue
+++ b/core/installer/values-tmpl/dodo-app-instance-status.cue
@@ -8,15 +8,17 @@
 
 _subdomain: "status.\(input.appSubdomain)"
 
-ingress: {
-	"status-\(input.appName)": {
-		auth: enabled: false
-		network: input.network
-		subdomain: _subdomain
-		appRoot: "/\(input.appName)"
-		service: {
-			name: "web"
-			port: name: "http"
+out: {
+	ingress: {
+		"status-\(input.appName)": {
+			auth: enabled: false
+			network: input.network
+			subdomain: _subdomain
+			appRoot: "/\(input.appName)"
+			service: {
+				name: "web"
+				port: name: "http"
+			}
 		}
 	}
 }
diff --git a/core/installer/values-tmpl/dodo-app-instance.cue b/core/installer/values-tmpl/dodo-app-instance.cue
index e0d6906..ad20ac7 100644
--- a/core/installer/values-tmpl/dodo-app-instance.cue
+++ b/core/installer/values-tmpl/dodo-app-instance.cue
@@ -5,6 +5,7 @@
 input: {
 	repoAddr: string
 	repoHost: string
+	branch: string
 	gitRepoPublicKey: string
 	// TODO(gio): auto generate
 	fluxKeys: #SSHKey
@@ -58,7 +59,7 @@
 		}
 		spec: {
 			interval: "1m0s"
-			ref: branch: "dodo"
+			ref: branch: input.branch
 			secretRef: name: "app"
 			timeout: "60s"
 			url: input.repoAddr
diff --git a/core/installer/values-tmpl/dodo-app.cue b/core/installer/values-tmpl/dodo-app.cue
index 07c2f1a..8bb57c0 100644
--- a/core/installer/values-tmpl/dodo-app.cue
+++ b/core/installer/values-tmpl/dodo-app.cue
@@ -52,57 +52,6 @@
 _domain: "\(input.subdomain).\(input.network.domain)"
 url: "https://\(_domain)"
 
-images: {
-	softserve: {
-		repository: "charmcli"
-		name: "soft-serve"
-		tag: "v0.7.1"
-		pullPolicy: "IfNotPresent"
-	}
-	dodoApp: {
-		repository: "giolekva"
-		name: "pcloud-installer"
-		tag: "latest"
-		pullPolicy: "Always"
-	}
-}
-
-charts: {
-	softserve: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/soft-serve"
-	}
-	dodoApp: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/dodo-app"
-	}
-}
-
-volumes: db: size: "10Gi"
-
-ingress: {
-	"dodo-app": {
-		auth: {
-			if input.external {
-				enabled: false
-			}
-			if !input.external {
-				enabled: true
-			}
-		}
-		network: input.network
-		subdomain: input.subdomain
-		service: {
-			name: "web"
-			port: name: "http"
-		}
-	}
-}
-
 portForward: [#PortForward & {
 	allocator: input.network.allocatePortAddr
 	reservator: input.network.reservePortAddr
@@ -112,51 +61,106 @@
 	targetPort: 22
 }]
 
-helm: {
-	softserve: {
-		chart: charts.softserve
-		info: "Installing Git server"
-		values: {
-			serviceType: "ClusterIP"
-			addressPool: ""
-			reservedIP: ""
-			adminKey: strings.Join([input.fluxKeys.public, input.dAppKeys.public], "\n")
-			privateKey: input.ssKeys.private
-			publicKey: input.ssKeys.public
-			ingress: {
-				enabled: false
+out: {
+	images: {
+		softserve: {
+			repository: "charmcli"
+			name: "soft-serve"
+			tag: "v0.7.1"
+			pullPolicy: "IfNotPresent"
+		}
+		dodoApp: {
+			repository: "giolekva"
+			name: "pcloud-installer"
+			tag: "latest"
+			pullPolicy: "Always"
+		}
+	}
+
+	charts: {
+		softserve: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/soft-serve"
+		}
+		dodoApp: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/dodo-app"
+		}
+	}
+
+	volumes: {
+		"config-repo": size: "10Gi"
+		db: size: "10Gi"
+	}
+
+	ingress: {
+		"dodo-app": {
+			auth: {
+				if input.external {
+					enabled: false
+				}
+				if !input.external {
+					enabled: true
+				}
 			}
-			image: {
-				repository: images.softserve.fullName
-				tag: images.softserve.tag
-				pullPolicy: images.softserve.pullPolicy
+			network: input.network
+			subdomain: input.subdomain
+			service: {
+				name: "web"
+				port: name: "http"
 			}
 		}
 	}
-	"dodo-app": {
-		chart: charts.dodoApp
-		info: "Installing supervisor"
-		values: {
-			image: {
-				repository: images.dodoApp.fullName
-				tag: images.dodoApp.tag
-				pullPolicy: images.dodoApp.pullPolicy
+
+	helm: {
+		softserve: {
+			chart: charts.softserve
+			info: "Installing Git server"
+			values: {
+				serviceType: "ClusterIP"
+				addressPool: ""
+				reservedIP: ""
+				adminKey: strings.Join([input.fluxKeys.public, input.dAppKeys.public], "\n")
+				privateKey: input.ssKeys.private
+				publicKey: input.ssKeys.public
+				image: {
+					repository: images.softserve.fullName
+					tag: images.softserve.tag
+					pullPolicy: images.softserve.pullPolicy
+				}
+				persistentVolumeClaimName: volumes["config-repo"].name
 			}
-			clusterRoleName: "\(release.namespace)-dodo-app"
-			port: 8080
-			apiPort: 8081
-			repoAddr: "soft-serve.\(release.namespace).svc.cluster.local:22"
-			sshPrivateKey: base64.Encode(null, input.dAppKeys.private)
-			self: "api.\(release.namespace).svc.cluster.local"
-			repoPublicAddr: "ssh://\(_domain):\(input.sshPort)"
-			namespace: release.namespace
-			envAppManagerAddr: "http://appmanager.\(global.namespacePrefix)appmanager.svc.cluster.local"
-			envConfig: base64.Encode(null, json.Marshal(global))
-			gitRepoPublicKey: input.ssKeys.public
-			persistentVolumeClaimName: volumes.db.name
-			allowedNetworks: strings.Join([for n in input.allowedNetworks { n.name }], ",")
-			external: input.external
-			fetchUsersAddr: "http://memberships-api.\(global.namespacePrefix)core-auth-memberships.svc.cluster.local/api/users"
+		}
+		"dodo-app": {
+			chart: charts.dodoApp
+			info: "Installing supervisor"
+			values: {
+				image: {
+					repository: images.dodoApp.fullName
+					tag: images.dodoApp.tag
+					pullPolicy: images.dodoApp.pullPolicy
+				}
+				clusterRoleName: "\(release.namespace)-dodo-app"
+				port: 8080
+				apiPort: 8081
+				repoAddr: "soft-serve.\(release.namespace).svc.cluster.local:22"
+				sshPrivateKey: base64.Encode(null, input.dAppKeys.private)
+				self: "api.\(release.namespace).svc.cluster.local"
+				repoPublicAddr: "ssh://\(_domain):\(input.sshPort)"
+				namespace: release.namespace
+				envAppManagerAddr: "http://appmanager.\(global.namespacePrefix)appmanager.svc.cluster.local"
+				envConfig: base64.Encode(null, json.Marshal(global))
+				gitRepoPublicKey: input.ssKeys.public
+				persistentVolumeClaimName: volumes.db.name
+				allowedNetworks: strings.Join([for n in input.allowedNetworks { n.name }], ",")
+				external: input.external
+				fetchUsersAddr: "http://memberships-api.\(global.namespacePrefix)core-auth-memberships.svc.cluster.local/api/users"
+				headscaleAPIAddr: "http://headscale-api.\(global.namespacePrefix)app-headscale.svc"
+			}
 		}
 	}
 }
diff --git a/core/installer/values-tmpl/env-dns.cue b/core/installer/values-tmpl/env-dns.cue
index adc8b3b..2b73ada 100644
--- a/core/installer/values-tmpl/env-dns.cue
+++ b/core/installer/values-tmpl/env-dns.cue
@@ -10,201 +10,203 @@
 description: "Environment local DNS manager"
 icon: ""
 
-images: {
-	coredns: {
-		repository: "coredns"
-		name: "coredns"
-		tag: "1.11.1"
-		pullPolicy: "IfNotPresent"
+out: {
+	images: {
+		coredns: {
+			repository: "coredns"
+			name: "coredns"
+			tag: "1.11.1"
+			pullPolicy: "IfNotPresent"
+		}
+		api: {
+			repository: "giolekva"
+			name: "dns-api"
+			tag: "latest"
+			pullPolicy: "Always"
+		}
 	}
-	api: {
-		repository: "giolekva"
-		name: "dns-api"
-		tag: "latest"
-		pullPolicy: "Always"
-	}
-}
 
-charts: {
-	coredns: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/coredns"
+	charts: {
+		coredns: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/coredns"
+		}
+		api: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/dns-api"
+		}
+		service: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/service"
+		}
+		ipAddressPool: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/metallb-ipaddresspool"
+		}
 	}
-	api: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/dns-api"
-	}
-	service: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/service"
-	}
-	ipAddressPool: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/metallb-ipaddresspool"
-	}
-}
 
-volumes: data: {
-	accessMode: "ReadWriteMany"
-	size: "5Gi"
-}
+	volumes: data: {
+		accessMode: "ReadWriteMany"
+		size: "5Gi"
+	}
 
-helm: {
-	coredns: {
-		chart: charts.coredns
-		values: {
-			image: {
-				repository: images.coredns.fullName
-				tag: images.coredns.tag
-				pullPolicy: images.coredns.pullPolicy
-			}
-			replicaCount: 1
-			resources: {
-				limits: {
-					cpu: "100m"
-					memory: "128Mi"
+	helm: {
+		coredns: {
+			chart: charts.coredns
+			values: {
+				image: {
+					repository: images.coredns.fullName
+					tag: images.coredns.tag
+					pullPolicy: images.coredns.pullPolicy
 				}
-				requests: {
-					cpu: "100m"
-					memory: "128Mi"
-				}
-			}
-			rollingUpdate: {
-				maxUnavailable: 1
-				maxSurge: "25%"
-			}
-			terminationGracePeriodSeconds: 30
-			serviceType: "LoadBalancer"
-			service: {
-				name: "coredns"
-				annotations: {
-					"metallb.universe.tf/loadBalancerIPs": global.network.dns
-				}
-			}
-			serviceAccount: create: false
-			rbac: {
-				create: false
-				pspEnable: false
-			}
-			isClusterService: false
-			servers: [{
-				zones: [{
-					zone: "."
-				}]
-				port: 53
-				plugins: [
-					{
-						name: "log"
-					},
-					{
-						name: "health"
-						configBlock: "lameduck 5s"
-					},
-					{
-						name: "ready"
+				replicaCount: 1
+				resources: {
+					limits: {
+						cpu: "100m"
+						memory: "128Mi"
 					}
-			    ]
-			}]
-			extraConfig: import: parameters: "\(_mountPath)/coredns.conf"
-			extraVolumes: [{
-				name: volumes.data.name
-				persistentVolumeClaim: claimName: volumes.data.name
-			}]
-			extraVolumeMounts: [{
-				name: volumes.data.name
-				mountPath: _mountPath
-			}]
-			livenessProbe: {
-				enabled: true
-				initialDelaySeconds: 60
-				periodSeconds: 10
-				timeoutSeconds: 5
-				failureThreshold: 5
-				successThreshold: 1
-			}
-			readinessProbe: {
-				enabled: true
-				initialDelaySeconds: 30
-				periodSeconds: 10
-				timeoutSeconds: 5
-				failureThreshold: 5
-				successThreshold: 1
-			}
-			zoneFiles: []
-			hpa: enabled: false
-			autoscaler: enabled: false
-			deployment: enabled: true
-		}
-	}
-	api: {
-		chart: charts.api
-		values: {
-			image: {
-				repository: images.api.fullName
-				tag: images.api.tag
-				pullPolicy: images.api.pullPolicy
-			}
-			config: "coredns.conf"
-			db: "records.db"
-			zone: networks.public.domain
-			publicIP: strings.Join(global.publicIP, ",")
-			privateIP: global.network.ingress
-			nameserverIP: strings.Join(global.nameserverIP, ",")
-			service: type: "ClusterIP"
-			volume: {
-				claimName: volumes.data.name
-				mountPath: _mountPath
+					requests: {
+						cpu: "100m"
+						memory: "128Mi"
+					}
+				}
+				rollingUpdate: {
+					maxUnavailable: 1
+					maxSurge: "25%"
+				}
+				terminationGracePeriodSeconds: 30
+				serviceType: "LoadBalancer"
+				service: {
+					name: "coredns"
+					annotations: {
+						"metallb.universe.tf/loadBalancerIPs": global.network.dns
+					}
+				}
+				serviceAccount: create: false
+				rbac: {
+					create: false
+					pspEnable: false
+				}
+				isClusterService: false
+				servers: [{
+					zones: [{
+						zone: "."
+					}]
+					port: 53
+					plugins: [
+						{
+							name: "log"
+						},
+						{
+							name: "health"
+							configBlock: "lameduck 5s"
+						},
+						{
+							name: "ready"
+						}
+					]
+				}]
+				extraConfig: import: parameters: "\(_mountPath)/coredns.conf"
+				extraVolumes: [{
+					name: volumes.data.name
+					persistentVolumeClaim: claimName: volumes.data.name
+				}]
+				extraVolumeMounts: [{
+					name: volumes.data.name
+					mountPath: _mountPath
+				}]
+				livenessProbe: {
+					enabled: true
+					initialDelaySeconds: 60
+					periodSeconds: 10
+					timeoutSeconds: 5
+					failureThreshold: 5
+					successThreshold: 1
+				}
+				readinessProbe: {
+					enabled: true
+					initialDelaySeconds: 30
+					periodSeconds: 10
+					timeoutSeconds: 5
+					failureThreshold: 5
+					successThreshold: 1
+				}
+				zoneFiles: []
+				hpa: enabled: false
+				autoscaler: enabled: false
+				deployment: enabled: true
 			}
 		}
-	}
-	"coredns-svc-cluster": {
-		chart: charts.service
-		values: {
-			name: "dns"
-			type: "LoadBalancer"
-			protocol: "TCP"
-			ports: [{
-				name: "udp-53"
-				port: 53
-				protocol: "UDP"
-				targetPort: 53
-			}]
-			targetPort: "http"
-			selector:{
-				"app.kubernetes.io/instance": "coredns"
-				"app.kubernetes.io/name": "coredns"
-			}
-			annotations: {
-				"metallb.universe.tf/loadBalancerIPs": global.network.dnsInClusterIP
+		api: {
+			chart: charts.api
+			values: {
+				image: {
+					repository: images.api.fullName
+					tag: images.api.tag
+					pullPolicy: images.api.pullPolicy
+				}
+				config: "coredns.conf"
+				db: "records.db"
+				zone: networks.public.domain
+				publicIP: strings.Join(global.publicIP, ",")
+				privateIP: global.network.ingress
+				nameserverIP: strings.Join(global.nameserverIP, ",")
+				service: type: "ClusterIP"
+				volume: {
+					claimName: volumes.data.name
+					mountPath: _mountPath
+				}
 			}
 		}
-	}
-	"ipaddresspool-dns": {
-		chart: charts.ipAddressPool
-		values: {
-			name: "\(global.id)-dns"
-			autoAssign: false
-			from: global.network.dns
-			to: global.network.dns
-			namespace: "metallb-system"
+		"coredns-svc-cluster": {
+			chart: charts.service
+			values: {
+				name: "dns"
+				type: "LoadBalancer"
+				protocol: "TCP"
+				ports: [{
+					name: "udp-53"
+					port: 53
+					protocol: "UDP"
+					targetPort: 53
+				}]
+				targetPort: "http"
+				selector:{
+					"app.kubernetes.io/instance": "coredns"
+					"app.kubernetes.io/name": "coredns"
+				}
+				annotations: {
+					"metallb.universe.tf/loadBalancerIPs": global.network.dnsInClusterIP
+				}
+			}
 		}
-	}
-	"ipaddresspool-dns-in-cluster": {
-		chart: charts.ipAddressPool
-		values: {
-			name: "\(global.id)-dns-in-cluster"
-			autoAssign: false
-			from: global.network.dnsInClusterIP
-			to: global.network.dnsInClusterIP
-			namespace: "metallb-system"
+		"ipaddresspool-dns": {
+			chart: charts.ipAddressPool
+			values: {
+				name: "\(global.id)-dns"
+				autoAssign: false
+				from: global.network.dns
+				to: global.network.dns
+				namespace: "metallb-system"
+			}
+		}
+		"ipaddresspool-dns-in-cluster": {
+			chart: charts.ipAddressPool
+			values: {
+				name: "\(global.id)-dns-in-cluster"
+				autoAssign: false
+				from: global.network.dnsInClusterIP
+				to: global.network.dnsInClusterIP
+				namespace: "metallb-system"
+			}
 		}
 	}
 }
diff --git a/core/installer/values-tmpl/env-manager.cue b/core/installer/values-tmpl/env-manager.cue
index cf38521..dbde7de 100644
--- a/core/installer/values-tmpl/env-manager.cue
+++ b/core/installer/values-tmpl/env-manager.cue
@@ -12,37 +12,39 @@
 name: "env-manager"
 namespace: "env-manager"
 
-images: {
-	envManager: {
-		repository: "giolekva"
-		name: "pcloud-installer"
-		tag: "latest"
-		pullPolicy: "Always"
+out: {
+	images: {
+		envManager: {
+			repository: "giolekva"
+			name: "pcloud-installer"
+			tag: "latest"
+			pullPolicy: "Always"
+		}
 	}
-}
 
-charts: {
-	envManager: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/env-manager"
+	charts: {
+		envManager: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/env-manager"
+		}
 	}
-}
 
-helm: {
-	"env-manager": {
-		chart: charts.envManager
-		values: {
-			repoIP: input.repoIP
-			repoPort: input.repoPort
-			repoName: input.repoName
-			sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
-			clusterRoleName: "\(global.pcloudEnvName)-env-manager"
-			image: {
-				repository: images.envManager.fullName
-				tag: images.envManager.tag
-				pullPolicy: images.envManager.pullPolicy
+	helm: {
+		"env-manager": {
+			chart: charts.envManager
+			values: {
+				repoIP: input.repoIP
+				repoPort: input.repoPort
+				repoName: input.repoName
+				sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
+				clusterRoleName: "\(global.pcloudEnvName)-env-manager"
+				image: {
+					repository: images.envManager.fullName
+					tag: images.envManager.tag
+					pullPolicy: images.envManager.pullPolicy
+				}
 			}
 		}
 	}
diff --git a/core/installer/values-tmpl/fluxcd-reconciler.cue b/core/installer/values-tmpl/fluxcd-reconciler.cue
index 3e742ff..fde9396 100644
--- a/core/installer/values-tmpl/fluxcd-reconciler.cue
+++ b/core/installer/values-tmpl/fluxcd-reconciler.cue
@@ -3,32 +3,34 @@
 name: "fluxcd-reconciler"
 namespace: "fluxcd-reconciler"
 
-images: {
-	fluxcdReconciler: {
-		repository: "giolekva"
-		name: "fluxcd-reconciler"
-		tag: "latest"
-		pullPolicy: "Always"
+out: {
+	images: {
+		fluxcdReconciler: {
+			repository: "giolekva"
+			name: "fluxcd-reconciler"
+			tag: "latest"
+			pullPolicy: "Always"
+		}
 	}
-}
 
-charts: {
-	fluxcdReconciler: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/fluxcd-reconciler"
+	charts: {
+		fluxcdReconciler: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/fluxcd-reconciler"
+		}
 	}
-}
 
-helm: {
-	"fluxcd-reconciler": {
-		chart: charts.fluxcdReconciler
-		values: {
-			image: {
-				repository: images.fluxcdReconciler.fullName
-				tag: images.fluxcdReconciler.tag
-				pullPolicy: images.fluxcdReconciler.pullPolicy
+	helm: {
+		"fluxcd-reconciler": {
+			chart: charts.fluxcdReconciler
+			values: {
+				image: {
+					repository: images.fluxcdReconciler.fullName
+					tag: images.fluxcdReconciler.tag
+					pullPolicy: images.fluxcdReconciler.pullPolicy
+				}
 			}
 		}
 	}
diff --git a/core/installer/values-tmpl/gerrit.cue b/core/installer/values-tmpl/gerrit.cue
index 91597da..4fc9eaf 100644
--- a/core/installer/values-tmpl/gerrit.cue
+++ b/core/installer/values-tmpl/gerrit.cue
@@ -32,76 +32,272 @@
   <path class='cls-1' d='m20.91007091,3.11035315l-.40677547-.44522187c.00619982-.006201.47746668-.43902117.68457526-.67217354.1996669-.24555409.56179703-.8011513.56799921-.8073523l.50351019.32244483c-.02604351.02604351-.38817364.59404272-.62008623.86564032s-.71061876.71061876-.73046245.73666257h.00123949Zm2.60932297,17.51743602c-.01984369-.00619982-.40057565-.25175391-.60768422-.36213013-.22991143-.11154711-.46273235-.21699848-.69821676-.31624294l.35468845-1.01446206-1.48572655-.73046127-.47126449,1.43984054c-.29019943.00619982-1.18188444.08433271-2.50018743.63992992-1.58866109.66473157-2.79038804,1.69903731-2.80279005,1.70523714l-.23935309.20710857,1.0653084.03844552.03224451-.02604351c.006201-.01240201,1.18808545-.88548401,1.84785668-1.16948362.14882055-.0582892.34228763-.12277823.5493962-.16742357-.34848863.16742357-.73046127.37453214-.99461837.52335388-.32988562.1996669-.94253135.63248824-.96857486.65233193l-.31004312.21331076,1.12979743.02604351.03224451-.01240201c.0198425-.01240201,1.92474772-1.03306389,2.95781042-1.26001615l.32244512-.07813052c.23935427-.0582892.40677547-.10293454.69821676-.10293454.2579561,0,.60768422.03224333,1.13723792.11657604.65853175.09673235,1.09135073.2579561,1.09755291.26539778l.39437583.15502156-.5295537-.72302077-.01984369-.01240201.00868117-.00744404Zm-14.84982503-12.065638h-.05208702l-1.66059121,1.51797106v.04464652c-.02604351.43902117.12897805.85943932.42661916,1.18808545.29764111.32244512.69077626.50971119,1.12979683.52335269h.07069003c.87804233,0,1.5948621-.67837425,1.64074811-1.55021558.0374887-.90290667-.65442416-1.66979928-1.55641658-1.72507964l.00124067.00123949Zm-.07813112,3.02850045h-.06448843c-.19170127-.00438435-.37976864-.05320469-.5493962-.14261955.46794696.07532156.90835369-.24296393.98367584-.7109109.01272844-.07907552.01432452-.1595431.00474153-.23906095-.06419689-.46466491-.49097867-.79070078-.95617285-.73046127-.43424238.06072443-.75232622.4398538-.73666227.87804233-.0493023-.1478661-.0732039-.30301539-.07069003-.45886367l1.50556906-1.38279201c.74277694.07474203,1.29972245.71365481,1.27241697,1.45968304-.0345792.7442305-.65017228,1.32863287-1.39519402,1.32450399l.00620041.00247898Zm15.42526651-6.16117612l-.48490836-.32244512-.67837543.43902117-.65233193-.47126568-.497308.29764111.67217325.48490718-.69077508.44522217.49110818.29764111.66473157-.42041815.63992992.46506467.50971001-.28399961-.67217325-.48490718.69821912-.44646166Zm15.66337715,25.53267101c-.35468845-.34228645-.69077508-.68457526-1.04050557-1.03306389-.79630214-.80614711-1.57814907-1.62644429-2.34516705-2.46050124-.90996396-.98277224-1.78242858-1.99961393-2.61552279-3.04834414-.37553745-.46918054-.73095919-.95411256-1.06530722-1.45348322-.41339634-.64390149-.78257785-1.31514039-1.10499459-2.00908043-.34239052-.73667646-.66127566-1.48404945-.95617285-2.24099065.40057565.34228645.76890797.73046245,1.09755291,1.14344011.03224333-.35468845.07068885-.71061876.09673235-1.0727489l.03844552-.5295537c0-.05828802.01984369-.11037504-.01240201-.16122256-.02454382-.04479436-.05499414-.08608905-.09053253-.12277705-.19346707-.24555409-.43282134-.46506467-.66473157-.68457526-.31804304-.29794743-.64094706-.59066724-.96857486-.87804233l-.29764111-.2579561c-.04464534-.04464652-.09673235-.08433153-.14261955-.12277705-.05208702-.05208702-.07068885-.16122256-.09053253-.22571159l-.2579561-.79495029c1.00825987.47126568,1.83421518,1.22777045,2.60932297,2.00907925.00619982-.36213013.00619982-.71681977,0-1.0789499,0-.18726607,0-.37453214-.01240201-.56799921,0-.05208702.01240201-.16742357-.02604351-.21330958-.01984369-.03224451-.05208702-.05828802-.07068885-.09053253-.18106507-.19346707-.38817364-.35468964-.5878429-.51715169-.28399961-.23935309-.58164071-.45886367-.87184014-.67837425-.2579561-.19346707-.52335388-.38073314-.7875098-.56799921-.03224333-.02604351-.12897805-.06448902-.11037622-.09673354l.06448902-.13517905c.03844552-.05828802.02604351-.07069003.09053253-.04464652l.28399961.09673354c.66473157.2579561,1.2848178.60768422,1.8863022,1.00825987-.20820614-.74461962-.49853567-1.46378708-.86564032-2.1442583-.65563881-1.25890203-1.46959424-2.42878067-2.42205572-3.4811643-.62615595-.7038808-1.29947171-1.36435044-2.01528025-1.97683473-.36833232-.31004312-.7552641-.60768422-1.15583975-.87804233-.18726489-.12277705-.36833232-.24555409-.56799921-.36213013-.08433271-.04464593-.17486524-.09053253-.25175391-.15502156-.29764111-.23315267-.60024254-.46506467-.90408584-.70441776.39437583.16122256.77510779.34228763,1.13723792.55559721-.20710857-.32244512-.56179703-.5493962-.89788365-.71681977-.51418543-.25650845-1.05045934-.46611966-1.60230259-.62628724.18726489-.19966778.38817364-.38073285.57544089-.57544001.18726489-.19966778.36833232-.40677665.5431952-.62008623.21331076-.24555409.42041934-.49110818.62008623-.74906398l-2.70605533-1.69903628c-.1996669.4005758-.45886485.78130865-.7552641,1.12359627-.29764111.33608662-.64612974.60768452-.99461837.89168413-.31004312.2579558-.62628724.51715199-.93012935.77510779-.08433153.07068973-.15502156.14261984-.24555409.19966778-.03844552.03844522-.15502156.006201-.20710857.006201h-.39437464c-.49110818.01240171-.98221636.05208731-1.47332454.11657634-.09673354.006201-.18726607.0198428-.29020061.01240171l-.29020061-.0198428c-.22571159-.01240171-.45266266-.02604351-.67837425-.02604351-.47746668-.006201-.95617285,0-1.43984054.03844522-.40456023.03341748-.80618022.09600232-1.20172695.18726607-.39394768.09978762-.76465136.27545061-1.09135191.51715199-.32244512.23315267-.63992873.5295537-.83339581.87804233-.19346707.34228763-.28399961.74906428-.35468964,1.13723792-.21951058.0198425-.43902117.0198425-.65853175.02604351-.67978583.03606293-1.3523068.15773294-2.00163816.36213013-.81508789.25388636-1.56763764.67642986-2.20874732,1.24017246-.32830018.30005267-.62690964.63105596-.89168383.98841737-.03224451.03844552-.06448902.08433153-.10293425.11037563-.0198428.01240201-.04464623.02604351-.05828802.04464652-.02450449.04328876-.05272095.08436938-.08433182.12277705l-.21951058.35468964c-.2777986.47126568-.45886367.98221636-.63248794,1.49812856l-.07813082.19966808.05828802-.03224451c-.01893831.1114525-.03383025.22355549-.04464623.33608662l-.01240171.16742357v.09053253l-.11037533.0198425c-.2219529.04571925-.44169382.10158624-.6585316.16742357-.19552693.05895389-.38235909.14361185-.55559735.25175509-.17486436.10293454-.32244483.25175509-.47126553.39437464-.53470711.51039244-.92674,1.15166711-1.13723814,1.86025869C-.00047122,14.62163461-.05875932,15.26032385.07021861,15.84816557c.03224448.14882055.07813087.31004312.17486431.43282016.12277708.14882055.32988588.20710857.51715187.15502156.21951051-.05828802.41297744-.19966808.61388537-.30384211l.52335269-.26539659c.74906398-.36833114,1.53781386-.73666227,2.37741068-.86564032.05828802-.006201.16122256.11037504.21330958.14882055.08433182.06448902.16742327.12277705.24555409.17486406.18106507.11657604.36833114.21330958.55559721.29764111.43282016.18726607.89168383.29020061,1.35054809.38073314.83339581.17486406,1.67919363.28399961,2.53243194.29764111.34228763.006201.68457526,0,1.03306389-.02604351.36833114-.03224451.74286328-.08433153,1.11739542-.08433153.41297766-.006201.81975431.03844552,1.22777045.09053253.42041815.05208702.83959681.11037504,1.26001497.17486406.85943932.12897805,1.70523714.2777986,2.55103495.45886367-.18726607.29764111-.37453214.60024373-.55559721.90408584-.01240201.0198425-.12277705,0-.14882055,0-.06889702-.00619746-.13821155-.00619746-.20710857,0-.12277705,0-.25175509.01240201-.37453214.03224451-.34628049.04689132-.68643449.13130683-1.01446088.25175509-.81355331.30384211-1.55641658.8011513-2.19634532,1.38279201-.36833114.32244512-.71061876.67217325-1.00825987,1.0591074-.07769174.10364181-.15294943.20908372-.22571159.31624294.16742357-.03844552.34848863-.07068885.52335269-.11657604.10293454-.02604351.19346707-.04464534.29020061-.06448902.03224451-.01240201.04464652-.02604351.08433153-.04464534.36033003-.33479036.74230148-.64550526,1.14343892-.93012935.18726607-.12897805.37453214-.25175391.58164071-.35468845.2715976-.13517787.58164071-.21331076.87804233-.28399961-.5431952.29764111-1.12359642.57544089-1.6147046.96237268-.28399961.23315208-.5431952.48490836-.81355331.73046245l1.0851509-.17486524c.01240201,0,.0198425-.00619982.03844552-.01984369l.2777986-.15502156c.18726607-.10293454.38073314-.20710857.57543971-.30384329.39437464-.20710857.79495029-.40057565,1.20792795-.56179703.39437464-.15502156.79495029-.29764111,1.21412895-.37453214.36213013-.07813052.70441776-.08433271,1.0727489,0,.56179821.12277823,1.10499341.38073196,1.60230259.68457526.03224451.02604351.05828802.0582892.09053253.02604351.02604351-.02604351.16122256-.12277823.14882055-.15502156l-.29764111-.54939738c-.02604351-.05208702-.03844552-.08433271-.08433153-.11037622l-.21951058-.12897805c-.28953356-.16791321-.56980641-.3513106-.83959681-.5493962-.06448902-.03844552-.12897805-.06448902-.13517905-.14882055-.01240201-.07069003.0198425-.14882055.04464652-.21951058.05208702-.14882055.12897805-.28399961.21330958-.41297766.05828802-.09673354.12277705-.18106507.18106507-.2715976.02604351-.03224451.0198425-.03844552.05208702-.02604351l.19966808.05828802c.38817364.12897805.77510779.26539659,1.17568344.36213013.44522217.11657604.89788483.20710857,1.35674969.29020061l.07813052.0198425c-.09053253-.05208702-.14882174-.14882055-.21951058-.22571159-.03844552-.05208702-.0582892-.08433153-.12277823-.09673354l-.17486524-.03844552c-.12277823-.02604351-.23935427-.04464652-.36833232-.07813052-.43511464-.0892824-.86422461-.20567275-1.28481898-.34848863-.62132927-.20887792-1.25299907-.38562961-1.89250321-.5295537-.8031773-.17243002-1.610817-.32337238-2.4220569-.45266266-.41297766-.07069003-.82719481-.12277705-1.24017246-.18106507-.40961518-.06287106-.82115347-.11250393-1.23397146-.14882055-.89168383-.05208702-1.77592716.14882055-2.673812.13517905-.60024373-.006201-1.21412895-.09053253-1.80817168-.17486406l-.11657604-.01240201c.42041815-.15502156.82719481-.31004312,1.24017246-.47126568.39437464-.15502156.78130879-.31624412,1.16948243-.47746668.20710857-.08433153.41297766-.17486406.61388523-.2579561.18726607-.08433153.37453214-.16742357.5431952-.28399961.67837425-.45886367,1.11739542-1.20172695,1.6147046-1.83421518.45266266-.59404272.96857486-1.15584093,1.63454711-1.49812856.21330958-.11037504.43282016-.19346707.66473275-.2579561-.47126568-.72302077-.91028684-1.53781357-1.03306389-2.40345389l.15502156.05828802c.05828802.0198425.11037504.02604351.12277705.07813112l.04464652.21950999c.04464652.14882055.09053253.29020061.14261955.43282016.12277705.31624412.26539659.62008623.43282016.91028684.36978352.65215334.79332405,1.27232236,1.26621597,1.85405769.89168383,1.13723792,1.89250321,2.1578998,2.86851856,3.21080619.23935309.2579561.45886367.5357547.67837425.81355331.25175509.31004312.50351019.62008623.7552641.92392834.1996669.23315208.38817364.47126568.5878429.70441776l.15502156.18726607c.02604351.03224451.06448902.04464652.10293454.05828802,1.00081819.49730919,2.00908043.99461837,3.00989862,1.47952673.72302077.34848863,1.43983936.69821676,2.17030181,1.03306389.38817364.18106507.77510779.36213013,1.17568344.5295537.04464534.01984369.10293454.04464534.16122138.06448902.02604351.01240201.04464534.02604351.07813052.03844552l.02604351.07813052c.04464534.12277823.08433271.23935427.12277823.36213013.08433271.23315208.16122138.45886485.24555409.68457526.54939738,1.51177006,1.14963993,3.00369643,1.89250202,4.42493633.578322,1.08265063,1.1791086,2.15315236,1.80196949,3.21080737.66473157,1.12359642,1.343107,2.23479083,2.02768226,3.34598525.54939738.88548401,1.09755291,1.77592598,1.66679162,2.65521017.23935427.37453214.47746668.76270578.72302077,1.13723792l.10293454.15502156,1.60230259-2.35136687c.01240201-.01240201.13517787-.16742357.12277823-.18106507l-.2579561-.40057565c-.47746668-.74286209-.96237268-1.48572655-1.43983936-2.22859101l-1.54401576-2.39601221,1.0851509,1.31830299,1.84785668,2.23479083c.1996669.23935427.38817364.47746668.58164071.72302077.36213013-.66473157.73046245-1.32450517,1.09755291-1.98303692.14261955-.2715976.29019943-.54939738.4390188-.81355331l-.00495796-.01239964ZM20.05063278,1.84413718c.50971001-.40677665.93012935-.93012935,1.25257447-1.49812827l1.95079123,1.23397175.29764111.18106507c-.40677547.49730919-.81975313,1.00081908-1.2848178,1.44728133-.06097634.05293118-.11933415.10880527-.17486524.16742327-.03224333.04464623-.07813052.08433182-.11657604.12277705l-.05208702.06448902c-.03844552-.01240171-.07813052-.02604351-.11657604-.03224451-.23935427-.06448902-.47746668-.12897805-.71681859-.18106507-.48385337-.10967516-.97316382-.19371042-1.46588405-.25175509-.31624412-.03844522-.63992873-.06448902-.95617285-.09673354.45266266-.38817394.91772734-.77510779,1.38279319-1.15584064l-.00000237-.00124038Zm-2.90076426,2.26703535c.38817364-.16122256.8011513-.24555409,1.20792795-.31624412.81333332-.13418557,1.6412555-.15588524,2.46050242-.06448902.5878429.06448902,1.16328143.17486406,1.72508082.34848863l-.23315208.18726607-.37453214-.10293454c-.23315208-.04464593-.46506467-.09673354-.69821676-.13517905-.41077543-.06070432-.8250115-.09508601-1.24017246-.10293454-.76375012-.01441855-1.52604905.07231982-2.26703535.2579561-.50273314.13067111-.99001049.31485561-1.45348204.5493962-.47126568.2579561-.91772734.57543971-1.28481898.96237386-.22622016.22823196-.4255346.4816405-.59404272.75526528-.09673354.14882055-.17486406.29764111-.25175509.45266266-.03753719.07754153-.06985385.15750409-.09673354.23935309l-.05208702.12277705c-.00516494.02191581-.01179409.04346025-.0198425.06448902l-.11037504-.0198425-.13517905-.01240201c.38817364-1.19552594,1.26001497-2.13185629,2.34516587-2.73210002.34228763-.18726607.70441776-.35468964,1.0727489-.45886367l-.00000118.00496151Zm-6.49726393.61388523c.43902117-.63248824,1.13723792-1.0727489,1.8863022-1.23397146.8271948-.16122256,1.67919363-.19346678,2.51879044-.17486436-.86564032.21951058-1.73748165.54319549-2.46050242,1.0789502-.56594483.12955226-1.09433828.38801929-1.54401457.75526528-.14516831.12120698-.28113388.2530277-.40677665.39437464-.0588983.06001006-.11281735.12471257-.16122256.19346707-.0198425.0198425-.04464652.04464593-.05828802.07813112h-.14261955c.03844552-.21951058.09053253-.44522217.16122256-.65853175.05208702-.14882055.11037504-.29764111.19966808-.43282016l.0074405-.00000059Zm1.45348204.09673354c-.23729515.23928035-.45347638.49861373-.64612974.77510779-.07813052.11037563-.16122256.25175509-.29764111.31004312-.14261955.06448902-.32244512-.006201-.46506467-.04464593.37108569-.46297185.85614544-.82153135,1.40759603-1.04050497h.00123949ZM3.81181355,9.56917155c.12897805-.26539659.2976414-.52335269.45266296-.76890678.02604351-.04464652.09053253-.07813052.12277705-.10293454.08433182-.05828802.16122227-.11657604.23935338-.18106507.5097106-.36833114,1.02686288-.73666227,1.55021558-1.0789499.2777986-.18106507.5493962-.35468964.83339581-.50971119.29764111-.16742357.60768481-.29764111.93012935-.40677665.57867031-.17905976,1.17035173-.31298572,1.76972616-.40057565.28399902-.03844552.59404213-.09673354.87804174-.02604351-.5493962.31004312-1.09135191.62008623-1.64074811.92392834-.56179821.31624412-1.13723792.60768422-1.71143814.91028684-.5493962.29764111-1.09135191.62008623-1.62090561.95617285-.55559721.34228763-1.1049937.69077626-1.65314982,1.04050438-.13517876.08433153-.26539689.17486406-.40057565.2579561.07813082-.20710857.14882055-.42041815.25175509-.61388523h-.00123979Zm1.66679162,5.05122238c-.04464593.09053253-.21951058.07069003-.31004312.07069003-.15502156,0-.30384211,0-.45886367.006201-.42560113.02528184-.8463806.10360514-1.25257417.23315208-.7875095.23935309-1.52417192.62008623-2.26083434.99461837-.16122241.09053253-.33608677.22571159-.52335277.2715976-.10319415.02663487-.21197917-.01459359-.27159775-.10293454-.05594793-.10200492-.09094479-.2141635-.10293432-.32988562-.04154617-.19982775-.06109201-.40360223-.05828809-.60768422,0-.5493962.12897794-1.09135191.35468934-1.58866109.16122241-.34848863.36833121-.66473275.61388537-.94997185.25175502-.29020061.54319557-.58164071.89168405-.74286328.58784172-.2715976,1.28481869-.35468964,1.92474772-.38817364.12277705-.01240201.23935338-.01240201.36213043-.01240201.16122256,0,.35468934-.02604351.5097109.03224451.12277734.04464652.19966778.17486406.2517548.28399961.07069003.17486406.12277705.37453214.16122256.55559721.10293454.43282016.20710857.86564032.23935309,1.31086249.0198425.19346707.03224451.39437464.006201.58784172-.01240201.12277705-.0198425.2579561-.07813112.37453214l.00124008.00123949Zm4.67669024-3.26785472c-.32191467.20096789-.6640036.36764871-1.02066188.49730919-.35418402.1315481-.71942294.2311592-1.09135191.29764111-.16122256.0198425-.32244512.04464652-.48490718.03224451-.16742357,0-.31624412-.05828802-.48490718-.11037504-.32637412-.09433498-.64220606-.22191387-.94253135-.38073314-.11292439-.06073625-.21015763-.14693057-.28399961-.25175509-.04557969-.08727414-.05918097-.18774744-.03844552-.28399961.0198425-.38073314.15502156-.75526528.33608662-1.09135191.31004312-.56799921.79495029-1.01446088,1.38899302-1.28481898,1.45348204-.65233074,3.08058865-.04464652,4.31456011.81355331l.13517905.09673354c-.2579561.2777986-.51715169.5357547-.78130879.79495029-.32988562.31004312-.67217325.62628724-1.05290639.87184133l.006201-.00123949Zm3.57789784-3.18476268c-.78077184-.34143075-1.53056113-.74972779-2.24099184-1.22032996.74286328.31004312,1.54401457.49730919,2.33772537.61388523.42661916.05828802.86564032.11657604,1.29225948.12897805.46506467.0198425.93633035-.03224451,1.38899302-.12277705.90408584-.16742357,1.76972616-.50351019,2.59071996-.91028684.82099498-.40677665,1.59486092-.88548283,2.33772537-1.42123753.35468845-.25175509.71061876-.51715169,1.04670539-.8011513.03224333-.02604351.28399961-.2715976.30384329-.2579561l.06448902.05208702.89168383.69077626c.5431952.42041815,1.0851509.83959681,1.63454829,1.26001497-1.50363058.54005862-3.02220601,1.03756828-4.55391438,1.49192756-.8594405.2579561-1.71143814.49730919-2.58327947.69821735-.68457526.16122256-1.38279201.31004252-2.07976928.34228704-.83959681.03844552-1.66059061-.21951058-2.42825791-.54319579l-.00248017-.0012389Zm14.63031445,6.51710643c.12897805.03224451.24555409.07813052.35468845.11037504l.16122138.05828802c.02604351.006201.04464534.02604351.07068885.03224451l.03224333.09673354c.07813052.24555409.14882174.47746668.23315208.71681977-.24555409-.21330958-.497308-.42041815-.74286209-.62628724l-.14882174-.12277705c-.03844552-.02604351-.07813052-.03844552-.05208702-.07813052l.09673235-.18726607h-.0049556Zm-1.10499459,2.38361139l.0582892-.10293454.04464534-.07069003c.01984369-.03224451.01240201-.03844552.05208702-.0198425.21331076.08433153.42661916.18106507.63248824.2777986.40057565.18106507.7875098.38073314,1.16328143.60024373.18106507.11037504.36213013.22571159.53575352.34228763l.24555409.17486406.12277823.09053253c.05208702.04464652.07068885.10293454.09673235.16742238.16742357.46506467.36213013.93632917.55559721,1.39519402.12897805.31624294.2715976.62628605.41297766.93632917-.49014545-.60819752-1.01871039-1.18440245-1.58245891-1.72508082-.56908259-.56757817-1.16417557-1.10845996-1.78336766-1.62090561l-.53575352-.43902117-.0186042-.00619746Zm6.21450381,10.62579745l-.1996669.14882174.01240201.02604351.10293454.15502156.45266266.69077508,1.49192637,2.31912354c.51715169.79495148,1.0268617,1.59486092,1.54401576,2.38981239l.50971001.79495148.13517787.21951058c.02113995.02331852.03869389.04964825.05208702.07813052-.43282134.63248824-.86564032,1.27241579-1.29846167,1.90490403-.04464534.0582892-.08433271.12277823-.12277823.18106507-.39437583-.61388641-.7875098-1.22777045-1.18188326-1.84785668-.65853175-1.04670539-1.31086131-2.11201261-1.95699105-3.16492018-.7160782-1.16891354-1.41396616-2.3488737-2.09341078-3.53945232-.63248824-1.11739423-1.24017246-2.25463452-1.74988247-3.43651778-.4903607-1.15152755-.93632207-2.32145824-1.33690482-3.50720899.8011513.32244512,1.62090442.60768422,2.44810159.86564032.23935427.07068885.47746668.14882174.71681859.20710857l.10293454.03224333c.01240201,0,.02604351-.06448902.03224333-.08433271.03224333-.10293454.05208702-.21331076.07813052-.32244512.04464534-.20710857.08433271-.42041934.11657604-.62628605.34228645.70441894.72302077,1.39519402,1.16328143,2.04132376.35468845.52335388.73666227,1.0268617,1.13723792,1.52417207.8640815,1.08055958,1.76924361,2.12762215,2.71349701,3.13887667.76890797.81355331,1.54401576,1.6283461,2.33152319,2.42205572l.63992992.63992992c.01240201.01240201.06448902.05208702.06448902.07068885l-.05208702.08433271-.24555409.45886485-.97477468,1.76352634c-.29764111-.36213013-.59404272-.71681859-.89168383-1.0851509l-1.86025869-2.25463452c-.52335388-.63992992-1.05290757-1.27861798-1.57625909-1.9185479l-.31004312-.36833232.00496033-.00123713Z'/>
 </svg>"""
 
-ingress: {
-	gerrit: {
-		auth: enabled: false
-		network: input.network
-		subdomain: input.subdomain
-		service: {
-			name: "gerrit-gerrit-service"
-			port: number: _httpPort // TODO(gio): make optional
+out: {
+	ingress: {
+		gerrit: {
+			auth: enabled: false
+			network: input.network
+			subdomain: input.subdomain
+			service: {
+				name: "gerrit-gerrit-service"
+				port: number: _httpPort // TODO(gio): make optional
+			}
 		}
 	}
-}
 
-// TODO(gio): configure busybox
-_images: {
-	gerrit: #Image & {
-		repository: "k8sgerrit"
-		name: "gerrit"
-		tag: _version
-		pullPolicy: "Always"
+	// TODO(gio): configure busybox
+	images: {
+		gerrit: {
+			repository: "k8sgerrit"
+			name: "gerrit"
+			tag: _version
+			pullPolicy: "Always"
+		}
+		gerritInit: {
+			repository: "k8sgerrit"
+			name: "gerrit-init"
+			tag: _version
+			pullPolicy: "Always"
+		}
+		gitGC: {
+			repository: "k8sgerrit"
+			name: "git-gc"
+			tag: _version
+			pullPolicy: "Always"
+		}
 	}
-	gerritInit: #Image & {
-		repository: "k8sgerrit"
-		name: "gerrit-init"
-		tag: _version
-		pullPolicy: "Always"
-	}
-	gitGC: #Image & {
-		repository: "k8sgerrit"
-		name: "git-gc"
-		tag: _version
-		pullPolicy: "Always"
-	}
-}
-images: _images
+	_images: images
 
-charts: {
-	ingress: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/ingress"
+	charts: {
+		ingress: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/ingress"
+		}
+		gerrit: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/gerrit"
+		}
+		oauth2Client: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/oauth2-client"
+		}
+		resourceRenderer: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/resource-renderer"
+		}
 	}
-	gerrit: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/gerrit"
-	}
-	oauth2Client: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/oauth2-client"
-	}
-	resourceRenderer: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/resource-renderer"
-	}
-}
 
-volumes: {
-	git: {
-		accessMode: "ReadWriteMany"
-		size: "50Gi"
+	volumes: {
+		git: {
+			accessMode: "ReadWriteMany"
+			size: "50Gi"
+		}
+		logs: {
+			accessMode: "ReadWriteMany"
+			size: "5Gi"
+		}
 	}
-	logs: {
-		accessMode: "ReadWriteMany"
-		size: "5Gi"
+
+	helm: {
+		"oauth2-client": {
+			chart: charts.oauth2Client
+			info: "Creating OAuth2 client"
+			values: {
+				name: "\(release.namespace)-gerrit"
+				secretName: _oauth2ClientCredentials
+				grantTypes: ["authorization_code"]
+				scope: "openid profile email"
+				hydraAdmin: "http://hydra-admin.\(global.id)-core-auth.svc.cluster.local"
+				redirectUris: ["https://\(_domain)/oauth"]
+			}
+		}
+		"config-renderer": {
+			chart: charts.resourceRenderer
+			info: "Generating Gerrit configuration"
+			values: {
+				name: "config-renderer"
+				secretName: _oauth2ClientCredentials
+				resourceTemplate: """
+				apiVersion: v1
+				kind: ConfigMap
+				metadata:
+				  name: \(_gerritConfigMapName)
+				  namespace: \(release.namespace)
+				data:
+				  replication.config: |
+					[gerrit]
+					  autoReload = false
+					  replicateOnStartup = true
+					  defaultForceUpdate = true
+				  gerrit.config: |
+					[gerrit]
+					  basePath = git # FIXED
+					  serverId = gerrit-1
+					  # The canonical web URL has to be set to the Ingress host, if an Ingress
+					  # is used. If a LoadBalancer-service is used, this should be set to the
+					  # LoadBalancer's external IP. This can only be done manually after installing
+					  # the chart, when you know the external IP the LoadBalancer got from the
+					  # cluster.
+					  canonicalWebUrl = https://\(_domain)
+					  disableReverseDnsLookup = true
+					[index]
+					  type = LUCENE
+					[auth]
+					  type = OAUTH
+					  gitBasicAuthPolicy = HTTP
+					  userNameToLowerCase = true
+					  userNameCaseInsensitive = true
+					[plugin "gerrit-oauth-provider-pcloud-oauth"]
+					  root-url = https://hydra.\(networks.public.domain)
+					  client-id = "{{ .client_id }}"
+					  client-secret = "{{ .client_secret }}"
+					  link-to-existing-openid-accounts = true
+					[download]
+					  command = branch
+					  command = checkout
+					  command = cherry_pick
+					  command = pull
+					  command = format_patch
+					  command = reset
+					  scheme = http
+					  scheme = anon_http
+					[httpd]
+					  # If using an ingress use proxy-http or proxy-https
+					  listenUrl = proxy-http://*:8080/
+					  requestLog = true
+					  gracefulStopTimeout = 1m
+					[sshd]
+					  listenAddress = 0.0.0.0:29418
+					  advertisedAddress = \(_domain):\(input.sshPort)
+					[transfer]
+					  timeout = 120 s
+					[user]
+					  name = Gerrit Code Review
+					  email = gerrit@\(networks.public.domain)
+					  anonymousCoward = Unnamed User
+					[cache]
+					  directory = cache
+					[container]
+					  user = gerrit # FIXED
+					  javaHome = /usr/lib/jvm/java-11-openjdk # FIXED
+					  javaOptions = -Djavax.net.ssl.trustStore=/var/gerrit/etc/keystore # FIXED
+					  javaOptions = -Xms200m
+					  # Has to be lower than 'gerrit.resources.limits.memory'. Also
+					  # consider memories used by other applications in the container.
+					  javaOptions = -Xmx4g
+				"""
+			}
+		}
+		gerrit: {
+			chart: charts.gerrit
+			info: "Installing Gerrit server"
+			values: {
+				images: {
+					busybox: {
+						registry: _dockerIO
+						tag: "latest"
+					}
+					registry: {
+						name: _dockerIO
+						ImagePullSecret: create: false
+						imagePullPolicy: "Always"
+					}
+					version: _version
+				}
+				storageClasses: {
+					default: {
+						name: _longhorn
+						create: false
+					}
+					shared: {
+						name: _longhorn
+						create: false
+					}
+				}
+				persistence: {
+					enabled: true
+					size: "10Gi"
+				}
+				nfsWorkaround: {
+					enabled: false
+					chownOnStartup: false
+					idDomain: _domain
+				}
+				networkPolicies: enabled: false
+				gitRepositoryStorage: {
+					externalPVC: {
+						use: true
+						name: volumes.git.name
+					}
+				}
+				logStorage: {
+					enabled: true
+					externalPVC: {
+						use: true
+						name: volumes.logs.name
+					}
+				}
+				ingress: enabled: false
+				gitGC: {
+					image: _images.gitGC.imageName
+					logging: persistence: enabled: false
+				}
+				gerrit: {
+					images: {
+						gerritInit: _images.gerritInit.imageName
+						gerrit: _images.gerrit.imageName
+					}
+					service: {
+						type: "LoadBalancer"
+						externalTrafficPolicy: ""
+						additionalAnnotations: {
+							"metallb.universe.tf/address-pool": global.id
+						}
+						http: port: _httpPort
+						ssh: {
+							enabled: true
+							port: _sshPort
+						}
+					}
+					pluginManagement: {
+						plugins: [{
+							name: "gitiles"
+						}, {
+							name: "download-commands"
+						}, {
+							name: "singleusergroup"
+						}, {
+							name: "codemirror-editor"
+						}, {
+							name: "reviewnotes"
+						}, {
+							name: "oauth"
+							url: "https://drive.google.com/uc?export=download&id=1rSUpZCAVvHZTmRgUl4enrsAM73gndjeP"
+							sha1: "cbdc5228a18b051a6e048a8e783e556394cc5db1"
+						}, {
+							name: "webhooks"
+						}]
+						libs: []
+						cache: enabled: false
+					}
+					etc: {
+						secret: {
+							ssh_host_ecdsa_key: input.key.private
+							"ssh_host_ecdsa_key.pub": input.key.public
+						}
+						existingConfigMapName: _gerritConfigMapName
+					}
+				}
+			}
+		}
 	}
 }
 
@@ -124,197 +320,3 @@
 
 _oauth2ClientCredentials: "gerrit-oauth2-credentials"
 _gerritConfigMapName: "gerrit-config"
-
-helm: {
-	"oauth2-client": {
-		chart: charts.oauth2Client
-		info: "Creating OAuth2 client"
-		values: {
-			name: "\(release.namespace)-gerrit"
-			secretName: _oauth2ClientCredentials
-			grantTypes: ["authorization_code"]
-			scope: "openid profile email"
-			hydraAdmin: "http://hydra-admin.\(global.id)-core-auth.svc.cluster.local"
-			redirectUris: ["https://\(_domain)/oauth"]
-		}
-	}
-	"config-renderer": {
-		chart: charts.resourceRenderer
-		info: "Generating Gerrit configuration"
-		values: {
-			name: "config-renderer"
-			secretName: _oauth2ClientCredentials
-			resourceTemplate: """
-apiVersion: v1
-kind: ConfigMap
-metadata:
-  name: \(_gerritConfigMapName)
-  namespace: \(release.namespace)
-data:
-  replication.config: |
-    [gerrit]
-      autoReload = false
-      replicateOnStartup = true
-      defaultForceUpdate = true
-  gerrit.config: |
-    [gerrit]
-      basePath = git # FIXED
-      serverId = gerrit-1
-      # The canonical web URL has to be set to the Ingress host, if an Ingress
-      # is used. If a LoadBalancer-service is used, this should be set to the
-      # LoadBalancer's external IP. This can only be done manually after installing
-      # the chart, when you know the external IP the LoadBalancer got from the
-      # cluster.
-      canonicalWebUrl = https://\(_domain)
-      disableReverseDnsLookup = true
-    [index]
-      type = LUCENE
-    [auth]
-      type = OAUTH
-      gitBasicAuthPolicy = HTTP
-      userNameToLowerCase = true
-      userNameCaseInsensitive = true
-    [plugin "gerrit-oauth-provider-pcloud-oauth"]
-      root-url = https://hydra.\(networks.public.domain)
-      client-id = "{{ .client_id }}"
-      client-secret = "{{ .client_secret }}"
-      link-to-existing-openid-accounts = true
-    [download]
-      command = branch
-      command = checkout
-      command = cherry_pick
-      command = pull
-      command = format_patch
-      command = reset
-      scheme = http
-      scheme = anon_http
-    [httpd]
-      # If using an ingress use proxy-http or proxy-https
-      listenUrl = proxy-http://*:8080/
-      requestLog = true
-      gracefulStopTimeout = 1m
-    [sshd]
-      listenAddress = 0.0.0.0:29418
-      advertisedAddress = \(_domain):\(input.sshPort)
-    [transfer]
-      timeout = 120 s
-    [user]
-      name = Gerrit Code Review
-      email = gerrit@\(networks.public.domain)
-      anonymousCoward = Unnamed User
-    [cache]
-      directory = cache
-    [container]
-      user = gerrit # FIXED
-      javaHome = /usr/lib/jvm/java-11-openjdk # FIXED
-      javaOptions = -Djavax.net.ssl.trustStore=/var/gerrit/etc/keystore # FIXED
-      javaOptions = -Xms200m
-      # Has to be lower than 'gerrit.resources.limits.memory'. Also
-      # consider memories used by other applications in the container.
-      javaOptions = -Xmx4g
-"""
-		}
-	}
-	gerrit: {
-		chart: charts.gerrit
-		info: "Installing Gerrit server"
-		values: {
-			images: {
-				busybox: {
-					registry: _dockerIO
-					tag: "latest"
-				}
-				registry: {
-					name: _dockerIO
-					ImagePullSecret: create: false
-					imagePullPolicy: "Always"
-				}
-				version: _version
-			}
-			storageClasses: {
-				default: {
-					name: _longhorn
-					create: false
-				}
-				shared: {
-					name: _longhorn
-					create: false
-				}
-			}
-			persistence: {
-				enabled: true
-				size: "10Gi"
-			}
-			nfsWorkaround: {
-				enabled: false
-				chownOnStartup: false
-				idDomain: _domain
-			}
-			networkPolicies: enabled: false
-			gitRepositoryStorage: {
-				externalPVC: {
-					use: true
-					name: volumes.git.name
-				}
-			}
-			logStorage: {
-				enabled: true
-				externalPVC: {
-					use: true
-					name: volumes.logs.name
-				}
-			}
-			ingress: enabled: false
-			gitGC: {
-				image: _images.gitGC.imageName
-				logging: persistence: enabled: false
-			}
-			gerrit: {
-				images: {
-					gerritInit: _images.gerritInit.imageName
-					gerrit: _images.gerrit.imageName
-				}
-				service: {
-					type: "LoadBalancer"
-					externalTrafficPolicy: ""
-					additionalAnnotations: {
-						"metallb.universe.tf/address-pool": global.id
-					}
-					http: port: _httpPort
-					ssh: {
-						enabled: true
-						port: _sshPort
-					}
-				}
-				pluginManagement: {
-					plugins: [{
-						name: "gitiles"
-					}, {
-						name: "download-commands"
-					}, {
-						name: "singleusergroup"
-					}, {
-						name: "codemirror-editor"
-					}, {
-						name: "reviewnotes"
-					}, {
-						name: "oauth"
-						url: "https://drive.google.com/uc?export=download&id=1rSUpZCAVvHZTmRgUl4enrsAM73gndjeP"
-						sha1: "cbdc5228a18b051a6e048a8e783e556394cc5db1"
-					}, {
-						name: "webhooks"
-					}]
-					libs: []
-					cache: enabled: false
-				}
-				etc: {
-					secret: {
-						ssh_host_ecdsa_key: input.key.private
-						"ssh_host_ecdsa_key.pub": input.key.public
-					}
-					existingConfigMapName: _gerritConfigMapName
-				}
-			}
-		}
-	}
-}
diff --git a/core/installer/values-tmpl/headscale-controller.cue b/core/installer/values-tmpl/headscale-controller.cue
index a2e3f22..7d61ac2 100644
--- a/core/installer/values-tmpl/headscale-controller.cue
+++ b/core/installer/values-tmpl/headscale-controller.cue
@@ -3,46 +3,48 @@
 name: "headscale-controller"
 namespace: "core-headscale"
 
-images: {
-	headscaleController: {
-		repository: "giolekva"
-		name: "headscale-controller"
-		tag: "latest"
-		pullPolicy: "Always"
+out: {
+	images: {
+		headscaleController: {
+			repository: "giolekva"
+			name: "headscale-controller"
+			tag: "latest"
+			pullPolicy: "Always"
+		}
+		kubeRBACProxy: {
+			registry: "gcr.io"
+			repository: "kubebuilder"
+			name: "kube-rbac-proxy"
+			tag: "v0.13.0"
+			pullPolicy: "IfNotPresent"
+		}
 	}
-	kubeRBACProxy: {
-		registry: "gcr.io"
-		repository: "kubebuilder"
-		name: "kube-rbac-proxy"
-		tag: "v0.13.0"
-		pullPolicy: "IfNotPresent"
-	}
-}
 
-charts: {
-	headscaleController: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/headscale-controller"
+	charts: {
+		headscaleController: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/headscale-controller"
+		}
 	}
-}
 
-helm: {
-	"headscale-controller": {
-		chart: charts.headscaleController
-		values: {
-			installCRDs: true
-			image: {
-				repository: images.headscaleController.fullName
-				tag: images.headscaleController.tag
-				pullPolicy: images.headscaleController.pullPolicy
-			}
-			kubeRBACProxy: {
+	helm: {
+		"headscale-controller": {
+			chart: charts.headscaleController
+			values: {
+				installCRDs: true
 				image: {
-					repository: images.kubeRBACProxy.fullName
-					tag: images.kubeRBACProxy.tag
-					pullPolicy: images.kubeRBACProxy.pullPolicy
+					repository: images.headscaleController.fullName
+					tag: images.headscaleController.tag
+					pullPolicy: images.headscaleController.pullPolicy
+				}
+				kubeRBACProxy: {
+					image: {
+						repository: images.kubeRBACProxy.fullName
+						tag: images.kubeRBACProxy.tag
+						pullPolicy: images.kubeRBACProxy.pullPolicy
+					}
 				}
 			}
 		}
diff --git a/core/installer/values-tmpl/headscale-user.cue b/core/installer/values-tmpl/headscale-user.cue
new file mode 100644
index 0000000..036d0a3
--- /dev/null
+++ b/core/installer/values-tmpl/headscale-user.cue
@@ -0,0 +1,34 @@
+input: {
+	username: string
+	preAuthKey: {
+		enabled: bool | *false
+	}
+}
+
+name: "headscale-user"
+namespace: "app-headscale"
+
+out: {
+	charts: {
+		headscaleUser: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/headscale-user"
+		}
+	}
+
+	helm: {
+		"headscale-user-\(input.username)": {
+			chart: charts.headscaleUser
+			values: {
+				username: input.username
+				headscaleApiAddress: "http://headscale-api.\(global.namespacePrefix)app-headscale.svc.cluster.local"
+				preAuthKey: {
+					enabled: input.preAuthKey.enabled
+					secretName: "\(input.username)-headscale-preauthkey"
+				}
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/headscale.cue b/core/installer/values-tmpl/headscale.cue
index e13c1f0..fdbcb5a 100644
--- a/core/installer/values-tmpl/headscale.cue
+++ b/core/installer/values-tmpl/headscale.cue
@@ -8,89 +8,92 @@
 namespace: "app-headscale"
 icon: "<svg xmlns='http://www.w3.org/2000/svg' width='50' height='50' viewBox='0 0 48 48'><circle cx='24' cy='24' r='4.5' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/><circle cx='38' cy='24' r='4.5' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/><circle cx='38' cy='10' r='4.5' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/><circle cx='24' cy='10' r='4.5' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/><circle cx='10' cy='10' r='4.5' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/><circle cx='10' cy='24' r='4.5' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/><circle cx='10' cy='38' r='4.5' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/><circle cx='24' cy='38' r='4.5' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/><circle cx='38' cy='38' r='4.5' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/><circle cx='24' cy='38' r='2' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/><circle cx='24' cy='24' r='2' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/><circle cx='10' cy='24' r='2' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/><circle cx='38' cy='24' r='2' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/></svg>"
 
-images: {
-	headscale: {
-		repository: "headscale"
-		name: "headscale"
-		tag: "0.22.3"
-		pullPolicy: "IfNotPresent"
-	}
-	api: {
-		repository: "giolekva"
-		name: "headscale-api"
-		tag: "latest"
-		pullPolicy: "Always"
-	}
-}
-
-charts: {
-	oauth2Client: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/oauth2-client"
-	}
-	headscale: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/headscale"
-	}
-}
-
 _domain: "\(input.subdomain).\(input.network.domain)"
 _oauth2ClientSecretName: "oauth2-client"
 
-helm: {
-	"oauth2-client": {
-		chart: charts.oauth2Client
-		// TODO(gio): remove once hydra maester is installed as part of dodo itself
-		dependsOn: [{
-			name: "auth"
-			namespace: "\(global.namespacePrefix)core-auth"
-		}]
-		values: {
-			name: "\(release.namespace)-headscale"
-			secretName: _oauth2ClientSecretName
-			grantTypes: ["authorization_code"]
-			responseTypes: ["code"]
-			scope: "openid profile email"
-			redirectUris: ["https://\(_domain)/oidc/callback"]
-			hydraAdmin: "http://hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
+out: {
+	images: {
+		headscale: {
+			repository: "headscale"
+			name: "headscale"
+			tag: "0.22.3"
+			pullPolicy: "IfNotPresent"
+		}
+		api: {
+			repository: "giolekva"
+			name: "headscale-api"
+			tag: "latest"
+			pullPolicy: "Always"
 		}
 	}
-	headscale: {
-		chart: charts.headscale
-		dependsOn: [{
-			name: "auth"
-			namespace: "\(global.namespacePrefix)core-auth"
-		}]
-		values: {
-			image: {
-				repository: images.headscale.fullName
-				tag: images.headscale.tag
-				pullPolicy: images.headscale.pullPolicy
-			}
-			storage: size: "5Gi"
-			ingressClassName: input.network.ingressClass
-			certificateIssuer: input.network.certificateIssuer
-			domain: _domain
-			publicBaseDomain: input.network.domain
-			ipAddressPool: "\(global.id)-headscale"
-			oauth2: {
+
+	charts: {
+		oauth2Client: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/oauth2-client"
+		}
+		headscale: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/headscale"
+		}
+	}
+
+	helm: {
+		"oauth2-client": {
+			chart: charts.oauth2Client
+			// TODO(gio): remove once hydra maester is installed as part of dodo itself
+			dependsOn: [{
+				name: "auth"
+				namespace: "\(global.namespacePrefix)core-auth"
+			}]
+			values: {
+				name: "\(release.namespace)-headscale"
 				secretName: _oauth2ClientSecretName
-				issuer: "https://hydra.\(input.network.domain)"
+				grantTypes: ["authorization_code"]
+				responseTypes: ["code"]
+				scope: "openid profile email"
+				redirectUris: ["https://\(_domain)/oidc/callback"]
+				hydraAdmin: "http://hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
 			}
-			api: {
-				port: 8585
-				ipSubnet: input.ipSubnet
+		}
+		headscale: {
+			chart: charts.headscale
+			dependsOn: [{
+				name: "auth"
+				namespace: "\(global.namespacePrefix)core-auth"
+			}]
+			values: {
 				image: {
-					repository: images.api.fullName
-					tag: images.api.tag
-					pullPolicy: images.api.pullPolicy
+					repository: images.headscale.fullName
+					tag: images.headscale.tag
+					pullPolicy: images.headscale.pullPolicy
 				}
+				storage: size: "5Gi"
+				ingressClassName: input.network.ingressClass
+				certificateIssuer: input.network.certificateIssuer
+				domain: _domain
+				publicBaseDomain: input.network.domain
+				ipAddressPool: "\(global.id)-headscale"
+				oauth2: {
+					secretName: _oauth2ClientSecretName
+					issuer: "https://hydra.\(input.network.domain)"
+				}
+				api: {
+					port: 8585
+					ipSubnet: input.ipSubnet
+					self: "http://headscale-api.\(release.namespace).svc.cluster/sync-users"
+					image: {
+						repository: images.api.fullName
+						tag: images.api.tag
+						pullPolicy: images.api.pullPolicy
+					}
+				}
+				ui: enabled: false
 			}
-			ui: enabled: false
 		}
 	}
 }
diff --git a/core/installer/values-tmpl/hydra-maester.cue b/core/installer/values-tmpl/hydra-maester.cue
index 406dab3..22ba9b5 100644
--- a/core/installer/values-tmpl/hydra-maester.cue
+++ b/core/installer/values-tmpl/hydra-maester.cue
@@ -3,37 +3,39 @@
 name: "hydra-maester"
 namespace: "auth"
 
-images: {
-	hydraMaester: {
-		repository: "giolekva"
-		name: "ory-hydra-maester"
-		tag: "latest"
-		pullPolicy: "Always"
+out: {
+	images: {
+		hydraMaester: {
+			repository: "giolekva"
+			name: "ory-hydra-maester"
+			tag: "latest"
+			pullPolicy: "Always"
+		}
 	}
-}
 
-charts: {
-	hydraMaester: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/hydra-maester"
+	charts: {
+		hydraMaester: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/hydra-maester"
+		}
 	}
-}
 
-helm: {
-	"hydra-maester": {
-		chart: charts.hydraMaester
-		values: {
-			adminService: {
-				name: "foo.bar.svc.cluster.local"
-				port: 80
-				scheme: "http"
-			}
-			image: {
-				repository: images.hydraMaester.fullName
-				tag: images.hydraMaester.tag
-				pullPolicy: images.hydraMaester.pullPolicy
+	helm: {
+		"hydra-maester": {
+			chart: charts.hydraMaester
+			values: {
+				adminService: {
+					name: "foo.bar.svc.cluster.local"
+					port: 80
+					scheme: "http"
+				}
+				image: {
+					repository: images.hydraMaester.fullName
+					tag: images.hydraMaester.tag
+					pullPolicy: images.hydraMaester.pullPolicy
+				}
 			}
 		}
 	}
diff --git a/core/installer/values-tmpl/ingress-public.cue b/core/installer/values-tmpl/ingress-public.cue
index abb2439..fe6098e 100644
--- a/core/installer/values-tmpl/ingress-public.cue
+++ b/core/installer/values-tmpl/ingress-public.cue
@@ -9,116 +9,118 @@
 name: "ingress-public"
 namespace: "ingress-public"
 
-images: {
-	ingressNginx: {
-		registry: "registry.k8s.io"
-		repository: "ingress-nginx"
-		name: "controller"
-		tag: "v1.8.0"
-		pullPolicy: "IfNotPresent"
-	}
-	portAllocator: {
-		repository: "giolekva"
-		name: "port-allocator"
-		tag: "latest"
-		pullPolicy: "Always"
-	}
-}
-
-charts: {
-	ingressNginx: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/ingress-nginx"
-	}
-	portAllocator: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/port-allocator"
-	}
-}
-
-helm: {
-	"ingress-public": {
-		chart: charts.ingressNginx
-		values: {
-			fullnameOverride: "\(global.pcloudEnvName)-ingress-public"
-			controller: {
-				kind: "Deployment"
-				replicaCount: 1 // TODO(gio): configurable
-				topologySpreadConstraints: [{
-					labelSelector: {
-						matchLabels: {
-							"app.kubernetes.io/instance": "ingress-public"
-						}
-					}
-					maxSkew: 1
-					topologyKey: "kubernetes.io/hostname"
-					whenUnsatisfiable: "DoNotSchedule"
-				}]
-				hostNetwork: false
-				hostPort: enabled: false
-				updateStrategy: {
-					type: "RollingUpdate"
-					rollingUpdate: {
-						maxSurge: "100%"
-						maxUnavailable: "30%"
-					}
-				}
-				service: {
-					enabled: true
-					type: "NodePort"
-					nodePorts: {
-						http: 80
-						https: 443
-						tcp: {
-							"53": 53
-						}
-						udp: {
-							"53": 53
-						}
-					}
-				}
-				ingressClassByName: true
-				ingressClassResource: {
-					name: networks.public.ingressClass
-					enabled: true
-					default: false
-					controllerValue: "k8s.io/\(networks.public.ingressClass)"
-				}
-				config: {
-					"proxy-body-size": "200M" // TODO(giolekva): configurable
-					"server-snippet": """
-					more_clear_headers "X-Frame-Options";
-					"""
-				}
-				image: {
-					registry: images.ingressNginx.registry
-					image: images.ingressNginx.imageName
-					tag: images.ingressNginx.tag
-					pullPolicy: images.ingressNginx.pullPolicy
-				}
-			}
-			tcp: {
-				"53": "\(global.pcloudEnvName)-dns-gateway/coredns:53"
-			}
-			udp: {
-				"53": "\(global.pcloudEnvName)-dns-gateway/coredns:53"
-			}
+out: {
+	images: {
+		ingressNginx: {
+			registry: "registry.k8s.io"
+			repository: "ingress-nginx"
+			name: "controller"
+			tag: "v1.8.0"
+			pullPolicy: "IfNotPresent"
+		}
+		portAllocator: {
+			repository: "giolekva"
+			name: "port-allocator"
+			tag: "latest"
+			pullPolicy: "Always"
 		}
 	}
-	"port-allocator": {
-		chart: charts.portAllocator
-		values: {
-			repoAddr: release.repoAddr
-			sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
-			ingressNginxPath: "\(release.appDir)/ingress-public.yaml"
-			image: {
-				repository: images.portAllocator.fullName
-				tag: images.portAllocator.tag
-				pullPolicy: images.portAllocator.pullPolicy
+
+	charts: {
+		ingressNginx: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/ingress-nginx"
+		}
+		portAllocator: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/port-allocator"
+		}
+	}
+
+	helm: {
+		"ingress-public": {
+			chart: charts.ingressNginx
+			values: {
+				fullnameOverride: "\(global.pcloudEnvName)-ingress-public"
+				controller: {
+					kind: "Deployment"
+					replicaCount: 1 // TODO(gio): configurable
+					topologySpreadConstraints: [{
+						labelSelector: {
+							matchLabels: {
+								"app.kubernetes.io/instance": "ingress-public"
+							}
+						}
+						maxSkew: 1
+						topologyKey: "kubernetes.io/hostname"
+						whenUnsatisfiable: "DoNotSchedule"
+					}]
+					hostNetwork: false
+					hostPort: enabled: false
+					updateStrategy: {
+						type: "RollingUpdate"
+						rollingUpdate: {
+							maxSurge: "100%"
+							maxUnavailable: "30%"
+						}
+					}
+					service: {
+						enabled: true
+						type: "NodePort"
+						nodePorts: {
+							http: 80
+							https: 443
+							tcp: {
+								"53": 53
+							}
+							udp: {
+								"53": 53
+							}
+						}
+					}
+					ingressClassByName: true
+					ingressClassResource: {
+						name: networks.public.ingressClass
+						enabled: true
+						default: false
+						controllerValue: "k8s.io/\(networks.public.ingressClass)"
+					}
+					config: {
+						"proxy-body-size": "200M" // TODO(giolekva): configurable
+						"server-snippet": """
+						more_clear_headers "X-Frame-Options";
+						"""
+					}
+					image: {
+						registry: images.ingressNginx.registry
+						image: images.ingressNginx.imageName
+						tag: images.ingressNginx.tag
+						pullPolicy: images.ingressNginx.pullPolicy
+					}
+				}
+				tcp: {
+					"53": "\(global.pcloudEnvName)-dns-gateway/coredns:53"
+				}
+				udp: {
+					"53": "\(global.pcloudEnvName)-dns-gateway/coredns:53"
+				}
+			}
+		}
+		"port-allocator": {
+			chart: charts.portAllocator
+			values: {
+				repoAddr: release.repoAddr
+				sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
+				ingressNginxPath: "\(release.appDir)/ingress-public.yaml"
+				image: {
+					repository: images.portAllocator.fullName
+					tag: images.portAllocator.tag
+					pullPolicy: images.portAllocator.pullPolicy
+				}
 			}
 		}
 	}
diff --git a/core/installer/values-tmpl/jellyfin.cue b/core/installer/values-tmpl/jellyfin.cue
index 4df8248..3fecee0 100644
--- a/core/installer/values-tmpl/jellyfin.cue
+++ b/core/installer/values-tmpl/jellyfin.cue
@@ -13,37 +13,39 @@
 
 readme: "jellyfin application will be installed on \(input.network.name) network and be accessible to any user on https://\(_domain)"
 
-images: {
-	jellyfin: {
-		repository: "jellyfin"
-		name: "jellyfin"
-		tag: "10.8.10"
-		pullPolicy: "IfNotPresent"
+out: {
+	images: {
+		jellyfin: {
+			repository: "jellyfin"
+			name: "jellyfin"
+			tag: "10.8.10"
+			pullPolicy: "IfNotPresent"
+		}
 	}
-}
 
-charts: {
-	jellyfin: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/jellyfin"
+	charts: {
+		jellyfin: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/jellyfin"
+		}
 	}
-}
 
-helm: {
-	jellyfin: {
-		chart: charts.jellyfin
-		values: {
-			pcloudInstanceId: global.id
-			ingress: {
-				className: input.network.ingressClass
-				domain: _domain
-			}
-			image: {
-				repository: images.jellyfin.fullName
-				tag: images.jellyfin.tag
-				pullPolicy: images.jellyfin.pullPolicy
+	helm: {
+		jellyfin: {
+			chart: charts.jellyfin
+			values: {
+				pcloudInstanceId: global.id
+				ingress: {
+					className: input.network.ingressClass
+					domain: _domain
+				}
+				image: {
+					repository: images.jellyfin.fullName
+					tag: images.jellyfin.tag
+					pullPolicy: images.jellyfin.pullPolicy
+				}
 			}
 		}
 	}
diff --git a/core/installer/values-tmpl/jenkins.cue b/core/installer/values-tmpl/jenkins.cue
index d2f3602..7da855d 100644
--- a/core/installer/values-tmpl/jenkins.cue
+++ b/core/installer/values-tmpl/jenkins.cue
@@ -32,114 +32,116 @@
 
 _jenkinsServiceHTTPPortNumber: 80
 
-ingress: {
-	jenkins: {
-		auth: enabled: false
-		network: input.network
-		subdomain: input.subdomain
-		service: {
+out: {
+	ingress: {
+		jenkins: {
+			auth: enabled: false
+			network: input.network
+			subdomain: input.subdomain
+			service: {
+				name: "jenkins"
+				port: number: _jenkinsServiceHTTPPortNumber
+			}
+		}
+	}
+
+	images: {
+		jenkins: {
+			repository: "jenkins"
 			name: "jenkins"
-			port: number: _jenkinsServiceHTTPPortNumber
+			tag: "2.452-jdk17"
+			pullPolicy: "IfNotPresent"
+		}
+	}
+
+	charts: {
+		jenkins: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/jenkins"
+		}
+		oauth2Client: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/oauth2-client"
+		}
+	}
+
+	volumes: jenkins: size: "10Gi"
+
+	helm: {
+		"oauth2-client": {
+			chart: charts.oauth2Client
+			info: "Creating OAuth2 client"
+			values: {
+				name: "\(release.namespace)-jenkins"
+				secretName: _oauth2ClientCredentials
+				grantTypes: ["authorization_code"]
+				scope: "openid profile email offline offline_access"
+				hydraAdmin: "http://hydra-admin.\(global.id)-core-auth.svc.cluster.local"
+				redirectUris: ["https://\(_domain)/securityRealm/finishLogin"]
+				tokenEndpointAuthMethod: "client_secret_post"
+			}
+		}
+		jenkins: {
+			chart: charts.jenkins
+			info: "Installing Jenkins server"
+			values: {
+				fullnameOverride: "jenkins"
+				controller: {
+					image: {
+						repository: images.jenkins.imageName
+						tag: images.jenkins.tag
+						pullPolicy: images.jenkins.pullPolicy
+					}
+					jenkinsUrlProtocol: "https://"
+					jenkinsUrl: _domain
+					sidecars: configAutoReload: enabled: false
+					ingress: enabled: false
+					servicePort: _jenkinsServiceHTTPPortNumber
+					installPlugins: [
+						"kubernetes:4203.v1dd44f5b_1cf9",
+						"workflow-aggregator:596.v8c21c963d92d",
+						"git:5.2.1",
+						"configuration-as-code:1775.v810dc950b_514",
+						"gerrit-code-review:0.4.9",
+						"oic-auth:4.239.v325750a_96f3b_",
+					]
+					additionalExistingSecrets: [{
+						name: _oauth2ClientCredentials
+						keyName: _oauth2ClientId
+					}, {
+						name: _oauth2ClientCredentials
+						keyName: _oauth2ClientSecret
+					}]
+					JCasC: {
+						defaultConfig: true
+						overwriteConfiguration: false
+						securityRealm: """
+	oic:
+	  clientId: "${\(_oauth2ClientCredentials)-\(_oauth2ClientId)}"
+	  clientSecret: "${\(_oauth2ClientCredentials)-\(_oauth2ClientSecret)}"
+	  wellKnownOpenIDConfigurationUrl: "https://hydra.\(networks.public.domain)/.well-known/openid-configuration"
+	  userNameField: "email"
+	"""
+					}
+				}
+				agent: {
+					runAsUser: 1000
+					runAsGroup: 1000
+					jenkinsUrl: "http://jenkins.\(release.namespace).svc.cluster.local"
+				}
+				persistence: {
+					enabled: true
+					existingClaim: volumes.jenkins.name
+				}
+			}
 		}
 	}
 }
 
-images: {
-    jenkins: {
-        repository: "jenkins"
-        name: "jenkins"
-        tag: "2.452-jdk17"
-        pullPolicy: "IfNotPresent"
-    }
-}
-
-charts: {
-    jenkins: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/jenkins"
-    }
-	oauth2Client: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/oauth2-client"
-	}
-}
-
-volumes: jenkins: size: "10Gi"
-
 _oauth2ClientCredentials:  "oauth2-credentials"
 _oauth2ClientId: "client_id"
 _oauth2ClientSecret: "client_secret"
-
-helm: {
-	"oauth2-client": {
-		chart: charts.oauth2Client
-		info: "Creating OAuth2 client"
-		values: {
-			name: "\(release.namespace)-jenkins"
-			secretName: _oauth2ClientCredentials
-			grantTypes: ["authorization_code"]
-			scope: "openid profile email offline offline_access"
-			hydraAdmin: "http://hydra-admin.\(global.id)-core-auth.svc.cluster.local"
-			redirectUris: ["https://\(_domain)/securityRealm/finishLogin"]
-			tokenEndpointAuthMethod: "client_secret_post"
-		}
-	}
-    jenkins: {
-        chart: charts.jenkins
-		info: "Installing Jenkins server"
-        values: {
-			fullnameOverride: "jenkins"
-			controller: {
-				image: {
-					repository: images.jenkins.imageName
-					tag: images.jenkins.tag
-					pullPolicy: images.jenkins.pullPolicy
-				}
-				jenkinsUrlProtocol: "https://"
-				jenkinsUrl: _domain
-				sidecars: configAutoReload: enabled: false
-				ingress: enabled: false
-				servicePort: _jenkinsServiceHTTPPortNumber
-				installPlugins: [
-					"kubernetes:4203.v1dd44f5b_1cf9",
-					"workflow-aggregator:596.v8c21c963d92d",
-					"git:5.2.1",
-					"configuration-as-code:1775.v810dc950b_514",
-					"gerrit-code-review:0.4.9",
-					"oic-auth:4.239.v325750a_96f3b_",
-				]
-				additionalExistingSecrets: [{
-					name: _oauth2ClientCredentials
-					keyName: _oauth2ClientId
-				}, {
-					name: _oauth2ClientCredentials
-					keyName: _oauth2ClientSecret
-				}]
-				JCasC: {
-					defaultConfig: true
-					overwriteConfiguration: false
-					securityRealm: """
-oic:
-  clientId: "${\(_oauth2ClientCredentials)-\(_oauth2ClientId)}"
-  clientSecret: "${\(_oauth2ClientCredentials)-\(_oauth2ClientSecret)}"
-  wellKnownOpenIDConfigurationUrl: "https://hydra.\(networks.public.domain)/.well-known/openid-configuration"
-  userNameField: "email"
-"""
-				}
-			}
-			agent: {
-				runAsUser: 1000
-				runAsGroup: 1000
-				jenkinsUrl: "http://jenkins.\(release.namespace).svc.cluster.local"
-			}
-			persistence: {
-				enabled: true
-				existingClaim: volumes.jenkins.name
-			}
-        }
-    }
-}
diff --git a/core/installer/values-tmpl/launcher.cue b/core/installer/values-tmpl/launcher.cue
index 4574b72..09c0ec3 100644
--- a/core/installer/values-tmpl/launcher.cue
+++ b/core/installer/values-tmpl/launcher.cue
@@ -19,51 +19,53 @@
 
 _httpPortName: "http"
 
-ingress: {
-	launcher: {
-		auth: enabled: true
-		network: input.network
-		subdomain: _subdomain
-		service: {
-			name: "launcher"
-			port: name: _httpPortName
+out: {
+	ingress: {
+		launcher: {
+			auth: enabled: true
+			network: input.network
+			subdomain: _subdomain
+			service: {
+				name: "launcher"
+				port: name: _httpPortName
+			}
 		}
 	}
-}
 
-images: {
-    launcher: {
-        repository: "giolekva"
-        name: "pcloud-installer"
-        tag: "latest"
-        pullPolicy: "Always"
-    }
-}
+	images: {
+		launcher: {
+			repository: "giolekva"
+			name: "pcloud-installer"
+			tag: "latest"
+			pullPolicy: "Always"
+		}
+	}
 
-charts: {
-    launcher: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/launcher"
-    }
-}
+	charts: {
+		launcher: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/launcher"
+		}
+	}
 
-helm: {
-    launcher: {
-        chart: charts.launcher
-        values: {
-            image: {
-                repository: images.launcher.fullName
-                tag: images.launcher.tag
-                pullPolicy: images.launcher.pullPolicy
-            }
-            portName: _httpPortName
-            repoAddr: input.repoAddr
-            sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
-            logoutUrl: "https://accounts-ui.\(networks.public.domain)/logout"
-			repoAddr: input.repoAddr
-			sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
-        }
-    }
+	helm: {
+		launcher: {
+			chart: charts.launcher
+			values: {
+				image: {
+					repository: images.launcher.fullName
+					tag: images.launcher.tag
+					pullPolicy: images.launcher.pullPolicy
+				}
+				portName: _httpPortName
+				repoAddr: input.repoAddr
+				sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
+				logoutUrl: "https://accounts-ui.\(networks.public.domain)/logout"
+				repoAddr: input.repoAddr
+				sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
+			}
+		}
+	}
 }
diff --git a/core/installer/values-tmpl/matrix.cue b/core/installer/values-tmpl/matrix.cue
index 4cb0f0e..8bf8426 100644
--- a/core/installer/values-tmpl/matrix.cue
+++ b/core/installer/values-tmpl/matrix.cue
@@ -29,131 +29,133 @@
   <path class='cls-1' d='m1.04503942.90944884v37.86613982h2.72503927v.90945071H0V0h3.77007869v.90944884H1.04503942Zm11.64590578,12.00472508v1.91314893h.05456692c.47654392-.69956134,1.10875881-1.27913948,1.84700726-1.69322862.71598361-.40511792,1.54771632-.60354293,2.48031496-.60354293.89291332,0,1.70811022.17692893,2.44889755.51921281.74078733.34393731,1.29803124.96236184,1.68661493,1.83212566.41999952-.61842453.99212662-1.16740212,1.70976444-1.64031434.71763782-.47456723,1.57086583-.71102334,2.55637717-.71102334.74905523,0,1.44188933.09259881,2.08346495.27614143.64157561.18188998,1.18393635.47291301,1.64196855.8763783.45637641.40511792.80858321.92433073,1.06818882,1.57252004.25133929.6481893.3803142,1.42700774.3803142,2.34307056v9.47149555h-3.88417161v-8.02133831c0-.4729138-.01653581-.92433073-.0529127-1.34267762-.02666609-.3797812-.12779852-.75060537-.2976383-1.09133833-.16496703-.31157689-.41647821-.56882971-.72425151-.74078733-.32078781-.1818892-.75566893-.27448879-1.29803124-.27448879-.54897601,0-.99212662.10582699-1.32779444.3125199-.33038665.20312114-.60355081.48709839-.79370003.82511744-.19910782.35594888-.32873086.74650374-.38196842,1.15086631-.06370056.42978918-.09685576.86355382-.09921329,1.29803124v7.88409548h-3.8858274v-7.93700819c0-.41999952-.00661369-.83173271-.0297632-1.24346433-.01353647-.38990201-.09350161-.7746348-.23645611-1.13763734-.13486952-.34292964-.3751576-.63417029-.68622041-.83173271-.32078781-.20669291-.78708634-.31417253-1.41212614-.31417253-.18354341,0-.42826743.03968532-.72590573.1223628-.2976383.08433012-.59527502.23645611-.87637751.46629853-.31383822.26829772-.56214032.60483444-.72590573.98385871-.19842501.42826743-.29763751.99212662-.29763751,1.68661335v8.21149541h-3.88417713v-14.16259852l3.66259868.00000079Zm25.94905485,25.86141789V.90944884h-2.72504056v-.90944884h3.77007988v39.68503937h-3.77007988v-.90944756h2.72504056Z'/>
 </svg>"""
 
-images: {
-	matrix: {
-		repository: "matrixdotorg"
-		name: "synapse"
-		tag: "v1.104.0"
-		pullPolicy: "IfNotPresent"
-	}
-	postgres: {
-		repository: "library"
-		name: "postgres"
-		tag: "15.3"
-		pullPolicy: "IfNotPresent"
-	}
-}
-
-charts: {
-	oauth2Client: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/oauth2-client"
-	}
-	matrix: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/matrix"
-	}
-	postgres: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/postgresql"
-	}
-}
-
-_oauth2ClientSecretName: "oauth2-client"
-
-helm: {
-	"oauth2-client": {
-		chart: charts.oauth2Client
-		info: "Creating OAuth2 client"
-		values: {
-			name: "\(release.namespace)-matrix"
-			secretName: _oauth2ClientSecretName
-			grantTypes: ["authorization_code"]
-			responseTypes: ["code"]
-			scope: "openid profile"
-			redirectUris: ["https://\(_domain)/_synapse/client/oidc/callback"]
-			hydraAdmin: "http://hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
+out: {
+	images: {
+		matrix: {
+			repository: "matrixdotorg"
+			name: "synapse"
+			tag: "v1.104.0"
+			pullPolicy: "IfNotPresent"
 		}
-	}
-	matrix: {
-		dependsOn: [{
+		postgres: {
+			repository: "library"
 			name: "postgres"
-			namespace: release.namespace
-		}]
-		chart: charts.matrix
-		info: "Installing Synapse server"
-		values: {
-			domain: input.network.domain
-			subdomain: input.subdomain
-			oauth2: {
-				secretName: "oauth2-client"
-				issuer: "https://hydra.\(input.network.domain)"
-			}
-			postgresql: {
-				host: "postgres"
-				port: 5432
-				database: "matrix"
-				user: "matrix"
-				password: "matrix"
-			}
-			certificateIssuer: input.network.certificateIssuer
-			ingressClassName: input.network.ingressClass
-			configMerge: {
-				configName: "config-to-merge"
-				fileName: "to-merge.yaml"
-			}
-			image: {
-				repository: images.matrix.fullName
-				tag: images.matrix.tag
-				pullPolicy: images.matrix.pullPolicy
-			}
+			tag: "15.3"
+			pullPolicy: "IfNotPresent"
 		}
 	}
-	postgres: {
-		chart: charts.postgres
-		info: "Installing PostgreSQL"
-		values: {
-			fullnameOverride: "postgres"
-			image: {
-				registry: images.postgres.registry
-				repository: images.postgres.imageName
-				tag: images.postgres.tag
-				pullPolicy: images.postgres.pullPolicy
+
+	charts: {
+		oauth2Client: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/oauth2-client"
+		}
+		matrix: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/matrix"
+		}
+		postgres: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/postgresql"
+		}
+	}
+
+	_oauth2ClientSecretName: "oauth2-client"
+
+	helm: {
+		"oauth2-client": {
+			chart: charts.oauth2Client
+			info: "Creating OAuth2 client"
+			values: {
+				name: "\(release.namespace)-matrix"
+				secretName: _oauth2ClientSecretName
+				grantTypes: ["authorization_code"]
+				responseTypes: ["code"]
+				scope: "openid profile"
+				redirectUris: ["https://\(_domain)/_synapse/client/oidc/callback"]
+				hydraAdmin: "http://hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
 			}
-			service: {
-				type: "ClusterIP"
-				port: 5432
+		}
+		matrix: {
+			dependsOn: [{
+				name: "postgres"
+				namespace: release.namespace
+			}]
+			chart: charts.matrix
+			info: "Installing Synapse server"
+			values: {
+				domain: input.network.domain
+				subdomain: input.subdomain
+				oauth2: {
+					secretName: "oauth2-client"
+					issuer: "https://hydra.\(input.network.domain)"
+				}
+				postgresql: {
+					host: "postgres"
+					port: 5432
+					database: "matrix"
+					user: "matrix"
+					password: "matrix"
+				}
+				certificateIssuer: input.network.certificateIssuer
+				ingressClassName: input.network.ingressClass
+				configMerge: {
+					configName: "config-to-merge"
+					fileName: "to-merge.yaml"
+				}
+				image: {
+					repository: images.matrix.fullName
+					tag: images.matrix.tag
+					pullPolicy: images.matrix.pullPolicy
+				}
 			}
-			primary: {
-				initdb: {
-					scripts: {
-						"init.sql": """
-						CREATE USER matrix WITH PASSWORD 'matrix';
-						CREATE DATABASE matrix WITH OWNER = matrix ENCODING = UTF8 LOCALE = 'C' TEMPLATE = template0;
-						"""
+		}
+		postgres: {
+			chart: charts.postgres
+			info: "Installing PostgreSQL"
+			values: {
+				fullnameOverride: "postgres"
+				image: {
+					registry: images.postgres.registry
+					repository: images.postgres.imageName
+					tag: images.postgres.tag
+					pullPolicy: images.postgres.pullPolicy
+				}
+				service: {
+					type: "ClusterIP"
+					port: 5432
+				}
+				primary: {
+					initdb: {
+						scripts: {
+							"init.sql": """
+							CREATE USER matrix WITH PASSWORD 'matrix';
+							CREATE DATABASE matrix WITH OWNER = matrix ENCODING = UTF8 LOCALE = 'C' TEMPLATE = template0;
+							"""
+						}
+					}
+					persistence: {
+						size: "10Gi"
+					}
+					securityContext: {
+						enabled: true
+						fsGroup: 0
+					}
+					containerSecurityContext: {
+						enabled: true
+						runAsUser: 0
 					}
 				}
-				persistence: {
-					size: "10Gi"
-				}
-				securityContext: {
-					enabled: true
-					fsGroup: 0
-				}
-				containerSecurityContext: {
-					enabled: true
-					runAsUser: 0
-				}
-			}
-			volumePermissions: {
-				securityContext: {
-					runAsUser: 0
+				volumePermissions: {
+					securityContext: {
+						runAsUser: 0
+					}
 				}
 			}
 		}
diff --git a/core/installer/values-tmpl/memberships.cue b/core/installer/values-tmpl/memberships.cue
index bf1424e..5fc55d1 100644
--- a/core/installer/values-tmpl/memberships.cue
+++ b/core/installer/values-tmpl/memberships.cue
@@ -15,52 +15,54 @@
 
 _httpPortName: "http"
 
-ingress: {
-	memberships: {
-		auth: {
-			enabled: true
-			groups: input.authGroups
-		}
-		network: input.network
-		subdomain: _subdomain
-		service: {
-			name: "memberships"
-			port: name: _httpPortName
+out: {
+	ingress: {
+		memberships: {
+			auth: {
+				enabled: true
+				groups: input.authGroups
+			}
+			network: input.network
+			subdomain: _subdomain
+			service: {
+				name: "memberships"
+				port: name: _httpPortName
+			}
 		}
 	}
-}
 
-images: {
-    memberships: {
-        repository: "giolekva"
-        name: "memberships"
-        tag: "latest"
-        pullPolicy: "Always"
-    }
-}
+	images: {
+		memberships: {
+			repository: "giolekva"
+			name: "memberships"
+			tag: "latest"
+			pullPolicy: "Always"
+		}
+	}
 
-charts: {
-    memberships: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/memberships"
-    }
-}
+	charts: {
+		memberships: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/memberships"
+		}
+	}
 
-helm: {
-    memberships: {
-        chart: charts.memberships
-        values: {
-            storage: {
-                size: "1Gi"
-            }
-            image: {
-                repository: images.memberships.fullName
-                tag: images.memberships.tag
-                pullPolicy: images.memberships.pullPolicy
-            }
-            portName: _httpPortName
-        }
-    }
+	helm: {
+		memberships: {
+			chart: charts.memberships
+			values: {
+				storage: {
+					size: "1Gi"
+				}
+				image: {
+					repository: images.memberships.fullName
+					tag: images.memberships.tag
+					pullPolicy: images.memberships.pullPolicy
+				}
+				portName: _httpPortName
+			}
+		}
+	}
 }
diff --git a/core/installer/values-tmpl/metallb-ipaddresspool.cue b/core/installer/values-tmpl/metallb-ipaddresspool.cue
index 47236ce..a0bb4e5 100644
--- a/core/installer/values-tmpl/metallb-ipaddresspool.cue
+++ b/core/installer/values-tmpl/metallb-ipaddresspool.cue
@@ -9,26 +9,26 @@
 name: "metallb-ipaddresspool"
 namespace: "metallb-ipaddresspool"
 
-images: {}
-
-charts: {
-	metallbIPAddressPool: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/metallb-ipaddresspool"
+out: {
+	charts: {
+		metallbIPAddressPool: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/metallb-ipaddresspool"
+		}
 	}
-}
 
-helm: {
-	"metallb-ipaddresspool-\(input.name)": {
-		chart: charts.metallbIPAddressPool
-		values: {
-			name: input.name
-			from: input.from
-			to: input.to
-			autoAssign: input.autoAssign
-			namespace: input.namespace
+	helm: {
+		"metallb-ipaddresspool-\(input.name)": {
+			chart: charts.metallbIPAddressPool
+			values: {
+				name: input.name
+				from: input.from
+				to: input.to
+				autoAssign: input.autoAssign
+				namespace: input.namespace
+			}
 		}
 	}
 }
diff --git a/core/installer/values-tmpl/open-project.cue b/core/installer/values-tmpl/open-project.cue
index 927db43..6598094 100644
--- a/core/installer/values-tmpl/open-project.cue
+++ b/core/installer/values-tmpl/open-project.cue
@@ -13,160 +13,163 @@
 icon: "<svg xmlns='http://www.w3.org/2000/svg' width='50' height='50' viewBox='0 0 24 24'><path fill='currentColor' d='M19.35.37h-1.86a4.63 4.63 0 0 0-4.652 4.624v5.609H4.652A4.63 4.63 0 0 0 0 15.23v3.721c0 2.569 2.083 4.679 4.652 4.679h1.86c2.57 0 4.652-2.11 4.652-4.679v-3.72c0-.063 0-.158-.005-.158H8.373v3.88c0 1.026-.835 1.886-1.861 1.886h-1.86c-1.027 0-1.861-.864-1.861-1.886V15.23a1.84 1.84 0 0 1 1.86-1.833h14.697c2.57 0 4.652-2.11 4.652-4.679V4.997A4.63 4.63 0 0 0 19.35.37m1.861 8.345c0 1.026-.835 1.886-1.861 1.886h-3.721V4.997a1.84 1.84 0 0 1 1.86-1.833h1.86a1.84 1.84 0 0 1 1.862 1.833zm-8.373 9.706v.03c0 .746.629 1.344 1.396 1.344s1.395-.594 1.395-1.34v-3.384h-2.791z'/></svg>"
 
 _httpPort: 8080
-ingress: {
-	gerrit: {
-		auth: enabled: false
-		network: input.network
-		subdomain: input.subdomain
-		service: {
-			name: "open-project"
-			port: number: _httpPort
+
+out: {
+	ingress: {
+		gerrit: {
+			auth: enabled: false
+			network: input.network
+			subdomain: input.subdomain
+			service: {
+				name: "open-project"
+				port: number: _httpPort
+			}
 		}
 	}
-}
 
-images: {
-	openProject: {
-		repository: "openproject"
-		name: "openproject"
-		tag: "13.4.1"
-		pullPolicy: "Always"
+	images: {
+		openProject: {
+			repository: "openproject"
+			name: "openproject"
+			tag: "13.4.1"
+			pullPolicy: "Always"
+		}
+		postgres: {
+			repository: "library"
+			name: "postgres"
+			tag: "15.3"
+			pullPolicy: "IfNotPresent"
+		}
 	}
-	postgres: {
-		repository: "library"
-		name: "postgres"
-		tag: "15.3"
-		pullPolicy: "IfNotPresent"
-	}
-}
 
-charts: {
-	openProject: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/openproject"
+	charts: {
+		openProject: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/openproject"
+		}
+		volume: {
+			path: "charts/volumes"
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+		}
+		postgres: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/postgresql"
+		}
 	}
-	volume: {
-		path: "charts/volumes"
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-	}
-	postgres: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/postgresql"
-	}
-}
 
-volumes: {
-	openProject: {
-		name: "open-project"
-		accessMode: "ReadWriteMany"
-		size: "50Gi"
+	volumes: {
+		openProject: {
+			name: "open-project"
+			accessMode: "ReadWriteMany"
+			size: "50Gi"
+		}
 	}
-}
 
-helm: {
-	"open-project": {
-		chart: charts.openProject
-		values: {
-			image: {
-				registry: images.openProject.registry
-				repository: images.openProject.imageName
-				tag: images.openProject.tag
-				imagePullPolicy: images.openProject.pullPolicy
-			}
-			nameOverride: "open-project"
-			ingress: enabled: false
-			memcached: bundled: true
-			s3: enabled: false
-			openproject: {
-				host: _domain
-				https: false
-				hsts: false
-				oidc: enabled: false // TODO(gio): enable
-				admin_user: {
-					password: "admin"
-					password_reset: false
-					name: "admin"
-					mail: "op-admin@\(networks.public.domain)"
+	helm: {
+		"open-project": {
+			chart: charts.openProject
+			values: {
+				image: {
+					registry: images.openProject.registry
+					repository: images.openProject.imageName
+					tag: images.openProject.tag
+					imagePullPolicy: images.openProject.pullPolicy
+				}
+				nameOverride: "open-project"
+				ingress: enabled: false
+				memcached: bundled: true
+				s3: enabled: false
+				openproject: {
+					host: _domain
+					https: false
+					hsts: false
+					oidc: enabled: false // TODO(gio): enable
+					admin_user: {
+						password: "admin"
+						password_reset: false
+						name: "admin"
+						mail: "op-admin@\(networks.public.domain)"
+					}
+				}
+				persistence: {
+					enabled: true
+					existingClaim: volumes.openProject.name
+				}
+				postgresql: {
+					bundled: false
+					connection: {
+						host: "postgres.\(release.namespace).svc.cluster.local"
+						port: 5432
+					}
+					auth: {
+						username: "openproject"
+						password: "openproject"
+						database: "openproject"
+					}
+				}
+				service: {
+					enabled: true
+					type: "ClusterIP"
+				}
+				initDb: {
+					image: {
+						registry: images.postgres.registry
+						repository: images.postgres.imageName
+						tag: images.postgres.tag
+						imagePullPolicy: images.postgres.pullPolicy
+					}
 				}
 			}
-			persistence: {
-				enabled: true
-				existingClaim: volumes.openProject.name
-			}
-			postgresql: {
-				bundled: false
-				connection: {
-					host: "postgres.\(release.namespace).svc.cluster.local"
-					port: 5432
-				}
-				auth: {
-					username: "openproject"
-					password: "openproject"
-					database: "openproject"
-				}
-			}
-			service: {
-				enabled: true
-				type: "ClusterIP"
-			}
-			initDb: {
+		}
+		"open-project-volume": {
+			chart: charts.volume
+			values: volumes.openProject
+		}
+		postgres: {
+			chart: charts.postgres
+			values: {
+				fullnameOverride: "postgres"
 				image: {
 					registry: images.postgres.registry
 					repository: images.postgres.imageName
 					tag: images.postgres.tag
-					imagePullPolicy: images.postgres.pullPolicy
+					pullPolicy: images.postgres.pullPolicy
 				}
-			}
-		}
-	}
-	"open-project-volume": {
-		chart: charts.volume
-		values: volumes.openProject
-	}
-	postgres: {
-		chart: charts.postgres
-		values: {
-			fullnameOverride: "postgres"
-			image: {
-				registry: images.postgres.registry
-				repository: images.postgres.imageName
-				tag: images.postgres.tag
-				pullPolicy: images.postgres.pullPolicy
-			}
-			service: {
-				type: "ClusterIP"
-				port: 5432
-			}
-			primary: {
-				initdb: {
-					scripts: {
-						"init.sql": """
-						CREATE USER openproject WITH PASSWORD 'openproject';
-						CREATE DATABASE openproject WITH OWNER = openproject ENCODING = UTF8 LOCALE = 'C' TEMPLATE = template0;
-						"""
+				service: {
+					type: "ClusterIP"
+					port: 5432
+				}
+				primary: {
+					initdb: {
+						scripts: {
+							"init.sql": """
+							CREATE USER openproject WITH PASSWORD 'openproject';
+							CREATE DATABASE openproject WITH OWNER = openproject ENCODING = UTF8 LOCALE = 'C' TEMPLATE = template0;
+							"""
+						}
+					}
+					persistence: {
+						size: "50Gi"
+					}
+					securityContext: {
+						enabled: true
+						fsGroup: 0
+					}
+					containerSecurityContext: {
+						enabled: true
+						runAsUser: 0
 					}
 				}
-				persistence: {
-					size: "50Gi"
-				}
-				securityContext: {
-					enabled: true
-					fsGroup: 0
-				}
-				containerSecurityContext: {
-					enabled: true
-					runAsUser: 0
-				}
-			}
-			volumePermissions: {
-				securityContext: {
-					runAsUser: 0
+				volumePermissions: {
+					securityContext: {
+						runAsUser: 0
+					}
 				}
 			}
 		}
diff --git a/core/installer/values-tmpl/penpot.cue b/core/installer/values-tmpl/penpot.cue
index 0e25a43..21004f4 100644
--- a/core/installer/values-tmpl/penpot.cue
+++ b/core/installer/values-tmpl/penpot.cue
@@ -12,165 +12,167 @@
 description: "Penpot is the first Open Source design and prototyping platform meant for cross-domain teams. Non dependent on operating systems, Penpot is web based and works with open standards (SVG). Penpot invites designers all over the world to fall in love with open source while getting developers excited about the design process in return."
 icon: "<svg xmlns='http://www.w3.org/2000/svg' width='50' height='50' viewBox='0 0 24 24'><path fill='currentColor' d='M7.654 0L5.13 3.554v2.01L2.934 6.608l-.02-.009v13.109l8.563 4.045L12 24l.523-.247l8.563-4.045V6.6l-.017.008l-2.196-1.045V3.555l-.077-.108L16.349.001l-2.524 3.554v.004L11.989.973l-1.823 2.566l-.065-.091zm.447 2.065l.976 1.374H6.232l.964-1.358zm8.694 0l.976 1.374h-2.845l.965-1.358zm-4.36.971l.976 1.375h-2.845l.965-1.359zM5.962 4.132h1.35v4.544l-1.35-.638Zm2.042 0h1.343v5.506l-1.343-.635zm6.652 0h1.35V9l-1.35.637zm2.042 0h1.343v3.905l-1.343.634zm-6.402.972h1.35v5.62l-1.35-.638zm2.042 0h1.343v4.993l-1.343.634zm6.534 1.493l1.188.486l-1.188.561zM5.13 6.6v1.047l-1.187-.561ZM3.96 8.251l7.517 3.55v10.795l-7.516-3.55zm16.08 0v10.794l-7.517 3.55V11.802z'/></svg>"
 
-images: {
-	postgres: {
-		repository: "library"
-		name: "postgres"
-		tag: "15.3"
-		pullPolicy: "IfNotPresent"
-	}
-	backend: {
-		repository: "penpotapp"
-		name: "backend"
-		tag: "1.16.0-beta"
-		pullPolicy: "IfNotPresent"
-	}
-	frontend: {
-		repository: "penpotapp"
-		name: "frontend"
-		tag: "1.16.0-beta"
-		pullPolicy: "IfNotPresent"
-	}
-	exporter: {
-		repository: "penpotapp"
-		name: "exporter"
-		tag: "1.16.0-beta"
-		pullPolicy: "IfNotPresent"
-	}
-}
-
-charts: {
-	postgres: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/postgresql"
-	}
-	oauth2Client: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/oauth2-client"
-	}
-	penpot: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/penpot"
-	}
-}
-
-_oauth2SecretName: "oauth2-credentials"
-
-helm: {
-	"oauth2-client": {
-		chart: charts.oauth2Client
-		values: {
-			name: "\(release.namespace)-penpot"
-			secretName: _oauth2SecretName
-			grantTypes: ["authorization_code"]
-			responseTypes: ["code"]
-			scope: "openid profile email"
-			redirectUris: ["https://\(_domain)/api/auth/oauth/oidc/callback"]
-			hydraAdmin: "http://hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
-			tokenEndpointAuthMethod: "client_secret_post"
+out: {
+	images: {
+		postgres: {
+			repository: "library"
+			name: "postgres"
+			tag: "15.3"
+			pullPolicy: "IfNotPresent"
+		}
+		backend: {
+			repository: "penpotapp"
+			name: "backend"
+			tag: "1.16.0-beta"
+			pullPolicy: "IfNotPresent"
+		}
+		frontend: {
+			repository: "penpotapp"
+			name: "frontend"
+			tag: "1.16.0-beta"
+			pullPolicy: "IfNotPresent"
+		}
+		exporter: {
+			repository: "penpotapp"
+			name: "exporter"
+			tag: "1.16.0-beta"
+			pullPolicy: "IfNotPresent"
 		}
 	}
-	postgres: {
-		chart: charts.postgres
-		values: {
-			fullnameOverride: "postgres"
-			image: {
-				registry: images.postgres.registry
-				repository: images.postgres.imageName
-				tag: images.postgres.tag
-				pullPolicy: images.postgres.pullPolicy
-			}
-			auth: {
-				username: "penpot"
-				password: "penpot"
-				database: "penpot"
-			}
+
+	charts: {
+		postgres: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/postgresql"
+		}
+		oauth2Client: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/oauth2-client"
+		}
+		penpot: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/penpot"
 		}
 	}
-	penpot: {
-		chart: charts.penpot
-		values: {
-			"global": {
-				postgresqlEnabled: false
-				redisEnabled: true // TODO(gio): provide redis from outside
+
+	_oauth2SecretName: "oauth2-credentials"
+
+	helm: {
+		"oauth2-client": {
+			chart: charts.oauth2Client
+			values: {
+				name: "\(release.namespace)-penpot"
+				secretName: _oauth2SecretName
+				grantTypes: ["authorization_code"]
+				responseTypes: ["code"]
+				scope: "openid profile email"
+				redirectUris: ["https://\(_domain)/api/auth/oauth/oidc/callback"]
+				hydraAdmin: "http://hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
+				tokenEndpointAuthMethod: "client_secret_post"
 			}
-			fullnameOverride: "penpot"
-			backend: {
+		}
+		postgres: {
+			chart: charts.postgres
+			values: {
+				fullnameOverride: "postgres"
 				image: {
-					repository: images.backend.fullName
-					tag: images.backend.tag
-					imagePullPolicy: images.backend.pullPolicy
+					registry: images.postgres.registry
+					repository: images.postgres.imageName
+					tag: images.postgres.tag
+					pullPolicy: images.postgres.pullPolicy
 				}
-			}
-			frontend: {
-				image: {
-					repository: images.frontend.fullName
-					tag: images.frontend.tag
-					imagePullPolicy: images.frontend.pullPolicy
-				}
-				ingress: {
-					enabled: true
-					className: input.network.ingressClass
-					if input.network.certificateIssuer != "" {
-						annotations: {
-							"acme.cert-manager.io/http01-edit-in-place": "true"
-							"cert-manager.io/cluster-issuer": input.network.certificateIssuer
-						}
-					}
-					hosts: [_domain]
-					tls: [{
-						hosts: [_domain]
-						secretName: "cert-\(_domain)"
-					}]
-				}
-			}
-			persistence: enabled: true
-			config: {
-				publicURI: _domain
-				flags: "enable-login-with-oidc enable-registration enable-insecure-register disable-demo-users disable-demo-warning" // TODO(gio): remove enable-insecure-register?
-				postgresql: {
-					host: "postgres.\(release.namespace).svc.cluster.local"
-					database: "penpot"
+				auth: {
 					username: "penpot"
 					password: "penpot"
+					database: "penpot"
 				}
-				redis: host: "penpot-redis-headless.\(release.namespace).svc.cluster.local"
-				providers: {
-					oidc: {
+			}
+		}
+		penpot: {
+			chart: charts.penpot
+			values: {
+				"global": {
+					postgresqlEnabled: false
+					redisEnabled: true // TODO(gio): provide redis from outside
+				}
+				fullnameOverride: "penpot"
+				backend: {
+					image: {
+						repository: images.backend.fullName
+						tag: images.backend.tag
+						imagePullPolicy: images.backend.pullPolicy
+					}
+				}
+				frontend: {
+					image: {
+						repository: images.frontend.fullName
+						tag: images.frontend.tag
+						imagePullPolicy: images.frontend.pullPolicy
+					}
+					ingress: {
 						enabled: true
-						baseURI: "https://hydra.\(networks.public.domain)"
-						clientID: ""
-						clientSecret: ""
-						authURI: ""
-						tokenURI: ""
-						userURI: ""
-						roles: ""
-						rolesAttribute: ""
-						scopes: ""
-						nameAttribute: "name"
-						emailAttribute: "email"
-					}
-					existingSecret: _oauth2SecretName
-					secretKeys: {
-						oidcClientIDKey: "client_id"
-						oidcClientSecretKey: "client_secret"
+						className: input.network.ingressClass
+						if input.network.certificateIssuer != "" {
+							annotations: {
+								"acme.cert-manager.io/http01-edit-in-place": "true"
+								"cert-manager.io/cluster-issuer": input.network.certificateIssuer
+							}
+						}
+						hosts: [_domain]
+						tls: [{
+							hosts: [_domain]
+							secretName: "cert-\(_domain)"
+						}]
 					}
 				}
-			}
-			exporter: {
-				image: {
-					repository: images.exporter.fullName
-					tag: images.exporter.tag
-					imagePullPolicy: images.exporter.pullPolicy
+				persistence: enabled: true
+				config: {
+					publicURI: _domain
+					flags: "enable-login-with-oidc enable-registration enable-insecure-register disable-demo-users disable-demo-warning" // TODO(gio): remove enable-insecure-register?
+					postgresql: {
+						host: "postgres.\(release.namespace).svc.cluster.local"
+						database: "penpot"
+						username: "penpot"
+						password: "penpot"
+					}
+					redis: host: "penpot-redis-headless.\(release.namespace).svc.cluster.local"
+					providers: {
+						oidc: {
+							enabled: true
+							baseURI: "https://hydra.\(networks.public.domain)"
+							clientID: ""
+							clientSecret: ""
+							authURI: ""
+							tokenURI: ""
+							userURI: ""
+							roles: ""
+							rolesAttribute: ""
+							scopes: ""
+							nameAttribute: "name"
+							emailAttribute: "email"
+						}
+						existingSecret: _oauth2SecretName
+						secretKeys: {
+							oidcClientIDKey: "client_id"
+							oidcClientSecretKey: "client_secret"
+						}
+					}
 				}
+				exporter: {
+					image: {
+						repository: images.exporter.fullName
+						tag: images.exporter.tag
+						imagePullPolicy: images.exporter.pullPolicy
+					}
+				}
+				redis: image: tag: "7.0.8-debian-11-r16"
 			}
-			redis: image: tag: "7.0.8-debian-11-r16"
 		}
 	}
 }
diff --git a/core/installer/values-tmpl/pihole.cue b/core/installer/values-tmpl/pihole.cue
index 59cab5d..01a3ca1 100644
--- a/core/installer/values-tmpl/pihole.cue
+++ b/core/installer/values-tmpl/pihole.cue
@@ -2,6 +2,7 @@
 	network: #Network @name(Network)
 	subdomain: string @name(Subdomain)
 	auth: #Auth @name(Authentication)
+	storageSize: string
 }
 
 _domain: "\(input.subdomain).\(input.network.domain)"
@@ -33,83 +34,87 @@
 
 _serviceWebPort: 80
 
-ingress: {
-	pihole: {
-		auth: input.auth
-		network: input.network
-		subdomain: input.subdomain
-		service: {
-			name: "pihole-web"
-			port: number: _serviceWebPort
+out: {
+	ingress: {
+		pihole: {
+			auth: input.auth
+			network: input.network
+			subdomain: input.subdomain
+			service: {
+				name: "pihole-web"
+				port: number: _serviceWebPort
+			}
 		}
 	}
-}
 
-images: {
-	pihole: {
-		repository: "pihole"
-		name: "pihole"
-		tag: "v5.8.1"
-		pullPolicy: "IfNotPresent"
+	images: {
+		pihole: {
+			repository: "pihole"
+			name: "pihole"
+			tag: "v5.8.1"
+			pullPolicy: "IfNotPresent"
+		}
 	}
-}
 
-charts: {
-	pihole: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/pihole"
+	charts: {
+		pihole: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/pihole"
+		}
 	}
-}
 
-helm: {
-	pihole: {
-		chart: charts.pihole
-		info: "Installing Pi-hole server"
-		values: {
-			fullnameOverride: "pihole"
-			persistentVolumeClaim: { // TODO(gio): create volume separately as a dependency
-				enabled: true
-				size: "5Gi"
-			}
-			admin: {
-				enabled: false
-			}
-			ingress: {
-				enabled: false
-			}
-			serviceDhcp: {
-				enabled: false
-			}
-			serviceDns: {
-				type: "ClusterIP"
-			}
-			serviceWeb: {
-				type: "ClusterIP"
-				http: {
+	volumes: data: size: input.storageSize
+
+	helm: {
+		pihole: {
+			chart: charts.pihole
+			info: "Installing Pi-hole server"
+			values: {
+				fullnameOverride: "pihole"
+				persistentVolumeClaim: {
 					enabled: true
-					port: _serviceWebPort
+					existingClaim: volumes.data.name
 				}
-				https: {
+				admin: {
 					enabled: false
 				}
-			}
-			virtualHost: _domain
-			resources: {
-				requests: {
-					cpu: "250m"
-					memory: "100M"
+				ingress: {
+					enabled: false
 				}
-				limits: {
-					cpu: "500m"
-					memory: "250M"
+				serviceDhcp: {
+					enabled: false
 				}
-			}
-			image: {
-				repository: images.pihole.fullName
-				tag: images.pihole.tag
-				pullPolicy: images.pihole.pullPolicy
+				serviceDns: {
+					type: "ClusterIP"
+				}
+				serviceWeb: {
+					type: "ClusterIP"
+					http: {
+						enabled: true
+						port: _serviceWebPort
+					}
+					https: {
+						enabled: false
+					}
+				}
+				virtualHost: _domain
+				resources: {
+					requests: {
+						cpu: "250m"
+						memory: "100M"
+					}
+					limits: {
+						cpu: "500m"
+						memory: "250M"
+					}
+				}
+				image: {
+					repository: images.pihole.fullName
+					tag: images.pihole.tag
+					pullPolicy: images.pihole.pullPolicy
+				}
 			}
 		}
 	}
diff --git a/core/installer/values-tmpl/private-network.cue b/core/installer/values-tmpl/private-network.cue
index 255b375..653da67 100644
--- a/core/installer/values-tmpl/private-network.cue
+++ b/core/installer/values-tmpl/private-network.cue
@@ -14,119 +14,121 @@
 name: "private-network"
 namespace: "ingress-private"
 
-images: {
-	"ingress-nginx": {
-		registry: "registry.k8s.io"
-		repository: "ingress-nginx"
-		name: "controller"
-		tag: "v1.8.0"
-		pullPolicy: "IfNotPresent"
+out: {
+	images: {
+		"ingress-nginx": {
+			registry: "registry.k8s.io"
+			repository: "ingress-nginx"
+			name: "controller"
+			tag: "v1.8.0"
+			pullPolicy: "IfNotPresent"
+		}
+		"tailscale-proxy": {
+			repository: "tailscale"
+			name: "tailscale"
+			tag: "v1.42.0"
+			pullPolicy: "IfNotPresent"
+		}
+		portAllocator: {
+			repository: "giolekva"
+			name: "port-allocator"
+			tag: "latest"
+			pullPolicy: "Always"
+		}
 	}
-	"tailscale-proxy": {
-		repository: "tailscale"
-		name: "tailscale"
-		tag: "v1.42.0"
-		pullPolicy: "IfNotPresent"
-	}
-	portAllocator: {
-		repository: "giolekva"
-		name: "port-allocator"
-		tag: "latest"
-		pullPolicy: "Always"
-	}
-}
 
-charts: {
-	"ingress-nginx": {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/ingress-nginx"
+	charts: {
+		"ingress-nginx": {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/ingress-nginx"
+		}
+		"tailscale-proxy": {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/tailscale-proxy"
+		}
+		portAllocator: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/port-allocator"
+		}
 	}
-	"tailscale-proxy": {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/tailscale-proxy"
-	}
-	portAllocator: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/port-allocator"
-	}
-}
 
-_ingressPrivate: "\(global.id)-ingress-private"
+	_ingressPrivate: "\(global.id)-ingress-private"
 
-helm: {
-	"ingress-nginx": {
-		chart: charts["ingress-nginx"]
-		values: {
-			fullnameOverride: "\(global.id)-nginx-private"
-			controller: {
-				service: {
-					enabled: true
-					type: "LoadBalancer"
-					annotations: {
-						"metallb.universe.tf/address-pool": _ingressPrivate
+	helm: {
+		"ingress-nginx": {
+			chart: charts["ingress-nginx"]
+			values: {
+				fullnameOverride: "\(global.id)-nginx-private"
+				controller: {
+					service: {
+						enabled: true
+						type: "LoadBalancer"
+						annotations: {
+							"metallb.universe.tf/address-pool": _ingressPrivate
+						}
+					}
+					ingressClassByName: true
+					ingressClassResource: {
+						name: _ingressPrivate
+						enabled: true
+						default: false
+						controllerValue: "k8s.io/\(_ingressPrivate)"
+					}
+					config: {
+						"proxy-body-size": "200M" // TODO(giolekva): configurable
+						"force-ssl-redirect": "true"
+						"server-snippet": """
+						more_clear_headers "X-Frame-Options";
+						"""
+					}
+					extraArgs: {
+						"default-ssl-certificate": "\(_ingressPrivate)/cert-wildcard.\(global.privateDomain)"
+					}
+					admissionWebhooks: {
+						enabled: false
+					}
+					image: {
+						registry: images["ingress-nginx"].registry
+						image: images["ingress-nginx"].imageName
+						tag: images["ingress-nginx"].tag
+						pullPolicy: images["ingress-nginx"].pullPolicy
 					}
 				}
-				ingressClassByName: true
-				ingressClassResource: {
-					name: _ingressPrivate
-					enabled: true
-					default: false
-					controllerValue: "k8s.io/\(_ingressPrivate)"
-				}
-				config: {
-					"proxy-body-size": "200M" // TODO(giolekva): configurable
-					"force-ssl-redirect": "true"
-					"server-snippet": """
-					more_clear_headers "X-Frame-Options";
-					"""
-				}
-				extraArgs: {
-					"default-ssl-certificate": "\(_ingressPrivate)/cert-wildcard.\(global.privateDomain)"
-				}
-				admissionWebhooks: {
-					enabled: false
-				}
+			}
+		}
+		"tailscale-proxy": {
+			chart: charts["tailscale-proxy"]
+			values: {
+				hostname: input.privateNetwork.hostname
+				apiServer: "http://headscale-api.\(global.namespacePrefix)app-headscale.svc.cluster.local"
+				loginServer: "https://headscale.\(networks.public.domain)" // TODO(gio): take headscale subdomain from configuration
+				ipSubnet: input.privateNetwork.ipSubnet
+				username: input.privateNetwork.username // TODO(gio): maybe install headscale-user chart separately?
+				preAuthKeySecret: "headscale-preauth-key"
 				image: {
-					registry: images["ingress-nginx"].registry
-					image: images["ingress-nginx"].imageName
-					tag: images["ingress-nginx"].tag
-					pullPolicy: images["ingress-nginx"].pullPolicy
+					repository: images["tailscale-proxy"].fullName
+					tag: images["tailscale-proxy"].tag
+					pullPolicy: images["tailscale-proxy"].pullPolicy
 				}
 			}
 		}
-	}
-	"tailscale-proxy": {
-		chart: charts["tailscale-proxy"]
-		values: {
-			hostname: input.privateNetwork.hostname
-			apiServer: "http://headscale-api.\(global.namespacePrefix)app-headscale.svc.cluster.local"
-			loginServer: "https://headscale.\(networks.public.domain)" // TODO(gio): take headscale subdomain from configuration
-			ipSubnet: input.privateNetwork.ipSubnet
-			username: input.privateNetwork.username // TODO(gio): maybe install headscale-user chart separately?
-			preAuthKeySecret: "headscale-preauth-key"
-			image: {
-				repository: images["tailscale-proxy"].fullName
-				tag: images["tailscale-proxy"].tag
-				pullPolicy: images["tailscale-proxy"].pullPolicy
-			}
-		}
-	}
-	"port-allocator": {
-		chart: charts.portAllocator
-		values: {
-			repoAddr: release.repoAddr
-			sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
-			ingressNginxPath: "\(release.appDir)/resources/ingress-nginx.yaml"
-			image: {
-				repository: images.portAllocator.fullName
-				tag: images.portAllocator.tag
-				pullPolicy: images.portAllocator.pullPolicy
+		"port-allocator": {
+			chart: charts.portAllocator
+			values: {
+				repoAddr: release.repoAddr
+				sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
+				ingressNginxPath: "\(release.appDir)/resources/ingress-nginx.yaml"
+				image: {
+					repository: images.portAllocator.fullName
+					tag: images.portAllocator.tag
+					pullPolicy: images.portAllocator.pullPolicy
+				}
 			}
 		}
 	}
diff --git a/core/installer/values-tmpl/qbittorrent.cue b/core/installer/values-tmpl/qbittorrent.cue
index d569311..bec6a3c 100644
--- a/core/installer/values-tmpl/qbittorrent.cue
+++ b/core/installer/values-tmpl/qbittorrent.cue
@@ -12,41 +12,43 @@
 description: "qBittorrent is a cross-platform free and open-source BitTorrent client written in native C++. It relies on Boost, Qt 6 toolkit and the libtorrent-rasterbar library, with an optional search engine written in Python."
 icon: "<svg xmlns='http://www.w3.org/2000/svg' width='50' height='50' viewBox='0 0 48 48'><circle cx='24' cy='24' r='21.5' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/><path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='M26.651 22.364a5.034 5.034 0 0 1 5.035-5.035h0a5.034 5.034 0 0 1 5.034 5.035v3.272a5.034 5.034 0 0 1-5.034 5.035h0a5.034 5.034 0 0 1-5.035-5.035m0 5.035V10.533m-5.302 15.103a5.034 5.034 0 0 1-5.035 5.035h0a5.034 5.034 0 0 1-5.034-5.035v-3.272a5.034 5.034 0 0 1 5.034-5.035h0a5.034 5.034 0 0 1 5.035 5.035m0-5.035v20.138'/></svg>"
 
-images: {
-	qbittorrent: {
-		registry: "lscr.io"
-		repository: "linuxserver"
-		name: "qbittorrent"
-		tag: "4.5.3"
-		pullPolicy: "IfNotPresent"
+out: {
+	images: {
+		qbittorrent: {
+			registry: "lscr.io"
+			repository: "linuxserver"
+			name: "qbittorrent"
+			tag: "4.5.3"
+			pullPolicy: "IfNotPresent"
+		}
 	}
-}
 
-charts: {
-	qbittorrent: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/qbittorrent"
+	charts: {
+		qbittorrent: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/qbittorrent"
+		}
 	}
-}
 
-helm: {
-	qbittorrent: {
-		chart: charts.qbittorrent
-		values: {
-			pcloudInstanceId: global.id
-			ingress: {
-				className: input.network.ingressClass
-				domain: _domain
-			}
-			webui: port: 8080
-			bittorrent: port: 6881
-			storage: size: "100Gi"
-			image: {
-				repository: images.qbittorrent.fullName
-				tag: images.qbittorrent.tag
-				pullPolicy: images.qbittorrent.pullPolicy
+	helm: {
+		qbittorrent: {
+			chart: charts.qbittorrent
+			values: {
+				pcloudInstanceId: global.id
+				ingress: {
+					className: input.network.ingressClass
+					domain: _domain
+				}
+				webui: port: 8080
+				bittorrent: port: 6881
+				storage: size: "100Gi"
+				image: {
+					repository: images.qbittorrent.fullName
+					tag: images.qbittorrent.tag
+					pullPolicy: images.qbittorrent.pullPolicy
+				}
 			}
 		}
 	}
diff --git a/core/installer/values-tmpl/resource-renderer-controller.cue b/core/installer/values-tmpl/resource-renderer-controller.cue
index 23d1a54..1169be7 100644
--- a/core/installer/values-tmpl/resource-renderer-controller.cue
+++ b/core/installer/values-tmpl/resource-renderer-controller.cue
@@ -3,46 +3,48 @@
 name: "resource-renderer-controller"
 namespace: "rr-controller"
 
-images: {
-	resourceRenderer: {
-		repository: "giolekva"
-		name: "resource-renderer-controller"
-		tag: "latest"
-		pullPolicy: "Always"
+out: {
+	images: {
+		resourceRenderer: {
+			repository: "giolekva"
+			name: "resource-renderer-controller"
+			tag: "latest"
+			pullPolicy: "Always"
+		}
+		kubeRBACProxy: {
+			registry: "gcr.io"
+			repository: "kubebuilder"
+			name: "kube-rbac-proxy"
+			tag: "v0.13.0"
+			pullPolicy: "IfNotPresent"
+		}
 	}
-	kubeRBACProxy: {
-		registry: "gcr.io"
-		repository: "kubebuilder"
-		name: "kube-rbac-proxy"
-		tag: "v0.13.0"
-		pullPolicy: "IfNotPresent"
-	}
-}
 
-charts: {
-	resourceRenderer: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/resource-renderer-controller"
+	charts: {
+		resourceRenderer: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/resource-renderer-controller"
+		}
 	}
-}
 
-helm: {
-	"resource-renderer": {
-		chart: charts.resourceRenderer
-		values: {
-			installCRDs: true
-			image: {
-				repository: images.resourceRenderer.fullName
-				tag: images.resourceRenderer.tag
-				pullPolicy: images.resourceRenderer.pullPolicy
-			}
-			kubeRBACProxy: {
+	helm: {
+		"resource-renderer": {
+			chart: charts.resourceRenderer
+			values: {
+				installCRDs: true
 				image: {
-					repository: images.kubeRBACProxy.fullName
-					tag: images.kubeRBACProxy.tag
-					pullPolicy: images.kubeRBACProxy.pullPolicy
+					repository: images.resourceRenderer.fullName
+					tag: images.resourceRenderer.tag
+					pullPolicy: images.resourceRenderer.pullPolicy
+				}
+				kubeRBACProxy: {
+					image: {
+						repository: images.kubeRBACProxy.fullName
+						tag: images.kubeRBACProxy.tag
+						pullPolicy: images.kubeRBACProxy.pullPolicy
+					}
 				}
 			}
 		}
diff --git a/core/installer/values-tmpl/rpuppy.cue b/core/installer/values-tmpl/rpuppy.cue
index 65bfbdb..ab77052 100644
--- a/core/installer/values-tmpl/rpuppy.cue
+++ b/core/installer/values-tmpl/rpuppy.cue
@@ -32,47 +32,49 @@
 
 _httpPortName: "http"
 
-ingress: {
-	rpuppy: {
-		auth: input.auth
-		network: input.network
-		subdomain: input.subdomain
-		service: {
-			name: "rpuppy"
-			port: name: _httpPortName
+out: {
+	ingress: {
+		rpuppy: {
+			auth: input.auth
+			network: input.network
+			subdomain: input.subdomain
+			service: {
+				name: "rpuppy"
+				port: name: _httpPortName
+			}
 		}
 	}
-}
 
-images: {
-	rpuppy: {
-		repository: "giolekva"
-		name: "rpuppy"
-		tag: "latest"
-		pullPolicy: "Always"
+	images: {
+		rpuppy: {
+			repository: "giolekva"
+			name: "rpuppy"
+			tag: "latest"
+			pullPolicy: "Always"
+		}
 	}
-}
 
-charts: {
-	rpuppy: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/rpuppy"
+	charts: {
+		rpuppy: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/rpuppy"
+		}
 	}
-}
 
-helm: {
-	rpuppy: {
-		chart: charts.rpuppy
-		info: "Installing rPuppy server"
-		values: {
-			image: {
-				repository: images.rpuppy.fullName
-				tag: images.rpuppy.tag
-				pullPolicy: images.rpuppy.pullPolicy
+	helm: {
+		rpuppy: {
+			chart: charts.rpuppy
+			info: "Installing rPuppy server"
+			values: {
+				image: {
+					repository: images.rpuppy.fullName
+					tag: images.rpuppy.tag
+					pullPolicy: images.rpuppy.pullPolicy
+				}
+				portName: _httpPortName
 			}
-			portName: _httpPortName
 		}
 	}
 }
diff --git a/core/installer/values-tmpl/soft-serve.cue b/core/installer/values-tmpl/soft-serve.cue
index a52ddd7..a6f85c0 100644
--- a/core/installer/values-tmpl/soft-serve.cue
+++ b/core/installer/values-tmpl/soft-serve.cue
@@ -36,36 +36,6 @@
   </g>
 </svg>"""
 
-images: {
-	softserve: {
-		repository: "charmcli"
-		name: "soft-serve"
-		tag: "v0.7.1"
-		pullPolicy: "IfNotPresent"
-	}
-}
-
-charts: {
-	softserve: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/soft-serve"
-	}
-}
-
-ingress: {
-	gerrit: { // TODO(gio): rename to soft-serve
-		auth: enabled: false
-		network: input.network
-		subdomain: input.subdomain
-		service: {
-			name: "soft-serve"
-			port: number: 80
-		}
-	}
-}
-
 portForward: [#PortForward & {
 	allocator: input.network.allocatePortAddr
 	reservator: input.network.reservePortAddr
@@ -75,22 +45,55 @@
 	targetPort: 22
 }]
 
-helm: {
-	softserve: {
-		chart: charts.softserve
-		info: "Installing SoftServe server"
-		values: {
-			serviceType: "ClusterIP"
-			adminKey: input.adminKey
-			sshPublicPort: input.sshPort
-			ingress: {
-				enabled: false
-				domain: _domain
+
+out: {
+	images: {
+		softserve: {
+			repository: "charmcli"
+			name: "soft-serve"
+			tag: "v0.7.1"
+			pullPolicy: "IfNotPresent"
+		}
+	}
+
+	charts: {
+		softserve: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/soft-serve"
+		}
+	}
+
+	ingress: {
+		softserve: {
+			auth: enabled: false
+			network: input.network
+			subdomain: input.subdomain
+			service: {
+				name: "soft-serve"
+				port: number: 80
 			}
-			image: {
-				repository: images.softserve.fullName
-				tag: images.softserve.tag
-				pullPolicy: images.softserve.pullPolicy
+		}
+	}
+
+	volumes: data: size: "1Gi"
+
+	helm: {
+		softserve: {
+			chart: charts.softserve
+			info: "Installing SoftServe server"
+			values: {
+				serviceType: "ClusterIP"
+				adminKey: input.adminKey
+				host: _domain
+				sshPublicPort: input.sshPort
+				persistentVolumeClaimName: volumes.data.name
+				image: {
+					repository: images.softserve.fullName
+					tag: images.softserve.tag
+					pullPolicy: images.softserve.pullPolicy
+				}
 			}
 		}
 	}
diff --git a/core/installer/values-tmpl/url-shortener.cue b/core/installer/values-tmpl/url-shortener.cue
index 26f3287..1fa70c4 100644
--- a/core/installer/values-tmpl/url-shortener.cue
+++ b/core/installer/values-tmpl/url-shortener.cue
@@ -2,6 +2,7 @@
     network: #Network @name(Network)
     subdomain: string @name(Subdomain)
 	auth: #Auth @name(Authentication)
+	storageSize: string @name(Storage Size)
 }
 
 _domain: "\(input.subdomain).\(input.network.domain)"
@@ -37,51 +38,53 @@
 
 _httpPortName: "http"
 
-ingress: {
-	"url-shorteners": {
-		auth: input.auth
-		network: input.network
-		subdomain: input.subdomain
-		service: {
-			name: "url-shortener"
-			port: name: _httpPortName
+out: {
+	ingress: {
+		"url-shorteners": {
+			auth: input.auth
+			network: input.network
+			subdomain: input.subdomain
+			service: {
+				name: "url-shortener"
+				port: name: _httpPortName
+			}
 		}
 	}
-}
 
-images: {
-	urlShortener: {
-		repository: "giolekva"
-		name: "url-shortener"
-		tag: "latest"
-		pullPolicy: "Always"
+	images: {
+		urlShortener: {
+			repository: "giolekva"
+			name: "url-shortener"
+			tag: "latest"
+			pullPolicy: "Always"
+		}
 	}
-}
 
-charts: {
-    urlShortener: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/url-shortener"
-    }
-}
+	charts: {
+		urlShortener: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/url-shortener"
+		}
+	}
 
-helm: {
-    "url-shortener": {
-        chart: charts.urlShortener
-		info: "Installing server"
-        values: {
-            storage: {
-                size: "1Gi"
-            }
-            image: {
-				repository: images.urlShortener.fullName
-				tag: images.urlShortener.tag
-				pullPolicy: images.urlShortener.pullPolicy
+		volumes: data: size: input.storageSize
+
+	helm: {
+		"url-shortener": {
+			chart: charts.urlShortener
+			info: "Installing server"
+			values: {
+				image: {
+					repository: images.urlShortener.fullName
+					tag: images.urlShortener.tag
+					pullPolicy: images.urlShortener.pullPolicy
+				}
+				portName: _httpPortName
+				persistentVolumeClaimNama: volumes.data.name
+				requireAuth: input.auth.enabled
 			}
-            portName: _httpPortName
-			requireAuth: input.auth.enabled
-        }
-    }
+		}
+	}
 }
diff --git a/core/installer/values-tmpl/vaultwarden.cue b/core/installer/values-tmpl/vaultwarden.cue
index 4b675c5..69ac75f 100644
--- a/core/installer/values-tmpl/vaultwarden.cue
+++ b/core/installer/values-tmpl/vaultwarden.cue
@@ -1,6 +1,7 @@
 input: {
     network: #Network @name(Network)
     subdomain: string @name(Subdomain)
+	storageSize: string
 }
 
 _domain: "\(input.subdomain).\(input.network.domain)"
@@ -30,36 +31,52 @@
   <path class='cls-1' d='m16.59579535,39.68503937c-.31541688,0-.63121702-.07051848-.92555501-.21155543l-1.05816041-.49976138c-.48500615-.2368501-1.24959508-.66149397-2.27996967-1.26396705-1.22238414-.71514934-2.19948662-1.35211514-3.07502717-2.00364455-1.11583992-.82092705-2.19661223-1.72003763-3.20954883-2.67050406-1.0945694-.97882712-2.11363804-2.07876206-3.01753928-3.26071305-.8868465-1.18501701-1.60889438-2.47811126-2.15023867-3.84785602-.57832816-1.42493314-.87515729-2.93341534-.87975632-4.47715678V2.24126116C.00747343,1.01102034,1.01121196.00766505,2.23762026,0h28.73053054c1.23024082.00766505,2.23397936,1.01102034,2.24145278,2.23742863v19.16109658c.00210789,1.56060455-.29472124,3.08671637-.88205584,4.53387816-.53732013,1.38124234-1.26281729,2.6911997-2.15292143,3.88541478-.90620075,1.18501701-1.92526939,2.28571846-3.03382752,3.27757616-1.01025383.92747127-2.09447541,1.80971874-3.21625575,2.6168487-1.1200557.77263722-2.08834337,1.40347099-3.0577808,1.99214697-.71150844.43230892-1.2898366.73661148-1.71198933.95889798-.20983079.11037675-.38651024.20312387-.53042159.28590643l-1.10759999.52582256c-.29203847.13950394-.6063056.21002242-.92095598.21002242ZM30.96431827,1.28006366H2.24145278c-.52505605.00306602-.95813148.43614145-.96138913.96503003v19.20248786c.00421578,1.37740981.26942657,2.72569243.78815895,4.00345657.50167764,1.27009909,1.16796227,2.46278115,1.98256566,3.55198502.85101238,1.11219902,1.8152759,2.15311306,2.86155548,3.08901589.98706705.92593826,2.02836434,1.79208912,3.10607063,2.58465548.83989805.62546823,1.77944178,1.23713937,2.96024302,1.92776054,1.24921182.73124595,1.87947071,1.0646757,2.18798905,1.21567722l1.05336975.49746186c.23934124.11420927.51068408.11420927.7469593.00153301l1.06390919-.5028274c.10941861-.06591945.30047003-.16633162.52754719-.28590643.43039266-.22688553.96579653-.50819294,1.64377037-.92057273.94778366-.57564539,1.89633382-1.19344857,2.98515444-1.94385715,1.07042449-.77033771,2.1147878-1.62039196,3.09399817-2.51873603,1.05471113-.94433439,2.01878303-1.98524843,2.87171167-3.10051347.81632802-1.09610241,1.4858703-2.30488109,1.98486517-3.58724426.5283137-1.3015258.79333286-2.66437202.79160823-4.06017796V2.24279417c-.0044074-.52812207-.43690795-.95966449-.96522165-.96273051Zm-14.35951648,33.14751666c-.11535903,0-.23033481-.03142671-.33208837-.09274713-.19124304-.11650879-.30794346-.32346519-.30794346-.5472847V5.42455718c0-.35335889.28648131-.64003183.64003183-.64003183h11.18024461c.35355052,0,.64003183.28667294.64003183.64003183v15.77237716c.01935426.06132041.02951045.12647336.02951045.1923928,0,3.12887416-2.06649797,6.31983524-6.14219766,9.48473514-1.68688628,1.31915542-3.50848585,2.49114185-5.4122931,3.48146654-.09274713.04828983-.19411744.07205149-.29529612.07205149Zm.64003183-28.3629913v26.64985222c1.49621812-.83932318,2.93245721-1.79592164,4.2805482-2.85063278,3.66581104-2.84603375,5.5638695-5.63534611,5.64626881-8.29358609-.01724637-.05518837-.02663606-.11267626-.02663606-.17169716V6.06458901h-9.90018095Z'/>
 </svg>"""
 
-images: {
-	vaultwarden: {
-		repository: "vaultwarden"
-		name: "server"
-		tag: "1.28.1"
-		pullPolicy: "IfNotPresent"
+out: {
+	ingress: {
+		vaultwarden: {
+			auth: enabled: false
+			network: input.network
+			subdomain: input.subdomain
+			service: {
+				name: "server"
+				port: name: _httpPortName
+			}
+		}
 	}
-}
 
-charts: {
-	vaultwarden: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/vaultwarden"
+	images: {
+		vaultwarden: {
+			repository: "vaultwarden"
+			name: "server"
+			tag: "1.28.1"
+			pullPolicy: "IfNotPresent"
+		}
 	}
-}
 
-helm: {
-	vaultwarden: {
-		chart: charts.vaultwarden
-		info: "Installing Vaultwarden server"
-		values: {
-			ingressClassName: input.network.ingressClass
-			certificateIssuer: input.network.certificateIssuer
-			domain: _domain
-			image: {
-				repository: images.vaultwarden.fullName
-				tag: images.vaultwarden.tag
-				pullPolicy: images.vaultwarden.pullPolicy
+	charts: {
+		vaultwarden: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/vaultwarden"
+		}
+	}
+
+	volumes: data: size: input.storageSize
+
+	helm: {
+		vaultwarden: {
+			chart: charts.vaultwarden
+			info: "Installing Vaultwarden server"
+			values: {
+				image: {
+					repository: images.vaultwarden.fullName
+					tag: images.vaultwarden.tag
+					pullPolicy: images.vaultwarden.pullPolicy
+				}
+				domain: _domain
+				persistentVolumeClaimName: volumes.data.name
+				httpPortName: _httpPortName
 			}
 		}
 	}
@@ -69,3 +86,5 @@
 	title: "Access"
 	contents: "Open [\(url)](\(url)) in a new tab."
 }]
+
+_httpPortName: "http"
diff --git a/core/installer/values-tmpl/virtual-machine.cue b/core/installer/values-tmpl/virtual-machine.cue
index 292c082..5b08788 100644
--- a/core/installer/values-tmpl/virtual-machine.cue
+++ b/core/installer/values-tmpl/virtual-machine.cue
@@ -11,62 +11,20 @@
 readme: "Virtual Machine"
 description: "Virtual Machine"
 icon: """
-<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 2048 2048"><path fill="currentColor" d="M1280 384H640V256h640zm0 1024H640v-128h640zm0 256H640v-128h640zM1408 0q27 0 50 10t40 27t28 41t10 50v1792H384V128q0-27 10-50t27-40t41-28t50-10zm0 128H512v1664h896z"/></svg>"""
+	   <svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 2048 2048"><path fill="currentColor" d="M1280 384H640V256h640zm0 1024H640v-128h640zm0 256H640v-128h640zM1408 0q27 0 50 10t40 27t28 41t10 50v1792H384V128q0-27 10-50t27-40t41-28t50-10zm0 128H512v1664h896z"/></svg>"""
 
-charts: {
-	virtualMachine: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/virtual-machine"
-	}
-}
-
-helm: {
-	"virtual-machine": {
-		chart: charts.virtualMachine
-		values: {
-			name: input.name
+out: {
+	vm: {
+		"\(input.name)": {
+			username: input.username
+			domain: global.domain
 			cpuCores: input.cpuCores
 			memory: input.memory
-			disk: {
-				source: "https://cloud.debian.org/images/cloud/bookworm-backports/latest/debian-12-backports-generic-amd64.qcow2"
-				size: "64Gi"
+			vpn: {
+				enabled: true
+				loginServer: "https://headscale.\(global.domain)"
+				authKey: input.authKey
 			}
-			ports: [22, 8080]
-			cloudInit: userData: _cloudInitUserData
 		}
 	}
 }
-
-_cloudInitUserData: {
-	system_info: {
-		default_user: {
-			name: input.username
-			home: "/home/\(input.username)"
-		}
-	}
-	password: "dodo" // TODO(gio): remove if possible
-	chpasswd: {
-		expire: false
-	}
-	hostname: input.name
-	ssh_pwauth: true
-	disable_root: false
-	ssh_authorized_keys: [
-		"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOa7FUrmXzdY3no8qNGUk7OPaRcIUi8G7MVbLlff9eB/ lekva@gl-mbp-m1-max.local"
-    ]
-	runcmd: [
-		["sh", "-c", "curl -fsSL https://tailscale.com/install.sh | sh"],
-		// TODO(gio): take auth key from input
-		// TODO(gio): enable tailscale ssh
-		["sh", "-c", "tailscale up --login-server=https://headscale.\(global.domain) --auth-key=\(input.authKey) --accept-routes"],
-		["sh", "-c", "curl -fsSL https://code-server.dev/install.sh | HOME=/home/\(input.username) sh"],
-		["sh", "-c", "systemctl enable --now code-server@\(input.username)"],
-		["sh", "-c", "sleep 10"],
-		// TODO(gio): listen only on tailscale interface
-		["sh", "-c", "sed -i -e 's/127.0.0.1/0.0.0.0/g' /home/\(input.username)/.config/code-server/config.yaml"],
-		["sh", "-c", "sed -i -e 's/auth: password/auth: none/g' /home/\(input.username)/.config/code-server/config.yaml"],
-		["sh", "-c", "systemctl restart --now code-server@\(input.username)"],
-    ]
-}
diff --git a/core/installer/values-tmpl/welcome.cue b/core/installer/values-tmpl/welcome.cue
index 2d68207..6022fb7 100644
--- a/core/installer/values-tmpl/welcome.cue
+++ b/core/installer/values-tmpl/welcome.cue
@@ -11,43 +11,45 @@
 name: "welcome"
 namespace: "app-welcome"
 
-images: {
-	welcome: {
-		repository: "giolekva"
-		name: "pcloud-installer"
-		tag: "latest"
-		pullPolicy: "Always"
+out: {
+	images: {
+		welcome: {
+			repository: "giolekva"
+			name: "pcloud-installer"
+			tag: "latest"
+			pullPolicy: "Always"
+		}
 	}
-}
 
-charts: {
-	welcome: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/welcome"
+	charts: {
+		welcome: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/welcome"
+		}
 	}
-}
 
-helm: {
-	welcome: {
-		chart: charts.welcome
-		values: {
-			repoAddr: input.repoAddr
-			sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
-			createAccountAddr: "http://api.\(global.namespacePrefix)core-auth.svc.cluster.local/identities"
-			loginAddr: "https://launcher.\(networks.public.domain)"
-			membershipsAddr: "http://memberships-api.\(global.namespacePrefix)core-auth-memberships.svc.cluster.local"
-			ingress: {
-				className: input.network.ingressClass
-				domain: "welcome.\(input.network.domain)"
-				certificateIssuer: input.network.certificateIssuer
-			}
-			clusterRoleName: "\(global.id)-welcome"
-			image: {
-				repository: images.welcome.fullName
-				tag: images.welcome.tag
-				pullPolicy: images.welcome.pullPolicy
+	helm: {
+		welcome: {
+			chart: charts.welcome
+			values: {
+				repoAddr: input.repoAddr
+				sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
+				createAccountAddr: "http://api.\(global.namespacePrefix)core-auth.svc.cluster.local/identities"
+				loginAddr: "https://launcher.\(networks.public.domain)"
+				membershipsAddr: "http://memberships-api.\(global.namespacePrefix)core-auth-memberships.svc.cluster.local"
+				ingress: {
+					className: input.network.ingressClass
+					domain: "welcome.\(input.network.domain)"
+					certificateIssuer: input.network.certificateIssuer
+				}
+				clusterRoleName: "\(global.id)-welcome"
+				image: {
+					repository: images.welcome.fullName
+					tag: images.welcome.tag
+					pullPolicy: images.welcome.pullPolicy
+				}
 			}
 		}
 	}
diff --git a/core/installer/values-tmpl/zot.cue b/core/installer/values-tmpl/zot.cue
index 35fdbc7..c5cf631 100644
--- a/core/installer/values-tmpl/zot.cue
+++ b/core/installer/values-tmpl/zot.cue
@@ -39,153 +39,155 @@
   </g>
 </svg>"""
 
-ingress: {
-	zot: {
-		auth: enabled: false
-		network: input.network
-		subdomain: input.subdomain
-		service: {
-			name: "zot"
-			port: number: _httpPort // TODO(gio): make optional
+out: {
+	ingress: {
+		zot: {
+			auth: enabled: false
+			network: input.network
+			subdomain: input.subdomain
+			service: {
+				name: "zot"
+				port: number: _httpPort // TODO(gio): make optional
+			}
 		}
 	}
-}
 
-// TODO(gio): configure busybox
-images: {
-	zot: {
-		registry: "ghcr.io"
-		repository: "project-zot"
-		name: "zot-linux-amd64"
-		tag: "v2.0.3"
-		pullPolicy: "IfNotPresent"
-	}
-}
-
-charts: {
-	zot: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/zot"
-	}
-	oauth2Client: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/oauth2-client"
-	}
-	resourceRenderer: {
-		kind: "GitRepository"
-		address: "https://code.v1.dodo.cloud/helm-charts"
-		branch: "main"
-		path: "charts/resource-renderer"
-	}
-}
-
-volumes: zot: size: "100Gi"
-
-_httpPort: 80
-_oauth2ClientSecretName: "oauth2-client"
-
-helm: {
-	"oauth2-client": {
-		chart: charts.oauth2Client
-		info: "Creating OAuth2 client"
-		// TODO(gio): remove once hydra maester is installed as part of dodo itself
-		dependsOn: [{
-			name: "auth"
-			namespace: "\(global.namespacePrefix)core-auth"
-		}]
-		values: {
-			name: "\(release.namespace)-zot"
-			secretName: _oauth2ClientSecretName
-			grantTypes: ["authorization_code"]
-			responseTypes: ["code"]
-			scope: "openid profile email groups"
-			redirectUris: ["https://\(_domain)/zot/auth/callback/oidc"]
-			hydraAdmin: "http://hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
+	// TODO(gio): configure busybox
+	images: {
+		zot: {
+			registry: "ghcr.io"
+			repository: "project-zot"
+			name: "zot-linux-amd64"
+			tag: "v2.0.3"
+			pullPolicy: "IfNotPresent"
 		}
 	}
-	"config-renderer": {
-		chart: charts.resourceRenderer
-		info: "Generating Zot configuration"
-		values: {
-			name: "config-renderer"
-			secretName: _oauth2ClientSecretName
-			resourceTemplate: yaml.Marshal({
-				apiVersion: "v1"
-				kind: "ConfigMap"
-				metadata: {
-					name: _zotConfigMapName
-					namespace: "\(release.namespace)"
-				}
-				data: {
-					"config.json": json.Marshal({
-						storage: rootDirectory: "/var/lib/registry"
-						http: {
-							address: "0.0.0.0"
-							port: "5000"
-							externalUrl: url
-							auth: openid: providers: oidc: {
-								name: "dodo:"
-								issuer: "https://hydra.\(networks.public.domain)"
-								clientid: "{{ .client_id }}"
-								clientsecret: "{{ .client_secret }}"
-								keypath: ""
-								scopes: ["openid", "profile", "email", "groups"]
-							}
-							accessControl: {
-								repositories: {
-									"**": {
-										defaultPolicy: ["read", "create", "update", "delete"]
-										anonymousPolicy: ["read"]
+
+	charts: {
+		zot: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/zot"
+		}
+		oauth2Client: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/oauth2-client"
+		}
+		resourceRenderer: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/resource-renderer"
+		}
+	}
+
+	volumes: zot: size: "100Gi"
+
+	_httpPort: 80
+	_oauth2ClientSecretName: "oauth2-client"
+
+	helm: {
+		"oauth2-client": {
+			chart: charts.oauth2Client
+			info: "Creating OAuth2 client"
+			// TODO(gio): remove once hydra maester is installed as part of dodo itself
+			dependsOn: [{
+				name: "auth"
+				namespace: "\(global.namespacePrefix)core-auth"
+			}]
+			values: {
+				name: "\(release.namespace)-zot"
+				secretName: _oauth2ClientSecretName
+				grantTypes: ["authorization_code"]
+				responseTypes: ["code"]
+				scope: "openid profile email groups"
+				redirectUris: ["https://\(_domain)/zot/auth/callback/oidc"]
+				hydraAdmin: "http://hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
+			}
+		}
+		"config-renderer": {
+			chart: charts.resourceRenderer
+			info: "Generating Zot configuration"
+			values: {
+				name: "config-renderer"
+				secretName: _oauth2ClientSecretName
+				resourceTemplate: yaml.Marshal({
+					apiVersion: "v1"
+					kind: "ConfigMap"
+					metadata: {
+						name: _zotConfigMapName
+						namespace: "\(release.namespace)"
+					}
+					data: {
+						"config.json": json.Marshal({
+							storage: rootDirectory: "/var/lib/registry"
+							http: {
+								address: "0.0.0.0"
+								port: "5000"
+								externalUrl: url
+								auth: openid: providers: oidc: {
+									name: "dodo:"
+									issuer: "https://hydra.\(networks.public.domain)"
+									clientid: "{{ .client_id }}"
+									clientsecret: "{{ .client_secret }}"
+									keypath: ""
+									scopes: ["openid", "profile", "email", "groups"]
+								}
+								accessControl: {
+									repositories: {
+										"**": {
+											defaultPolicy: ["read", "create", "update", "delete"]
+											anonymousPolicy: ["read"]
+										}
 									}
 								}
 							}
-						}
-						log: level: "debug"
-						extensions: {
-							ui: enable: true
-							search: enable: true
-						}
-					})
-				}
-			})
+							log: level: "debug"
+							extensions: {
+								ui: enable: true
+								search: enable: true
+							}
+						})
+					}
+				})
+			}
 		}
-	}
-	zot: {
-		chart: charts.zot
-		info: "Installing Zot server"
-		values: {
-			image: {
-				repository: images.zot.fullName
-				tag: images.zot.tag
-				pullPolicy: images.zot.pullPolicy
-			}
-			service: {
-				type: "ClusterIP"
-				additionalAnnotations: {
-					"metallb.universe.tf/address-pool": global.id
+		zot: {
+			chart: charts.zot
+			info: "Installing Zot server"
+			values: {
+				image: {
+					repository: images.zot.fullName
+					tag: images.zot.tag
+					pullPolicy: images.zot.pullPolicy
 				}
-				port: _httpPort
+				service: {
+					type: "ClusterIP"
+					additionalAnnotations: {
+						"metallb.universe.tf/address-pool": global.id
+					}
+					port: _httpPort
+				}
+				ingress: enabled: false
+				mountConfig: false
+				persistence: true
+				pvc: {
+					create: false
+					name: volumes.zot.name
+				}
+				extraVolumes: [{
+					name: "config"
+					configMap: name: _zotConfigMapName
+				}]
+				extraVolumeMounts: [{
+					name: "config"
+					mountPath: "/etc/zot"
+				}]
+				startupProbe: {}
 			}
-			ingress: enabled: false
-			mountConfig: false
-			persistence: true
-			pvc: {
-				create: false
-				name: volumes.zot.name
-			}
-			extraVolumes: [{
-				name: "config"
-				configMap: name: _zotConfigMapName
-			}]
-			extraVolumeMounts: [{
-				name: "config"
-				mountPath: "/etc/zot"
-			}]
-			startupProbe: {}
 		}
 	}
 }
diff --git a/core/installer/welcome/app_tmpl.go b/core/installer/welcome/app_tmpl.go
index 33f8d8d..fb512fa 100644
--- a/core/installer/welcome/app_tmpl.go
+++ b/core/installer/welcome/app_tmpl.go
@@ -1,15 +1,14 @@
 package welcome
 
 import (
+	"bytes"
 	"fmt"
-	"io"
 	"io/fs"
 	"sort"
 	"strings"
 	"text/template"
 
 	"github.com/giolekva/pcloud/core/installer"
-	"github.com/giolekva/pcloud/core/installer/soft"
 )
 
 const tmplSuffix = ".gotmpl"
@@ -69,7 +68,7 @@
 }
 
 type AppTmpl interface {
-	Render(network installer.Network, subdomain string, out soft.RepoFS) error
+	Render(network installer.Network, subdomain string) (map[string][]byte, error)
 }
 
 type appTmplFS struct {
@@ -109,29 +108,20 @@
 	return &appTmplFS{files, tmpls}, nil
 }
 
-func (a *appTmplFS) Render(network installer.Network, subdomain string, out soft.RepoFS) error {
+func (a *appTmplFS) Render(network installer.Network, subdomain string) (map[string][]byte, error) {
+	ret := map[string][]byte{}
+	for path, contents := range a.files {
+		ret[path] = contents
+	}
 	for path, tmpl := range a.tmpls {
-		f, err := out.Writer(path)
-		if err != nil {
-			return err
-		}
-		defer f.Close()
-		if err := tmpl.Execute(f, map[string]any{
+		var buf bytes.Buffer
+		if err := tmpl.Execute(&buf, map[string]any{
 			"Network":   network,
 			"Subdomain": subdomain,
 		}); err != nil {
-			return err
+			return nil, err
 		}
+		ret[path] = buf.Bytes()
 	}
-	for path, contents := range a.files {
-		f, err := out.Writer(path)
-		if err != nil {
-			return err
-		}
-		defer f.Close()
-		if _, err := io.WriteString(f, string(contents)); err != nil {
-			return err
-		}
-	}
-	return nil
+	return ret, nil
 }
diff --git a/core/installer/welcome/app_tmpl_test.go b/core/installer/welcome/app_tmpl_test.go
index daaf4c8..00e5a3c 100644
--- a/core/installer/welcome/app_tmpl_test.go
+++ b/core/installer/welcome/app_tmpl_test.go
@@ -6,10 +6,7 @@
 	"io/fs"
 	"testing"
 
-	"github.com/go-git/go-billy/v5/memfs"
-
 	"github.com/giolekva/pcloud/core/installer"
-	"github.com/giolekva/pcloud/core/installer/soft"
 )
 
 //go:embed app-tmpl
@@ -38,8 +35,7 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	out := soft.NewBillyRepoFS(memfs.New())
-	if err := a.Render(network, "testapp", out); err != nil {
+	if _, err := a.Render(network, "testapp"); err != nil {
 		t.Fatal(err)
 	}
 }
@@ -57,8 +53,7 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	out := soft.NewBillyRepoFS(memfs.New())
-	if err := a.Render(network, "testapp", out); err != nil {
+	if _, err := a.Render(network, "testapp"); err != nil {
 		t.Fatal(err)
 	}
 }
@@ -76,8 +71,7 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	out := soft.NewBillyRepoFS(memfs.New())
-	if err := a.Render(network, "testapp", out); err != nil {
+	if _, err := a.Render(network, "testapp"); err != nil {
 		t.Fatal(err)
 	}
 }
diff --git a/core/installer/welcome/appmanager-tmpl/app.html b/core/installer/welcome/appmanager-tmpl/app.html
index b25f5b1..c6874cb 100644
--- a/core/installer/welcome/appmanager-tmpl/app.html
+++ b/core/installer/welcome/appmanager-tmpl/app.html
@@ -31,9 +31,9 @@
     {{ else if eq $schema.Kind 1 }}
       <label {{ if $schema.Advanced }}hidden{{ end }}>
           {{ $schema.Name }}
-	  <input type="text" name="{{ $name }}" oninput="valueChanged({{ $name }}, this.value)" {{ if $readonly }}disabled{{ end }} value="{{ index $data $name }}" />
+		  <input type="text" name="{{ $name }}" oninput="valueChanged({{ $name }}, this.value)" {{ if $readonly }}disabled{{ end }} value="{{ index $data $name }}" />
+	  </label>
     {{ else if eq $schema.Kind 4 }}
-      </label>
       <label {{ if $schema.Advanced }}hidden{{ end }}>
           {{ $schema.Name }}
 		  <input type="text" name="{{ $name }}" oninput="valueChanged({{ $name }}, this.value)" {{ if $readonly }}disabled{{ end }} value="{{ index $data $name }}" />
diff --git a/core/installer/welcome/appmanager.go b/core/installer/welcome/appmanager.go
index 82421e9..7f168dc 100644
--- a/core/installer/welcome/appmanager.go
+++ b/core/installer/welcome/appmanager.go
@@ -163,7 +163,7 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	instances, err := s.m.FindAllAppInstances(slug)
+	instances, err := s.m.GetAllAppInstances(slug)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -182,7 +182,7 @@
 		http.Error(w, "empty slug", http.StatusBadRequest)
 		return
 	}
-	instance, err := s.m.FindInstance(slug)
+	instance, err := s.m.GetInstance(slug)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -339,7 +339,7 @@
 	}
 	resp := make([]app, 0)
 	for _, a := range apps {
-		instances, err := s.m.FindAllAppInstances(a.Slug())
+		instances, err := s.m.GetAllAppInstances(a.Slug())
 		if err != nil {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
@@ -393,7 +393,7 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	instances, err := s.m.FindAllAppInstances(slug)
+	instances, err := s.m.GetAllAppInstances(slug)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -427,7 +427,7 @@
 		return
 	}
 	t, ok := s.tasks[slug]
-	instance, err := s.m.FindInstance(slug)
+	instance, err := s.m.GetInstance(slug)
 	if err != nil && !ok {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -446,7 +446,7 @@
 			panic("MUST NOT REACH!")
 		}
 	}
-	instances, err := s.m.FindAllAppInstances(a.Slug())
+	instances, err := s.m.GetAllAppInstances(a.Slug())
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
diff --git a/core/installer/welcome/dodo-app-tmpl/app_status.html b/core/installer/welcome/dodo-app-tmpl/app_status.html
index e96b801..916525e 100644
--- a/core/installer/welcome/dodo-app-tmpl/app_status.html
+++ b/core/installer/welcome/dodo-app-tmpl/app_status.html
@@ -3,11 +3,23 @@
 {{ end }}
 {{- define "content" -}}
 {{ .GitCloneCommand }}<br/>
+<form action="/{{ .Name }}/dev-branch/create" method="POST">
+	<fieldset class="grid">
+		<input type="text" name="branch" placeholder="branch" />
+		<button id="create-dev-branch-button" aria-busy="false" type="submit" name="create-dev-branch">create dev branch</button>
+	</fieldset>
+</form>
 <a href="/{{ .Name }}/logs">Logs</a>
 <hr class="divider">
 {{- template "resources" .LastCommit -}}
 <hr class="divider">
+<h3>Commit History</h3>
 {{- range .Commits -}}
 {{if eq .Status "OK" }}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="black" d="M21 7L9 19l-5.5-5.5l1.41-1.41L9 16.17L19.59 5.59z"/></svg>{{ else }}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 48 48"><path fill="black" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M6 11L11 6L24 19L37 6L42 11L29 24L42 37L37 42L24 29L11 42L6 37L19 24L6 11Z" clip-rule="evenodd"/></svg>{{ end }} <a href="/{{ $.Name }}/{{ .Hash }}">{{ .Hash }}</a> {{ .Message }}<br/>
 {{- end -}}
+<hr class="divider">
+<h3>Branches</h3>
+{{- range .Branches -}}
+<a href="/{{ $.Name }}/branch/{{ . }}">{{ . }}</a><br/>
+{{- end -}}
 {{- end -}}
diff --git a/core/installer/welcome/dodo-app-tmpl/base.html b/core/installer/welcome/dodo-app-tmpl/base.html
index 644ff41..4e9401b 100644
--- a/core/installer/welcome/dodo-app-tmpl/base.html
+++ b/core/installer/welcome/dodo-app-tmpl/base.html
@@ -20,11 +20,20 @@
 </body>
 </html>
 {{ define "resources" }}
-{{- if gt (len .Volume) 0 -}}
-<h3>Volumes</h3>
-{{- range $v := .Volume -}}
-Name: {{ $v.Name }}<br/>
-Size: {{ $v.Size }}<br/>
+{{- if gt (len .Ingress) 0 -}}
+<h3>Ingress</h3>
+{{- range $i := .Ingress -}}
+Host: <a href="{{ $i.Host }}">{{ $i.Host }}</a><br/>
+<br/>
+{{- end -}}
+{{- end -}}
+{{- if gt (len .VirtualMachine) 0 -}}
+<h3>Virtual Machine</h3>
+{{- range $i := .VirtualMachine -}}
+Name: {{ $i.Name }}<br/>
+User: {{ $i.User }}<br/>
+CPU Cores: {{ $i.CPUCores }}<br/>
+Memory: {{ $i.Memory }}<br/>
 <br/>
 {{- end -}}
 {{- end -}}
@@ -37,10 +46,11 @@
 <br/>
 {{- end -}}
 {{- end -}}
-{{- if gt (len .Ingress) 0 -}}
-<h3>Ingress</h3>
-{{- range $i := .Ingress -}}
-Host: {{ $i.Host }}<br/>
+{{- if gt (len .Volume) 0 -}}
+<h3>Volumes</h3>
+{{- range $v := .Volume -}}
+Name: {{ $v.Name }}<br/>
+Size: {{ $v.Size }}<br/>
 <br/>
 {{- end -}}
 {{- end -}}
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index bd72aef..57de8b2 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -12,6 +12,7 @@
 	"io/fs"
 	"net/http"
 	"slices"
+	"strconv"
 	"strings"
 	"sync"
 	"time"
@@ -23,6 +24,7 @@
 	"github.com/giolekva/pcloud/core/installer/soft"
 	"github.com/giolekva/pcloud/core/installer/tasks"
 
+	"cuelang.org/go/cue"
 	"github.com/gorilla/mux"
 	"github.com/gorilla/securecookie"
 )
@@ -194,7 +196,21 @@
 	return s, nil
 }
 
+func (s *DodoAppServer) getAppConfig(app, branch string) appConfig {
+	return s.appConfigs[fmt.Sprintf("%s-%s", app, branch)]
+}
+
+func (s *DodoAppServer) setAppConfig(app, branch string, cfg appConfig) {
+	s.appConfigs[fmt.Sprintf("%s-%s", app, branch)] = cfg
+}
+
 func (s *DodoAppServer) Start() error {
+	// if err := s.client.DisableKeyless(); err != nil {
+	// 	return err
+	// }
+	// if err := s.client.DisableAnonAccess(); err != nil {
+	// 	return err
+	// }
 	e := make(chan error)
 	go func() {
 		r := mux.NewRouter()
@@ -207,6 +223,8 @@
 		r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
 		r.HandleFunc("/{app-name}/logs", s.handleAppLogs).Methods(http.MethodGet)
 		r.HandleFunc("/{app-name}/{hash}", s.handleAppCommit).Methods(http.MethodGet)
+		r.HandleFunc("/{app-name}/dev-branch/create", s.handleCreateDevBranch).Methods(http.MethodPost)
+		r.HandleFunc("/{app-name}/branch/{branch}", s.handleAppStatus).Methods(http.MethodGet)
 		r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
 		r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
 		r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
@@ -216,7 +234,7 @@
 		r := mux.NewRouter()
 		r.HandleFunc("/update", s.handleAPIUpdate)
 		r.HandleFunc("/api/apps/{app-name}/workers", s.handleAPIRegisterWorker).Methods(http.MethodPost)
-		r.HandleFunc("/api/add-admin-key", s.handleAPIAddAdminKey).Methods(http.MethodPost)
+		r.HandleFunc("/api/add-public-key", s.handleAPIAddPublicKey).Methods(http.MethodPost)
 		if !s.external {
 			r.HandleFunc("/api/sync-users", s.handleAPISyncUsers).Methods(http.MethodGet)
 		}
@@ -224,7 +242,6 @@
 	}()
 	if !s.external {
 		go func() {
-			rand.Seed(uint64(time.Now().UnixNano()))
 			s.syncUsers()
 			for {
 				delay := time.Duration(rand.Intn(60)+60) * time.Second
@@ -434,6 +451,7 @@
 	GitCloneCommand string
 	Commits         []CommitMeta
 	LastCommit      resourceData
+	Branches        []string
 }
 
 func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
@@ -443,6 +461,10 @@
 		http.Error(w, "missing app-name", http.StatusBadRequest)
 		return
 	}
+	branch, ok := vars["branch"]
+	if !ok || branch == "" {
+		branch = "master"
+	}
 	u := r.Context().Value(userCtx)
 	if u == nil {
 		http.Error(w, "unauthorized", http.StatusUnauthorized)
@@ -462,7 +484,7 @@
 		http.Error(w, "unauthorized", http.StatusUnauthorized)
 		return
 	}
-	commits, err := s.st.GetCommitHistory(appName)
+	commits, err := s.st.GetCommitHistory(appName, branch)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -481,6 +503,11 @@
 		}
 		lastCommitResources = r
 	}
+	branches, err := s.st.GetBranches(appName)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
 	data := appStatusData{
 		Navigation: []navItem{
 			navItem{"Home", "/"},
@@ -490,6 +517,10 @@
 		GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
 		Commits:         commits,
 		LastCommit:      lastCommitResources,
+		Branches:        branches,
+	}
+	if branch != "master" {
+		data.Navigation = append(data.Navigation, navItem{branch, fmt.Sprintf("/%s/branch/%s", appName, branch)})
 	}
 	if err := s.tmplts.appStatus.Execute(w, data); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -512,10 +543,18 @@
 	Host string
 }
 
+type vm struct {
+	Name     string
+	User     string
+	CPUCores int
+	Memory   string
+}
+
 type resourceData struct {
-	Volume     []volume
-	PostgreSQL []postgresql
-	Ingress    []ingress
+	Volume         []volume
+	PostgreSQL     []postgresql
+	Ingress        []ingress
+	VirtualMachine []vm
 }
 
 type commitStatusData struct {
@@ -659,7 +698,12 @@
 		http.Error(w, err.Error(), http.StatusBadRequest)
 		return
 	}
-	if req.Ref != "refs/heads/master" || req.Repository.Name == ConfigRepoName {
+	if strings.HasPrefix(req.Ref, "refs/heads/dodo_") || req.Repository.Name == ConfigRepoName {
+		return
+	}
+	branch, ok := strings.CutPrefix(req.Ref, "refs/heads/")
+	if !ok {
+		http.Error(w, "invalid branch", http.StatusBadRequest)
 		return
 	}
 	// TODO(gio): Create commit record on app init as well
@@ -690,8 +734,10 @@
 			fmt.Printf("Error: could not find commit message")
 			return
 		}
-		resources, err := s.updateDodoApp(instanceAppStatus, req.Repository.Name, s.appConfigs[req.Repository.Name].Namespace, networks)
-		if err = s.createCommit(req.Repository.Name, req.After, commitMsg, err, resources); err != nil {
+		s.l.Lock()
+		defer s.l.Unlock()
+		resources, err := s.updateDodoApp(instanceAppStatus, req.Repository.Name, branch, s.getAppConfig(req.Repository.Name, branch).Namespace, networks, owner)
+		if err = s.createCommit(req.Repository.Name, branch, req.After, commitMsg, err, resources); err != nil {
 			fmt.Printf("Error: %s\n", err.Error())
 			return
 		}
@@ -710,6 +756,7 @@
 }
 
 func (s *DodoAppServer) handleAPIRegisterWorker(w http.ResponseWriter, r *http.Request) {
+	// TODO(gio): lock
 	vars := mux.Vars(r)
 	appName, ok := vars["app-name"]
 	if !ok || appName == "" {
@@ -782,6 +829,35 @@
 	http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
 }
 
+func (s *DodoAppServer) handleCreateDevBranch(w http.ResponseWriter, r *http.Request) {
+	u := r.Context().Value(userCtx)
+	if u == nil {
+		http.Error(w, "unauthorized", http.StatusUnauthorized)
+		return
+	}
+	user, ok := u.(string)
+	if !ok {
+		http.Error(w, "could not get user", http.StatusInternalServerError)
+		return
+	}
+	vars := mux.Vars(r)
+	appName, ok := vars["app-name"]
+	if !ok || appName == "" {
+		http.Error(w, "missing app-name", http.StatusBadRequest)
+		return
+	}
+	branch := r.FormValue("branch")
+	if branch == "" {
+		http.Error(w, "missing network", http.StatusBadRequest)
+		return
+	}
+	if err := s.createDevBranch(appName, "master", branch, user); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	http.Redirect(w, r, fmt.Sprintf("/%s/branch/%s", appName, branch), http.StatusSeeOther)
+}
+
 type apiCreateAppReq struct {
 	AppType        string `json:"type"`
 	AdminPublicKey string `json:"adminPublicKey"`
@@ -889,7 +965,52 @@
 	if err != nil {
 		return err
 	}
-	commit, err := s.initRepo(appRepo, appType, n, subdomain)
+	files, err := s.renderAppConfigTemplate(appType, n, subdomain)
+	if err != nil {
+		return err
+	}
+	return s.createAppForBranch(appRepo, appName, "master", user, network, files)
+}
+
+func (s *DodoAppServer) createDevBranch(appName, fromBranch, toBranch, user string) error {
+	s.l.Lock()
+	defer s.l.Unlock()
+	fmt.Printf("Creating dev branch app: %s %s %s\n", appName, fromBranch, toBranch)
+	appRepo, err := s.client.GetRepoBranch(appName, fromBranch)
+	if err != nil {
+		return err
+	}
+	appCfg, err := soft.ReadFile(appRepo, "app.cue")
+	if err != nil {
+		return err
+	}
+	network, branchCfg, err := createDevBranchAppConfig(appCfg, toBranch, user)
+	if err != nil {
+		return err
+	}
+	return s.createAppForBranch(appRepo, appName, toBranch, user, network, map[string][]byte{"app.cue": branchCfg})
+}
+
+func (s *DodoAppServer) createAppForBranch(
+	repo soft.RepoIO,
+	appName string,
+	branch string,
+	user string,
+	network string,
+	files map[string][]byte,
+) error {
+	commit, err := repo.Do(func(fs soft.RepoFS) (string, error) {
+		for path, contents := range files {
+			if err := soft.WriteFile(fs, path, string(contents)); err != nil {
+				return "", err
+			}
+		}
+		return "init", nil
+	}, soft.WithCommitToBranch(branch))
+	if err != nil {
+		return err
+	}
+	networks, err := s.getNetworks(user)
 	if err != nil {
 		return err
 	}
@@ -908,12 +1029,13 @@
 		return err
 	}
 	namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, instanceApp.Namespace(), suffix)
-	s.appConfigs[appName] = appConfig{namespace, network}
-	resources, err := s.updateDodoApp(instanceAppStatus, appName, namespace, networks)
+	s.setAppConfig(appName, branch, appConfig{namespace, network})
+	resources, err := s.updateDodoApp(instanceAppStatus, appName, branch, namespace, networks, user)
 	if err != nil {
+		fmt.Printf("Error: %s\n", err.Error())
 		return err
 	}
-	if err = s.createCommit(appName, commit, initCommitMsg, err, resources); err != nil {
+	if err = s.createCommit(appName, branch, commit, initCommitMsg, err, resources); err != nil {
 		fmt.Printf("Error: %s\n", err.Error())
 		return err
 	}
@@ -926,6 +1048,7 @@
 	if err != nil {
 		return err
 	}
+	appPath := fmt.Sprintf("/%s/%s", appName, branch)
 	_, err = configRepo.Do(func(fs soft.RepoFS) (string, error) {
 		w, err := fs.Writer(appConfigsFile)
 		if err != nil {
@@ -938,11 +1061,12 @@
 		if _, err := m.Install(
 			instanceApp,
 			appName,
-			"/"+appName,
+			appPath,
 			namespace,
 			map[string]any{
 				"repoAddr":         s.client.GetRepoAddress(appName),
 				"repoHost":         strings.Split(s.client.Address(), ":")[0],
+				"branch":           fmt.Sprintf("dodo_%s", branch),
 				"gitRepoPublicKey": s.gitRepoPublicKey,
 			},
 			installer.WithConfig(&s.env),
@@ -957,7 +1081,11 @@
 	if err != nil {
 		return err
 	}
-	cfg, err := m.FindInstance(appName)
+	return s.initAppACLs(m, appPath, appName, branch, user)
+}
+
+func (s *DodoAppServer) initAppACLs(m *installer.AppManager, path, appName, branch, user string) error {
+	cfg, err := m.GetInstance(path)
 	if err != nil {
 		return err
 	}
@@ -980,13 +1108,16 @@
 			return err
 		}
 	}
+	if branch != "master" {
+		return nil
+	}
 	if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
 		return err
 	}
-	if err := s.client.AddWebhook(appName, fmt.Sprintf("http://%s/update", s.self), "--active=true", "--events=push", "--content-type=json"); err != nil {
+	if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
 		return err
 	}
-	if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
+	if err := s.client.AddWebhook(appName, fmt.Sprintf("http://%s/update", s.self), "--active=true", "--events=push", "--content-type=json"); err != nil {
 		return err
 	}
 	if !s.external {
@@ -1010,16 +1141,25 @@
 }
 
 type apiAddAdminKeyReq struct {
-	Public string `json:"public"`
+	User      string `json:"user"`
+	PublicKey string `json:"publicKey"`
 }
 
-func (s *DodoAppServer) handleAPIAddAdminKey(w http.ResponseWriter, r *http.Request) {
+func (s *DodoAppServer) handleAPIAddPublicKey(w http.ResponseWriter, r *http.Request) {
 	var req apiAddAdminKeyReq
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		http.Error(w, err.Error(), http.StatusBadRequest)
 		return
 	}
-	if err := s.client.AddPublicKey("admin", req.Public); err != nil {
+	if req.User == "" {
+		http.Error(w, "invalid user", http.StatusBadRequest)
+		return
+	}
+	if req.PublicKey == "" {
+		http.Error(w, "invalid public key", http.StatusBadRequest)
+		return
+	}
+	if err := s.client.AddPublicKey(req.User, req.PublicKey); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
@@ -1037,12 +1177,16 @@
 	} `json:"input"`
 }
 
+// TODO(gio): must not require owner, now we need it to bootstrap dev vm.
 func (s *DodoAppServer) updateDodoApp(
 	appStatus installer.EnvApp,
-	name, namespace string,
+	name string,
+	branch string,
+	namespace string,
 	networks []installer.Network,
+	owner string,
 ) (installer.ReleaseResources, error) {
-	repo, err := s.client.GetRepo(name)
+	repo, err := s.client.GetRepoBranch(name, branch)
 	if err != nil {
 		return installer.ReleaseResources{}, err
 	}
@@ -1068,10 +1212,13 @@
 			"/.dodo/app",
 			namespace,
 			map[string]any{
-				"repoAddr":      repo.FullAddress(),
-				"managerAddr":   fmt.Sprintf("http://%s", s.self),
-				"appId":         name,
-				"sshPrivateKey": s.sshKey,
+				"repoAddr":       repo.FullAddress(),
+				"repoPublicAddr": s.repoPublicAddr,
+				"managerAddr":    fmt.Sprintf("http://%s", s.self),
+				"appId":          name,
+				"branch":         branch,
+				"sshPrivateKey":  s.sshKey,
+				"username":       owner,
 			},
 			installer.WithNoPull(),
 			installer.WithNoPublish(),
@@ -1108,7 +1255,7 @@
 		}
 		return "install app", nil
 	},
-		soft.WithCommitToBranch("dodo"),
+		soft.WithCommitToBranch(fmt.Sprintf("dodo_%s", branch)),
 		soft.WithForce(),
 	); err != nil {
 		return installer.ReleaseResources{}, err
@@ -1118,18 +1265,13 @@
 	return ret, nil
 }
 
-func (s *DodoAppServer) initRepo(repo soft.RepoIO, appType string, network installer.Network, subdomain string) (string, error) {
+func (s *DodoAppServer) renderAppConfigTemplate(appType string, network installer.Network, subdomain string) (map[string][]byte, error) {
 	appType = strings.Replace(appType, ":", "-", 1)
 	appTmpl, err := s.appTmpls.Find(appType)
 	if err != nil {
-		return "", err
+		return nil, err
 	}
-	return repo.Do(func(fs soft.RepoFS) (string, error) {
-		if err := appTmpl.Render(network, subdomain, repo); err != nil {
-			return "", err
-		}
-		return initCommitMsg, nil
-	})
+	return appTmpl.Render(network, subdomain)
 }
 
 func generatePassword() string {
@@ -1183,10 +1325,10 @@
 	}
 }
 
-func (s *DodoAppServer) createCommit(name, hash, message string, err error, resources installer.ReleaseResources) error {
+func (s *DodoAppServer) createCommit(name, branch, hash, message string, err error, resources installer.ReleaseResources) error {
 	if err != nil {
 		fmt.Printf("Error: %s\n", err.Error())
-		if err := s.st.CreateCommit(name, hash, message, "FAILED", err.Error(), nil); err != nil {
+		if err := s.st.CreateCommit(name, branch, hash, message, "FAILED", err.Error(), nil); err != nil {
 			fmt.Printf("Error: %s\n", err.Error())
 			return err
 		}
@@ -1194,13 +1336,13 @@
 	}
 	var resB bytes.Buffer
 	if err := json.NewEncoder(&resB).Encode(resources); err != nil {
-		if err := s.st.CreateCommit(name, hash, message, "FAILED", err.Error(), nil); err != nil {
+		if err := s.st.CreateCommit(name, branch, hash, message, "FAILED", err.Error(), nil); err != nil {
 			fmt.Printf("Error: %s\n", err.Error())
 			return err
 		}
 		return err
 	}
-	if err := s.st.CreateCommit(name, hash, message, "OK", "", resB.Bytes()); err != nil {
+	if err := s.st.CreateCommit(name, branch, hash, message, "OK", "", resB.Bytes()); err != nil {
 		fmt.Printf("Error: %s\n", err.Error())
 		return err
 	}
@@ -1446,9 +1588,75 @@
 				return resourceData{}, fmt.Errorf("no host")
 			}
 			ret.Ingress = append(ret.Ingress, ingress{host})
+		case "virtual-machine":
+			name, ok := r.Annotations["dodo.cloud/resource.virtual-machine.name"]
+			if !ok {
+				return resourceData{}, fmt.Errorf("no name")
+			}
+			user, ok := r.Annotations["dodo.cloud/resource.virtual-machine.user"]
+			if !ok {
+				return resourceData{}, fmt.Errorf("no user")
+			}
+			cpuCoresS, ok := r.Annotations["dodo.cloud/resource.virtual-machine.cpu-cores"]
+			if !ok {
+				return resourceData{}, fmt.Errorf("no cpu cores")
+			}
+			cpuCores, err := strconv.Atoi(cpuCoresS)
+			if err != nil {
+				return resourceData{}, fmt.Errorf("invalid cpu cores: %s", cpuCoresS)
+			}
+			memory, ok := r.Annotations["dodo.cloud/resource.virtual-machine.memory"]
+			if !ok {
+				return resourceData{}, fmt.Errorf("no memory")
+			}
+			ret.VirtualMachine = append(ret.VirtualMachine, vm{name, user, cpuCores, memory})
 		default:
 			fmt.Printf("Unknown resource: %+v\n", r.Annotations)
 		}
 	}
 	return ret, nil
 }
+
+func createDevBranchAppConfig(from []byte, branch, username string) (string, []byte, error) {
+	cfg, err := installer.ParseCueAppConfig(installer.CueAppData{"app.cue": from})
+	if err != nil {
+		return "", nil, err
+	}
+	if err := cfg.Err(); err != nil {
+		return "", nil, err
+	}
+	if err := cfg.Validate(); err != nil {
+		return "", nil, err
+	}
+	subdomain := cfg.LookupPath(cue.ParsePath("app.ingress.subdomain"))
+	if err := subdomain.Err(); err != nil {
+		return "", nil, err
+	}
+	subdomainStr, err := subdomain.String()
+	network := cfg.LookupPath(cue.ParsePath("app.ingress.network"))
+	if err := network.Err(); err != nil {
+		return "", nil, err
+	}
+	networkStr, err := network.String()
+	if err != nil {
+		return "", nil, err
+	}
+	newCfg := map[string]any{}
+	if err := cfg.Decode(&newCfg); err != nil {
+		return "", nil, err
+	}
+	app, ok := newCfg["app"].(map[string]any)
+	if !ok {
+		return "", nil, fmt.Errorf("not a map")
+	}
+	app["ingress"].(map[string]any)["subdomain"] = fmt.Sprintf("%s-%s", branch, subdomainStr)
+	app["dev"] = map[string]any{
+		"enabled":  true,
+		"username": username,
+	}
+	buf, err := json.MarshalIndent(newCfg, "", "\t")
+	if err != nil {
+		return "", nil, err
+	}
+	return networkStr, buf, nil
+}
diff --git a/core/installer/welcome/dodo_app_test.go b/core/installer/welcome/dodo_app_test.go
new file mode 100644
index 0000000..0f0f526
--- /dev/null
+++ b/core/installer/welcome/dodo_app_test.go
@@ -0,0 +1,24 @@
+package welcome
+
+import (
+	"testing"
+)
+
+func TestCreateDevBranch(t *testing.T) {
+	cfg := []byte(`
+app: {
+	type: "golang:1.22.0"
+	run: "main.go"
+	ingress: {
+		network: "private"
+		subdomain: "testapp"
+		auth: enabled: false
+	}
+}`)
+	network, newCfg, err := createDevBranchAppConfig(cfg, "foo", "bar")
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Log(network)
+	t.Log(string(newCfg))
+}
diff --git a/core/installer/welcome/env_test.go b/core/installer/welcome/env_test.go
index f41110d..ee80e4e 100644
--- a/core/installer/welcome/env_test.go
+++ b/core/installer/welcome/env_test.go
@@ -15,6 +15,7 @@
 
 	"golang.org/x/crypto/ssh"
 
+	"cuelang.org/go/cue/errors"
 	"github.com/go-git/go-billy/v5"
 	"github.com/go-git/go-billy/v5/memfs"
 	"github.com/go-git/go-billy/v5/util"
@@ -117,6 +118,10 @@
 	return mockRepoIO{soft.NewBillyRepoFS(f.envFS), "foo.bar", f.t, &l}, nil
 }
 
+func (f fakeSoftServeClient) GetRepoBranch(name, branch string) (soft.RepoIO, error) {
+	return f.GetRepo(name)
+}
+
 func (f fakeSoftServeClient) GetAllRepos() ([]string, error) {
 	return []string{}, nil
 }
@@ -176,6 +181,14 @@
 	return nil
 }
 
+func (f fakeSoftServeClient) DisableAnonAccess() error {
+	return nil
+}
+
+func (f fakeSoftServeClient) DisableKeyless() error {
+	return nil
+}
+
 type fakeClientGetter struct {
 	t     *testing.T
 	envFS billy.Filesystem
@@ -279,6 +292,9 @@
 		if _, err := infraMgr.Install(app, "/infrastructure/dns-gateway", "dns-gateway", map[string]any{
 			"servers": []installer.EnvDNS{},
 		}); err != nil {
+			for _, e := range errors.Errors(err) {
+				t.Log(e)
+			}
 			t.Fatal(err)
 		}
 	}
diff --git a/core/installer/welcome/launcher.go b/core/installer/welcome/launcher.go
index a828f1a..88047d8 100644
--- a/core/installer/welcome/launcher.go
+++ b/core/installer/welcome/launcher.go
@@ -42,7 +42,7 @@
 }
 
 func (d *AppManagerDirectory) GetAllApps() ([]AppLauncherInfo, error) {
-	all, err := d.AppManager.FindAllInstances()
+	all, err := d.AppManager.GetAllInstances()
 	if err != nil {
 		return nil, err
 	}
diff --git a/core/installer/welcome/store.go b/core/installer/welcome/store.go
index 06be06a..3f8428d 100644
--- a/core/installer/welcome/store.go
+++ b/core/installer/welcome/store.go
@@ -40,9 +40,10 @@
 	GetUserApps(username string) ([]string, error)
 	CreateApp(name, username string) error
 	GetAppOwner(name string) (string, error)
-	CreateCommit(name, hash, message, status, error string, resources []byte) error
-	GetCommitHistory(name string) ([]CommitMeta, error)
+	CreateCommit(name, branch, hash, message, status, error string, resources []byte) error
+	GetCommitHistory(name, branch string) ([]CommitMeta, error)
 	GetCommit(hash string) (Commit, error)
+	GetBranches(name string) ([]string, error)
 }
 
 func NewStore(cf soft.RepoIO, db *sql.DB) (Store, error) {
@@ -71,6 +72,7 @@
 		);
 		CREATE TABLE IF NOT EXISTS commits (
 			app_name TEXT,
+			branch TEXT,
             hash TEXT,
             message TEXT,
             status TEXT,
@@ -186,15 +188,15 @@
 	return ret, nil
 }
 
-func (s *storeImpl) CreateCommit(name, hash, message, status, error string, resources []byte) error {
-	query := `INSERT INTO commits (app_name, hash, message, status, error, resources) VALUES (?, ?, ?, ?, ?, ?)`
-	_, err := s.db.Exec(query, name, hash, message, status, error, resources)
+func (s *storeImpl) CreateCommit(name, branch, hash, message, status, error string, resources []byte) error {
+	query := `INSERT INTO commits (app_name, branch, hash, message, status, error, resources) VALUES (?, ?, ?, ?, ?, ?, ?)`
+	_, err := s.db.Exec(query, name, branch, hash, message, status, error, resources)
 	return err
 }
 
-func (s *storeImpl) GetCommitHistory(name string) ([]CommitMeta, error) {
-	query := `SELECT hash, message, status, error FROM commits WHERE app_name = ?`
-	rows, err := s.db.Query(query, name)
+func (s *storeImpl) GetCommitHistory(name, branch string) ([]CommitMeta, error) {
+	query := `SELECT hash, message, status, error FROM commits WHERE app_name = ? AND branch = ?`
+	rows, err := s.db.Query(query, name, branch)
 	if err != nil {
 		return nil, err
 	}
@@ -231,3 +233,25 @@
 	}
 	return ret, nil
 }
+
+func (s *storeImpl) GetBranches(name string) ([]string, error) {
+	query := `SELECT DISTINCT branch FROM commits WHERE app_name = ?`
+	rows, err := s.db.Query(query, name)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	ret := []string{}
+	for rows.Next() {
+		if err := rows.Err(); err != nil {
+			return nil, err
+		}
+		var b string
+		if err := rows.Scan(&b); err != nil {
+			return nil, err
+		}
+		ret = append(ret, b)
+
+	}
+	return ret, nil
+}