installer: create individual soft-serve instances for each env
diff --git a/core/installer/bootstrapper.go b/core/installer/bootstrapper.go
index dcf7ea1..4ad44d7 100644
--- a/core/installer/bootstrapper.go
+++ b/core/installer/bootstrapper.go
@@ -7,9 +7,9 @@
 	"log"
 	"net/netip"
 	"path/filepath"
+	"strings"
 	"time"
 
-	"github.com/cenkalti/backoff/v4"
 	"helm.sh/helm/v3/pkg/action"
 	"helm.sh/helm/v3/pkg/chart"
 	"helm.sh/helm/v3/pkg/chart/loader"
@@ -41,7 +41,6 @@
 	if err := b.installLonghorn(env.Name, env.StorageDir, env.VolumeDefaultReplicaCount); err != nil {
 		return err
 	}
-	time.Sleep(1 * time.Minute) // TODO(giolekva): implement proper wait
 	bootstrapJobKeys, err := NewSSHKeyPair("bootstrapper")
 	if err != nil {
 		return err
@@ -49,22 +48,25 @@
 	if err := b.installSoftServe(bootstrapJobKeys.AuthorizedKey(), env.Name, env.ServiceIPs.ConfigRepo); err != nil {
 		return err
 	}
-	var ss *soft.Client
-	err = backoff.Retry(func() error {
-		var err error
-		ss, err = soft.NewClient(netip.AddrPortFrom(env.ServiceIPs.ConfigRepo, 22), bootstrapJobKeys.RawPrivateKey(), log.Default())
-		return err
-	}, backoff.NewConstantBackOff(5*time.Second))
+	ss, err := soft.WaitForClient(
+		netip.AddrPortFrom(env.ServiceIPs.ConfigRepo, 22).String(),
+		bootstrapJobKeys.RawPrivateKey(),
+		log.Default())
 	if err != nil {
 		return err
 	}
+	defer func() {
+		if ss.RemovePublicKey("admin", bootstrapJobKeys.AuthorizedKey()); err != nil {
+			fmt.Printf("Failed to remove admin public key: %s\n", err.Error())
+		}
+	}()
 	if ss.AddPublicKey("admin", string(env.AdminPublicKey)); err != nil {
 		return err
 	}
 	if err := b.installFluxcd(ss, env.Name); err != nil {
 		return err
 	}
-	repo, err := ss.GetRepo(env.Name)
+	repo, err := ss.GetRepo("config")
 	if err != nil {
 		return err
 	}
@@ -79,9 +81,6 @@
 	if err := b.installEnvManager(ss, repoIO, nsGen, b.ns, env); err != nil {
 		return err
 	}
-	if ss.RemovePublicKey("admin", bootstrapJobKeys.AuthorizedKey()); err != nil {
-		return err
-	}
 	return nil
 }
 
@@ -243,13 +242,13 @@
 	return nil
 }
 
-func (b Bootstrapper) installSoftServe(adminPublicKey string, envName string, repoIP netip.Addr) error {
+func (b Bootstrapper) installSoftServe(adminPublicKey string, namespace string, repoIP netip.Addr) error {
 	fmt.Println("Installing SoftServe")
 	keys, err := NewSSHKeyPair("soft-serve")
 	if err != nil {
 		return err
 	}
-	config, err := b.ha.New(envName)
+	config, err := b.ha.New(namespace)
 	if err != nil {
 		return err
 	}
@@ -263,13 +262,14 @@
 			"tag":        "v0.7.1",
 			"pullPolicy": "IfNotPresent",
 		},
-		"privateKey": string(keys.RawPrivateKey()),
-		"publicKey":  string(keys.RawAuthorizedKey()),
-		"adminKey":   adminPublicKey,
-		"reservedIP": repoIP.String(),
+		"privateKey":  string(keys.RawPrivateKey()),
+		"publicKey":   string(keys.RawAuthorizedKey()),
+		"adminKey":    adminPublicKey,
+		"reservedIP":  repoIP.String(),
+		"serviceType": "LoadBalancer",
 	}
 	installer := action.NewInstall(config)
-	installer.Namespace = envName
+	installer.Namespace = namespace
 	installer.CreateNamespace = true
 	installer.ReleaseName = "soft-serve"
 	installer.Wait = true
@@ -292,8 +292,15 @@
 	if err := ss.MakeUserAdmin("flux"); err != nil {
 		return err
 	}
-	fmt.Printf("Creating /%s repo", envName)
-	if err := ss.AddRepository(envName, "# dodo Systems"); err != nil {
+	if err := ss.AddRepository("config"); err != nil {
+		return err
+	}
+	repo, err := ss.GetRepo("config")
+	if err != nil {
+		return err
+	}
+	repoIO := NewRepoIO(repo, ss.Signer)
+	if err := repoIO.WriteCommitAndPush("README.md", fmt.Sprintf("# %s systems", envName), "readme"); err != nil {
 		return err
 	}
 	fmt.Println("Installing Flux")
@@ -301,9 +308,10 @@
 	if err != nil {
 		return err
 	}
+	host := strings.Split(ss.Addr, ":")[0]
 	if err := b.installFluxBootstrap(
-		ss.GetRepoAddress(envName),
-		ss.Addr.Addr().String(),
+		ss.GetRepoAddress("config"),
+		host,
 		string(ssPublic),
 		string(keys.RawPrivateKey()),
 		envName,
@@ -481,7 +489,7 @@
 		Values: map[string]any{
 			"RepoIP":        env.ServiceIPs.ConfigRepo,
 			"RepoPort":      22,
-			"RepoName":      env.Name,
+			"RepoName":      "config",
 			"SSHPrivateKey": string(keys.RawPrivateKey()),
 		},
 	}
diff --git a/core/installer/cmd/env_manager.go b/core/installer/cmd/env_manager.go
index 66bfb85..e177c08 100644
--- a/core/installer/cmd/env_manager.go
+++ b/core/installer/cmd/env_manager.go
@@ -2,7 +2,6 @@
 
 import (
 	"log"
-	"net/netip"
 
 	"github.com/spf13/cobra"
 
@@ -55,11 +54,7 @@
 	if err != nil {
 		return err
 	}
-	repoAddr, err := netip.ParseAddrPort(envManagerFlags.repoAddr)
-	if err != nil {
-		return err
-	}
-	ss, err := soft.NewClient(repoAddr, sshKey.RawPrivateKey(), log.Default())
+	ss, err := soft.WaitForClient(envManagerFlags.repoAddr, sshKey.RawPrivateKey(), log.Default())
 	if err != nil {
 		return err
 	}
diff --git a/core/installer/repoio.go b/core/installer/repoio.go
index 54d9a7d..204caec 100644
--- a/core/installer/repoio.go
+++ b/core/installer/repoio.go
@@ -7,7 +7,6 @@
 	"io/fs"
 	"io/ioutil"
 	"net"
-	"net/netip"
 	"path"
 	"path/filepath"
 	"time"
@@ -23,7 +22,7 @@
 )
 
 type RepoIO interface {
-	Addr() netip.AddrPort
+	Addr() string
 	Fetch() error
 	ReadConfig() (Config, error)
 	ReadAppConfig(path string) (AppConfig, error)
@@ -32,6 +31,7 @@
 	ReadYaml(path string) (any, error)
 	WriteYaml(path string, data any) error
 	CommitAndPush(message string) error
+	WriteCommitAndPush(path, contents, message string) error
 	Reader(path string) (io.ReadCloser, error)
 	Writer(path string) (io.WriteCloser, error)
 	CreateDir(path string) error
@@ -54,7 +54,7 @@
 	}
 }
 
-func (r *repoIO) Addr() netip.AddrPort {
+func (r *repoIO) Addr() string {
 	return r.repo.Addr.Addr
 }
 
@@ -161,6 +161,18 @@
 	return data, err
 }
 
+func (r *repoIO) WriteCommitAndPush(path, contents, message string) error {
+	w, err := r.Writer(path)
+	if err != nil {
+		return err
+	}
+	defer w.Close()
+	if _, err := io.WriteString(w, contents); err != nil {
+		return err
+	}
+	return r.CommitAndPush(message)
+}
+
 func (r *repoIO) CommitAndPush(message string) error {
 	wt, err := r.repo.Worktree()
 	if err != nil {
diff --git a/core/installer/soft/client.go b/core/installer/soft/client.go
index 4f0bf16..5686ba5 100644
--- a/core/installer/soft/client.go
+++ b/core/installer/soft/client.go
@@ -6,11 +6,12 @@
 	"golang.org/x/crypto/ssh"
 	"log"
 	"net"
-	"net/netip"
 	"os"
 	"regexp"
 	"strings"
+	"time"
 
+	"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/transport"
@@ -19,13 +20,13 @@
 )
 
 type Client struct {
-	Addr     netip.AddrPort
+	Addr     string
 	Signer   ssh.Signer
 	log      *log.Logger
 	pemBytes []byte
 }
 
-func NewClient(addr netip.AddrPort, clientPrivateKey []byte, log *log.Logger) (*Client, error) {
+func NewClient(addr string, clientPrivateKey []byte, log *log.Logger) (*Client, error) {
 	signer, err := ssh.ParsePrivateKey(clientPrivateKey)
 	if err != nil {
 		return nil, err
@@ -40,6 +41,24 @@
 	}, nil
 }
 
+func WaitForClient(addr string, clientPrivateKey []byte, log *log.Logger) (*Client, error) {
+	var client *Client
+	err := backoff.RetryNotify(func() error {
+		var err error
+		client, err = NewClient(addr, clientPrivateKey, log)
+		if err != nil {
+			return err
+		}
+		if _, err := client.GetPublicKey(); err != nil {
+			return err
+		}
+		return nil
+	}, backoff.NewConstantBackOff(5*time.Second), func(err error, _ time.Duration) {
+		log.Printf("Failed to create client:  %s\n", err.Error())
+	})
+	return client, err
+}
+
 func (ss *Client) AddUser(name, pubKey string) error {
 	log.Printf("Adding user %s", name)
 	if err := ss.RunCommand("user", "create", name); err != nil {
@@ -59,17 +78,18 @@
 }
 
 func (ss *Client) RemovePublicKey(user string, pubKey string) error {
-	log.Printf("Adding public key: %s %s\n", user, pubKey)
+	log.Printf("Removing public key: %s %s\n", user, pubKey)
 	return ss.RunCommand("user", "remove-pubkey", user, pubKey)
 }
 
 func (ss *Client) RunCommand(args ...string) error {
 	cmd := strings.Join(args, " ")
 	log.Printf("Running command %s", cmd)
-	client, err := ssh.Dial("tcp", ss.Addr.String(), ss.sshClientConfig())
+	client, err := ssh.Dial("tcp", ss.Addr, ss.sshClientConfig())
 	if err != nil {
 		return err
 	}
+	defer client.Close()
 	session, err := client.NewSession()
 	if err != nil {
 		return err
@@ -80,14 +100,19 @@
 	return session.Run(cmd)
 }
 
-func (ss *Client) AddRepository(name, readme string) error {
+func (ss *Client) AddRepository(name string) error {
 	log.Printf("Adding repository %s", name)
-	return ss.RunCommand("repo", "create", name, "-d", fmt.Sprintf("\"%s\"", readme))
+	return ss.RunCommand("repo", "create", name)
 }
 
-func (ss *Client) AddCollaborator(repo, user string) error {
-	log.Printf("Adding collaborator %s %s", repo, user)
-	return ss.RunCommand("repo", "collab", "add", repo, user)
+func (ss *Client) AddReadWriteCollaborator(repo, user string) error {
+	log.Printf("Adding read-write collaborator %s %s", repo, user)
+	return ss.RunCommand("repo", "collab", "add", repo, user, "read-write")
+}
+
+func (ss *Client) AddReadOnlyCollaborator(repo, user string) error {
+	log.Printf("Adding read-only collaborator %s %s", repo, user)
+	return ss.RunCommand("repo", "collab", "add", repo, user, "read-only")
 }
 
 type Repository struct {
@@ -100,7 +125,7 @@
 }
 
 type RepositoryAddress struct {
-	Addr netip.AddrPort
+	Addr string
 	Name string
 }
 
@@ -109,11 +134,7 @@
 	if len(items) != 3 {
 		return RepositoryAddress{}, fmt.Errorf("Invalid address")
 	}
-	ipPort, err := netip.ParseAddrPort(items[1])
-	if err != nil {
-		return RepositoryAddress{}, err
-	}
-	return RepositoryAddress{ipPort, items[2]}, nil
+	return RepositoryAddress{items[1], items[2]}, nil
 }
 
 func (r RepositoryAddress) FullAddress() string {
@@ -199,10 +220,11 @@
 			return nil
 		},
 	}
-	_, err := ssh.Dial("tcp", ss.Addr.String(), config)
+	client, err := ssh.Dial("tcp", ss.Addr, config)
 	if err != nil {
 		return nil, err
 	}
+	defer client.Close()
 	return ret, nil
 }
 
diff --git a/core/installer/values-tmpl/soft-serve.yaml b/core/installer/values-tmpl/soft-serve.yaml
index 529986b..1c6fa87 100644
--- a/core/installer/values-tmpl/soft-serve.yaml
+++ b/core/installer/values-tmpl/soft-serve.yaml
@@ -10,15 +10,23 @@
       sourceRef:
         kind: GitRepository
         name: pcloud
-        namespace: {{ .Global.Id }}
+        namespace: {{ or .Values.ChartRepositoryNamespace .Global.Id }}
   interval: 1m0s
   values:
+    {{- if .Values.ServiceType }}
+    serviceType: {{ .Values.ServiceType }}
+    {{- end }}
     reservedIP: ""
     addressPool: {{ .Global.Id }}
     adminKey: {{ .Values.AdminKey }}
+    {{- if and .Values.PrivateKey .Values.PublicKey }}
+    privateKey: |
+{{ .Values.PrivateKey | indent 6 }}
+    publicKey: {{ .Values.PublicKey }}
+    {{- end }}
     {{- if .Values.Network }}
     ingress:
-      enabled: true # TODO(giolekva): make it configurable
+      enabled: {{ .Values.Ingress.Enabled }}
       ingressClassName: {{ .Values.Network.IngressClass }}
       certificateIssuer: {{ .Values.Network.CertificateIssuer }}
       domain: {{ .Values.Subdomain }}.{{ .Values.Network.Domain }}
diff --git a/core/installer/welcome/create-env.html b/core/installer/welcome/create-env.html
index abaa197..d9f62c5 100644
--- a/core/installer/welcome/create-env.html
+++ b/core/installer/welcome/create-env.html
@@ -23,6 +23,12 @@
                       placeholder="Contact Email"
                       required
                     />
+                    <input
+                      type="string"
+                      name="admin-public-key"
+                      placeholder="Admin SSH Public Key"
+                      required
+                    /> <!-- TODO(gio): remove-->
                     <textarea
                       name="secret-token"
                       placeholder="Secret Token"
diff --git a/core/installer/welcome/env-tmpl/config-secret.yaml b/core/installer/welcome/env-tmpl/config-secret.yaml
index 3ea515b..bba3de0 100644
--- a/core/installer/welcome/env-tmpl/config-secret.yaml
+++ b/core/installer/welcome/env-tmpl/config-secret.yaml
@@ -1,10 +1,10 @@
 apiVersion: v1
+kind: Secret
+type: Opaque
+metadata:
+  name: {{ .Name }}
+  namespace: {{ .Name }}
 data:
   identity: {{ .PrivateKey }}
   identity.pub: {{ .PublicKey }}
   known_hosts: {{ .KnownHosts }}
-kind: Secret
-metadata:
-  name: {{ .Name }}
-  namespace: {{ .Name }}
-type: Opaque
diff --git a/core/installer/welcome/env-tmpl/config-source.yaml b/core/installer/welcome/env-tmpl/config-source.yaml
index 895c5e4..d22ab03 100644
--- a/core/installer/welcome/env-tmpl/config-source.yaml
+++ b/core/installer/welcome/env-tmpl/config-source.yaml
@@ -10,4 +10,4 @@
   secretRef:
     name: {{ .Name }}
   timeout: 60s
-  url: ssh://{{ .GitHost }}/{{ .Name }}
+  url: ssh://{{ .RepoHost }}/{{ .RepoName }}
diff --git a/core/installer/welcome/env-tmpl/kustomization.yaml b/core/installer/welcome/env-tmpl/kustomization.yaml
index 70db25f..070ae80 100644
--- a/core/installer/welcome/env-tmpl/kustomization.yaml
+++ b/core/installer/welcome/env-tmpl/kustomization.yaml
@@ -1,7 +1,7 @@
 apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 resources:
-- namespace.yaml
+- config-repo
 - config-secret.yaml
 - config-source.yaml
 - config-kustomization.yaml
diff --git a/core/installer/welcome/env-tmpl/namespace.yaml b/core/installer/welcome/env-tmpl/namespace.yaml
deleted file mode 100644
index 0c14654..0000000
--- a/core/installer/welcome/env-tmpl/namespace.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-apiVersion: v1
-kind: Namespace
-metadata:
-  name: {{ .Name }}
-  labels:
-    pcloud-instance-id: {{ .Name }}
-  annotations:
-    helm.sh/resource-policy: keep
diff --git a/core/installer/welcome/env.go b/core/installer/welcome/env.go
index 029f6df..aa3e061 100644
--- a/core/installer/welcome/env.go
+++ b/core/installer/welcome/env.go
@@ -4,12 +4,16 @@
 	"embed"
 	"encoding/base64"
 	"encoding/json"
+	"errors"
 	"fmt"
 	htemplate "html/template"
 	"io"
+	"io/fs"
 	"log"
 	"net/http"
 	"path"
+	"path/filepath"
+	"strings"
 	"text/template"
 
 	"github.com/charmbracelet/keygen"
@@ -99,15 +103,19 @@
 }
 
 type createEnvReq struct {
-	Name         string
-	ContactEmail string `json:"contactEmail"`
-	Domain       string `json:"domain"`
-	SecretToken  string `json:"secretToken"`
+	Name           string
+	ContactEmail   string `json:"contactEmail"`
+	Domain         string `json:"domain"`
+	AdminPublicKey string `json:"adminPublicKey"`
+	SecretToken    string `json:"secretToken"`
 }
 
 func (s *EnvServer) readInvitations() ([]invitation, error) {
 	r, err := s.repo.Reader("invitations")
 	if err != nil {
+		if errors.Is(err, fs.ErrNotExist) {
+			return make([]invitation, 0), nil
+		}
 		return nil, err
 	}
 	defer r.Close()
@@ -138,7 +146,7 @@
 	return s.repo.CommitAndPush("Generated new invitation")
 }
 
-func (s *EnvServer) createEnv(w http.ResponseWriter, r *http.Request) {
+func extractRequest(r *http.Request) (createEnvReq, error) {
 	var req createEnvReq
 	if err := func() error {
 		var err error
@@ -154,31 +162,55 @@
 		if req.ContactEmail, err = getFormValue(r.PostForm, "contact-email"); err != nil {
 			return err
 		}
+		if req.AdminPublicKey, err = getFormValue(r.PostForm, "admin-public-key"); err != nil {
+			return err
+		}
 		return nil
 	}(); err != nil {
 		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
+			return createEnvReq{}, err
 		}
 	}
+	return req, nil
+}
+
+func (s *EnvServer) acceptInvitation(token string) error {
 	invitations, err := s.readInvitations()
 	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
+		return err
 	}
 	found := false
-	for _, i := range invitations {
-		if i.Token == req.SecretToken && i.Status == StatusActive {
-			i.Status = StatusAccepted
+	for i := range invitations {
+		if invitations[i].Token == token && invitations[i].Status == StatusActive {
+			invitations[i].Status = StatusAccepted
 			found = true
 			break
 		}
 	}
 	if !found {
-		http.Error(w, "Invalid invitation", http.StatusNotFound)
+		return fmt.Errorf("Invitation not found")
+	}
+	return s.writeInvitations(invitations)
+}
+
+func (s *EnvServer) createEnv(w http.ResponseWriter, r *http.Request) {
+	req, err := extractRequest(r)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	if err := s.writeInvitations(invitations); err != nil {
+	var env installer.EnvConfig
+	cr, err := s.repo.Reader("config.yaml")
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	defer cr.Close()
+	if err := installer.ReadYaml(cr, &env); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	if err := s.acceptInvitation(req.SecretToken); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
@@ -188,6 +220,74 @@
 	} else {
 		req.Name = name
 	}
+	appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+	ssApp, err := appsRepo.Find("soft-serve")
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	ssAdminKeys, err := installer.NewSSHKeyPair(fmt.Sprintf("%s-config-repo-admin-keys", req.Name))
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	ssKeys, err := installer.NewSSHKeyPair(fmt.Sprintf("%s-config-repo-keys", req.Name))
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	ssValues := map[string]any{
+		"ChartRepositoryNamespace": env.Name,
+		"ServiceType":              "ClusterIP",
+		"PrivateKey":               string(ssKeys.RawPrivateKey()),
+		"PublicKey":                string(ssKeys.RawAuthorizedKey()),
+		"AdminKey":                 string(ssAdminKeys.RawAuthorizedKey()),
+		"Ingress": map[string]any{
+			"Enabled": false,
+		},
+	}
+	derived := installer.Derived{
+		Global: installer.Values{
+			Id:            req.Name,
+			PCloudEnvName: env.Name,
+		},
+		Release: installer.Release{
+			Namespace: req.Name,
+		},
+		Values: ssValues,
+	}
+	if err := s.nsCreator.Create(req.Name); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	if err := s.repo.InstallApp(*ssApp, filepath.Join("/environments", req.Name, "config-repo"), ssValues, derived); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	k := installer.NewKustomization()
+	k.AddResources("config-repo")
+	if err := s.repo.WriteKustomization(filepath.Join("/environments", req.Name, "kustomization.yaml"), k); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	ssClient, err := soft.WaitForClient(
+		fmt.Sprintf("soft-serve.%s.svc.cluster.local:%d", req.Name, 22),
+		ssAdminKeys.RawPrivateKey(),
+		log.Default())
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	if err := ssClient.AddPublicKey("admin", req.AdminPublicKey); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	defer func() {
+		if err := ssClient.RemovePublicKey("admin", string(ssAdminKeys.RawAuthorizedKey())); err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+	}()
 	fluxUserName := fmt.Sprintf("flux-%s", req.Name)
 	keys, err := installer.NewSSHKeyPair(fluxUserName)
 	if err != nil {
@@ -195,44 +295,42 @@
 		return
 	}
 	{
-		readme := fmt.Sprintf("# %s PCloud environment", req.Name)
-		if err := s.ss.AddRepository(req.Name, readme); err != nil {
+		if err := ssClient.AddRepository("config"); err != nil {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
-		if err := s.ss.AddUser(fluxUserName, keys.AuthorizedKey()); err != nil {
+		repo, err := ssClient.GetRepo("config")
+		if err != nil {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
-		if err := s.ss.AddCollaborator(req.Name, fluxUserName); err != nil {
+		repoIO := installer.NewRepoIO(repo, ssClient.Signer)
+		if err := repoIO.WriteCommitAndPush("README.md", fmt.Sprintf("# %s PCloud environment", req.Name), "readme"); err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		if err := ssClient.AddUser(fluxUserName, keys.AuthorizedKey()); err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		if err := ssClient.AddReadOnlyCollaborator("config", fluxUserName); err != nil {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
 	}
 	{
-		repo, err := s.ss.GetRepo(req.Name)
+		repo, err := ssClient.GetRepo("config")
 		if err != nil {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
-		var env installer.EnvConfig
-		r, err := s.repo.Reader("config.yaml")
-		if err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-		defer r.Close()
-		if err := installer.ReadYaml(r, &env); err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-		if err := initNewEnv(s.ss, installer.NewRepoIO(repo, s.ss.Signer), s.nsCreator, req, env); err != nil {
+		if err := initNewEnv(ssClient, installer.NewRepoIO(repo, ssClient.Signer), s.nsCreator, req, env.Name, env.PublicIP); err != nil {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
 	}
 	{
-		ssPubKey, err := s.ss.GetPublicKey()
+		ssPubKey, err := ssClient.GetPublicKey()
 		if err != nil {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
@@ -240,6 +338,7 @@
 		if err := addNewEnv(
 			s.repo,
 			req,
+			strings.Split(ssClient.Addr, ":")[0],
 			keys,
 			ssPubKey,
 		); err != nil {
@@ -265,7 +364,8 @@
 	r installer.RepoIO,
 	nsCreator installer.NamespaceCreator,
 	req createEnvReq,
-	env installer.EnvConfig,
+	pcloudEnvName string,
+	pcloudPublicIP string,
 ) error {
 	appManager, err := installer.NewAppManager(r, nsCreator)
 	if err != nil {
@@ -275,12 +375,12 @@
 	// TODO(giolekva): private domain can be configurable as well
 	config := installer.Config{
 		Values: installer.Values{
-			PCloudEnvName:   env.Name,
+			PCloudEnvName:   pcloudEnvName,
 			Id:              req.Name,
 			ContactEmail:    req.ContactEmail,
 			Domain:          req.Domain,
 			PrivateDomain:   fmt.Sprintf("p.%s", req.Domain),
-			PublicIP:        env.PublicIP,
+			PublicIP:        pcloudPublicIP,
 			NamespacePrefix: fmt.Sprintf("%s-", req.Name),
 		},
 	}
@@ -344,15 +444,6 @@
 		}); err != nil {
 			return err
 		}
-		if err := appManager.Install(*app, nsGen, installer.NewSuffixGenerator("-soft-serve"), map[string]any{
-			"Name":       fmt.Sprintf("%s-soft-serve", req.Name), // TODO(giolekva): rename to config repo
-			"From":       "10.1.0.3",
-			"To":         "10.1.0.3",
-			"AutoAssign": false,
-			"Namespace":  "metallb-system",
-		}); err != nil {
-			return err
-		}
 		if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
 			"Name":       req.Name,
 			"From":       "10.1.0.100",
@@ -412,7 +503,7 @@
 		if err := ss.AddUser(user, keys.AuthorizedKey()); err != nil {
 			return err
 		}
-		if err := ss.AddCollaborator(req.Name, user); err != nil {
+		if err := ss.AddReadWriteCollaborator("config", user); err != nil {
 			return err
 		}
 		app, err := appsRepo.Find("welcome")
@@ -420,7 +511,7 @@
 			return err
 		}
 		if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
-			"RepoAddr":      ss.GetRepoAddress(req.Name),
+			"RepoAddr":      ss.GetRepoAddress("config"),
 			"SSHPrivateKey": string(keys.RawPrivateKey()),
 		}); err != nil {
 			return err
@@ -435,7 +526,7 @@
 		if err := ss.AddUser(user, keys.AuthorizedKey()); err != nil {
 			return err
 		}
-		if err := ss.AddCollaborator(req.Name, user); err != nil {
+		if err := ss.AddReadWriteCollaborator("config", user); err != nil {
 			return err
 		}
 		app, err := appsRepo.Find("app-manager") // TODO(giolekva): configure
@@ -443,7 +534,7 @@
 			return err
 		}
 		if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
-			"RepoAddr":      ss.GetRepoAddress(req.Name),
+			"RepoAddr":      ss.GetRepoAddress("config"),
 			"SSHPrivateKey": string(keys.RawPrivateKey()),
 		}); err != nil {
 			return err
@@ -455,6 +546,7 @@
 func addNewEnv(
 	repoIO installer.RepoIO,
 	req createEnvReq,
+	repoHost string,
 	keys *keygen.KeyPair,
 	pcloudRepoPublicKey []byte,
 ) error {
@@ -467,7 +559,6 @@
 	if err != nil {
 		return err
 	}
-	repoIP := repoIO.Addr().Addr().String()
 	for _, tmpl := range tmpls.Templates() {
 		dstPath := path.Join("environments", req.Name, tmpl.Name())
 		dst, err := repoIO.Writer(dstPath)
@@ -479,8 +570,9 @@
 			"Name":       req.Name,
 			"PrivateKey": base64.StdEncoding.EncodeToString(keys.RawPrivateKey()),
 			"PublicKey":  base64.StdEncoding.EncodeToString(keys.RawAuthorizedKey()),
-			"GitHost":    repoIP,
-			"KnownHosts": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s %s", repoIP, pcloudRepoPublicKey))),
+			"RepoHost":   repoHost,
+			"RepoName":   "config",
+			"KnownHosts": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s %s", repoHost, pcloudRepoPublicKey))),
 		}); err != nil {
 			return err
 		}