installer: env form
diff --git a/core/installer/cmd/env.go b/core/installer/cmd/env.go
deleted file mode 100644
index 045b258..0000000
--- a/core/installer/cmd/env.go
+++ /dev/null
@@ -1,268 +0,0 @@
-// TODO
-// * flux -n lekva create source git pcloud --url=ssh://192.168.0.211/pcloud-apps --branch=main --private-key-file=/Users/lekva/.ssh/id_rsa
-
-package main
-
-import (
-	"embed"
-	"encoding/base64"
-	"fmt"
-	"github.com/spf13/cobra"
-	"log"
-	"os"
-	"path"
-	"text/template"
-
-	"github.com/giolekva/pcloud/core/installer"
-	"github.com/giolekva/pcloud/core/installer/soft"
-)
-
-//go:embed env-tmpl
-var filesTmpls embed.FS
-
-var createEnvFlags struct {
-	name          string
-	ip            string
-	port          int
-	adminPrivKey  string
-	adminUsername string
-}
-
-func createEnvCmd() *cobra.Command {
-	cmd := &cobra.Command{
-		Use:  "create-env",
-		RunE: createEnvCmdRun,
-	}
-	cmd.Flags().StringVar(
-		&createEnvFlags.name,
-		"name",
-		"",
-		"",
-	)
-	cmd.Flags().StringVar(
-		&createEnvFlags.ip,
-		"ip",
-		"",
-		"",
-	)
-	cmd.Flags().IntVar(
-		&createEnvFlags.port,
-		"port",
-		22,
-		"",
-	)
-	cmd.Flags().StringVar(
-		&createEnvFlags.adminPrivKey,
-		"admin-priv-key",
-		"",
-		"",
-	)
-	cmd.Flags().StringVar(
-		&createEnvFlags.adminUsername,
-		"admin-username",
-		"",
-		"",
-	)
-	return cmd
-}
-
-func createEnvCmdRun(cmd *cobra.Command, args []string) error {
-	adminPrivKey, err := os.ReadFile(createEnvFlags.adminPrivKey)
-	if err != nil {
-		return err
-	}
-	ss, err := soft.NewClient(createEnvFlags.ip, createEnvFlags.port, adminPrivKey, log.Default())
-	if err != nil {
-		return err
-	}
-	ssPubKey, err := ss.GetPublicKey()
-	if err != nil {
-		return err
-	}
-	keys, err := installer.NewSSHKeyPair()
-	if err != nil {
-		return err
-	}
-	if 1 == 2 {
-		readme := fmt.Sprintf("# %s PCloud environment", createEnvFlags.name)
-		if err := ss.AddRepository(createEnvFlags.name, readme); err != nil {
-			return err
-		}
-		fluxUserName := fmt.Sprintf("flux-%s", createEnvFlags.name)
-		if err := ss.AddUser(fluxUserName, keys.Public); err != nil {
-			return err
-		}
-		if err := ss.AddCollaborator(createEnvFlags.name, fluxUserName); err != nil {
-			return err
-		}
-	}
-	envRepo, err := ss.GetRepo(createEnvFlags.name)
-	if envRepo == nil {
-		return err
-	}
-	if err := initEnvRepo(installer.NewRepoIO(envRepo, ss.Signer)); err != nil {
-		return err
-	}
-	if 1 == 2 {
-		repo, err := ss.GetRepo("pcloud")
-		if err != nil {
-			return err
-		}
-		repoIO := installer.NewRepoIO(repo, ss.Signer)
-		kust, err := repoIO.ReadKustomization("environments/kustomization.yaml")
-		if err != nil {
-			return err
-		}
-		kust.AddResources(createEnvFlags.name)
-		tmpls, err := template.ParseFS(filesTmpls, "env-tmpl/*.yaml")
-		if err != nil {
-			return err
-		}
-		for _, tmpl := range tmpls.Templates() {
-			dstPath := path.Join("environments", createEnvFlags.name, tmpl.Name())
-			dst, err := repoIO.Writer(dstPath)
-			if err != nil {
-				return err
-			}
-			defer dst.Close()
-			if err := tmpl.Execute(dst, map[string]string{
-				"Name":       createEnvFlags.name,
-				"PrivateKey": base64.StdEncoding.EncodeToString([]byte(keys.Private)),
-				"PublicKey":  base64.StdEncoding.EncodeToString([]byte(keys.Public)),
-				"GitHost":    createEnvFlags.ip,
-				"KnownHosts": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s %s", createEnvFlags.ip, ssPubKey))),
-			}); err != nil {
-				return err
-			}
-		}
-		if err := repoIO.WriteKustomization("environments/kustomization.yaml", *kust); err != nil {
-			return err
-		}
-		if err := repoIO.CommitAndPush(fmt.Sprintf("%s: initialize environment", createEnvFlags.name)); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func initEnvRepo(r installer.RepoIO) error {
-	appManager, err := installer.NewAppManager(r)
-	if err != nil {
-		return err
-	}
-	appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
-	if 1 == 2 {
-		config := installer.Config{ // TODO(gioleka): configurable
-			Values: installer.Values{
-				PCloudEnvName:   "pcloud",
-				Id:              "lekva",
-				ContactEmail:    "giolekva@gmail.com",
-				Domain:          "lekva.me",
-				PrivateDomain:   "p.lekva.me",
-				PublicIP:        "46.49.35.44",
-				NamespacePrefix: "lekva-",
-			},
-		}
-		if err := r.WriteYaml("config.yaml", config); err != nil {
-			return err
-		}
-		{
-			out, err := r.Writer("pcloud-charts.yaml")
-			if err != nil {
-				return err
-			}
-			defer out.Close()
-			_, err = out.Write([]byte(`
-apiVersion: source.toolkit.fluxcd.io/v1beta2
-kind: GitRepository
-metadata:
-  name: pcloud
-  namespace: lekva
-spec:
-  interval: 1m0s
-  url: https://github.com/giolekva/pcloud
-  ref:
-    branch: main
-`))
-			if err != nil {
-				return err
-			}
-		}
-		rootKust := installer.NewKustomization()
-		rootKust.AddResources("pcloud-charts.yaml", "apps")
-		if err := r.WriteKustomization("kustomization.yaml", rootKust); err != nil {
-			return err
-		}
-		appsKust := installer.NewKustomization()
-		if err := r.WriteKustomization("apps/kustomization.yaml", appsKust); err != nil {
-			return err
-		}
-		r.CommitAndPush("initialize config")
-		{
-			app, err := appsRepo.Find("metallb-config-env")
-			if err != nil {
-				return err
-			}
-			if err := appManager.Install(*app, map[string]any{
-				"IngressPrivate": "10.1.0.1",
-				"Headscale":      "10.1.0.2",
-				"SoftServe":      "10.1.0.3",
-				"Rest": map[string]any{
-					"From": "10.1.0.100",
-					"To":   "10.1.0.255",
-				},
-			}); err != nil {
-				return err
-			}
-		}
-		{
-			app, err := appsRepo.Find("ingress-private")
-			if err != nil {
-				return err
-			}
-			if err := appManager.Install(*app, map[string]any{
-				"GandiAPIToken": "", // TODO(gioleka): configurable
-			}); err != nil {
-				return err
-			}
-		}
-		{
-			app, err := appsRepo.Find("core-auth")
-			if err != nil {
-				return err
-			}
-			if err := appManager.Install(*app, map[string]any{
-				"Subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
-			}); err != nil {
-				return err
-			}
-		}
-	}
-	{
-		app, err := appsRepo.Find("headscale")
-		if err != nil {
-			return err
-		}
-		if err := appManager.Install(*app, map[string]any{
-			"Subdomain": "headscale",
-		}); err != nil {
-			return err
-		}
-	}
-	if 1 == 2 {
-		{
-			app, err := appsRepo.Find("tailscale-proxy")
-			if err != nil {
-				return err
-			}
-			if err := appManager.Install(*app, map[string]any{
-				"Username": createEnvFlags.adminUsername,
-				"IPSubnet": "10.1.0.0/24",
-			}); err != nil {
-				return err
-			}
-			// TODO(giolekva): headscale accept routes
-		}
-	}
-	return nil
-}
diff --git a/core/installer/cmd/env_manager.go b/core/installer/cmd/env_manager.go
index 33b4058..647850d 100644
--- a/core/installer/cmd/env_manager.go
+++ b/core/installer/cmd/env_manager.go
@@ -1,19 +1,14 @@
 package main
 
 import (
-	"encoding/base64"
-	"encoding/json"
-	"fmt"
 	"log"
 	"os"
-	"path"
-	"text/template"
 
-	"github.com/labstack/echo/v4"
 	"github.com/spf13/cobra"
 
 	"github.com/giolekva/pcloud/core/installer"
 	"github.com/giolekva/pcloud/core/installer/soft"
+	"github.com/giolekva/pcloud/core/installer/welcome"
 )
 
 var envManagerFlags struct {
@@ -60,277 +55,20 @@
 	if err != nil {
 		return err
 	}
-	fmt.Println(string(sshKey))
 	ss, err := soft.NewClient(envManagerFlags.repoIP, envManagerFlags.repoPort, sshKey, log.Default())
 	if err != nil {
 		return err
 	}
-	b, err := ss.GetPublicKey()
-	if err != nil {
-		return err
-	}
-	fmt.Println(string(b))
-	fmt.Println(111)
 	repo, err := ss.GetRepo("pcloud")
-	fmt.Println(222)
 	if err != nil {
 		return err
 	}
-	fmt.Println(333)
 	repoIO := installer.NewRepoIO(repo, ss.Signer)
-	s := &envServer{
-		port: envManagerFlags.port,
-		ss:   ss,
-		repo: repoIO,
-	}
-	s.start()
-	return nil
-}
-
-type envServer struct {
-	port int
-	ss   *soft.Client
-	repo installer.RepoIO
-}
-
-func (s *envServer) start() {
-	e := echo.New()
-	e.POST("/env", s.createEnv)
-	log.Fatal(e.Start(fmt.Sprintf(":%d", s.port)))
-}
-
-type createEnvReq struct {
-	Name         string `json:"name"`
-	ContactEmail string `json:"contactEmail"`
-	Domain       string `json:"domain"`
-}
-
-func (s *envServer) createEnv(c echo.Context) error {
-	var req createEnvReq
-	if err := json.NewDecoder(c.Request().Body).Decode(&req); err != nil {
-		return err
-	}
-	keys, err := installer.NewSSHKeyPair()
-	if err != nil {
-		return err
-	}
-	{
-		readme := fmt.Sprintf("# %s PCloud environment", req.Name)
-		if err := s.ss.AddRepository(req.Name, readme); err != nil {
-			return err
-		}
-		fluxUserName := fmt.Sprintf("flux-%s", req.Name)
-		if err := s.ss.AddUser(fluxUserName, keys.Public); err != nil {
-			return err
-		}
-		if err := s.ss.AddCollaborator(req.Name, fluxUserName); err != nil {
-			return err
-		}
-	}
-	{
-		repo, err := s.ss.GetRepo(req.Name)
-		if repo == nil {
-			return err
-		}
-		if err := initNewEnv(s.ss, installer.NewRepoIO(repo, s.ss.Signer), req); err != nil {
-			return err
-		}
-	}
-	{
-		repo, err := s.ss.GetRepo("pcloud")
-		if err != nil {
-			return err
-		}
-		ssPubKey, err := s.ss.GetPublicKey()
-		if err != nil {
-			return err
-		}
-		if err := addNewEnv(
-			installer.NewRepoIO(repo, s.ss.Signer),
-			req,
-			keys,
-			ssPubKey,
-		); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func initNewEnv(ss *soft.Client, r installer.RepoIO, req createEnvReq) error {
-	appManager, err := installer.NewAppManager(r)
-	if err != nil {
-		return err
-	}
-	appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
-	// TODO(giolekva): env name and ip should come from pcloud repo config.yaml
-	// TODO(giolekva): private domain can be configurable as well
-	config := installer.Config{
-		Values: installer.Values{
-			PCloudEnvName:   "pcloud",
-			Id:              req.Name,
-			ContactEmail:    req.ContactEmail,
-			Domain:          req.Domain,
-			PrivateDomain:   fmt.Sprintf("p.%s", req.Domain),
-			PublicIP:        "46.49.35.44",
-			NamespacePrefix: fmt.Sprintf("%s-", req.Name),
-		},
-	}
-	if err := r.WriteYaml("config.yaml", config); err != nil {
-		return err
-	}
-	{
-		out, err := r.Writer("pcloud-charts.yaml")
-		if err != nil {
-			return err
-		}
-		defer out.Close()
-		_, err = out.Write([]byte(`
-apiVersion: source.toolkit.fluxcd.io/v1beta2
-kind: GitRepository
-metadata:
-  name: pcloud
-  namespace: lekva
-spec:
-  interval: 1m0s
-  url: https://github.com/giolekva/pcloud
-  ref:
-    branch: main
-`))
-		if err != nil {
-			return err
-		}
-	}
-	rootKust := installer.NewKustomization()
-	rootKust.AddResources("pcloud-charts.yaml", "apps")
-	if err := r.WriteKustomization("kustomization.yaml", rootKust); err != nil {
-		return err
-	}
-	appsKust := installer.NewKustomization()
-	if err := r.WriteKustomization("apps/kustomization.yaml", appsKust); err != nil {
-		return err
-	}
-	r.CommitAndPush("initialize config")
-	{
-		app, err := appsRepo.Find("metallb-config-env")
-		if err != nil {
-			return err
-		}
-		if err := appManager.Install(*app, map[string]any{
-			"IngressPrivate": "10.1.0.1",
-			"Headscale":      "10.1.0.2",
-			"SoftServe":      "10.1.0.3",
-			"Rest": map[string]any{
-				"From": "10.1.0.100",
-				"To":   "10.1.0.255",
-			},
-		}); err != nil {
-			return err
-		}
-	}
-	{
-		app, err := appsRepo.Find("ingress-private")
-		if err != nil {
-			return err
-		}
-		if err := appManager.Install(*app, map[string]any{}); err != nil {
-			return err
-		}
-	}
-	{
-		app, err := appsRepo.Find("certificate-issuer-public")
-		if err != nil {
-			return err
-		}
-		if err := appManager.Install(*app, map[string]any{}); err != nil {
-			return err
-		}
-	}
-	{
-		app, err := appsRepo.Find("core-auth")
-		if err != nil {
-			return err
-		}
-		if err := appManager.Install(*app, map[string]any{
-			"Subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
-		}); err != nil {
-			return err
-		}
-	}
-	{
-		app, err := appsRepo.Find("headscale")
-		if err != nil {
-			return err
-		}
-		if err := appManager.Install(*app, map[string]any{
-			"Subdomain": "headscale",
-		}); err != nil {
-			return err
-		}
-	}
-	{
-		keys, err := installer.NewSSHKeyPair()
-		if err != nil {
-			return err
-		}
-		user := fmt.Sprintf("%s-welcome", req.Name)
-		if err := ss.AddUser(user, keys.Public); err != nil {
-			return err
-		}
-		if err := ss.AddCollaborator(req.Name, user); err != nil {
-			return err
-		}
-		app, err := appsRepo.Find("welcome")
-		if err != nil {
-			return err
-		}
-		if err := appManager.Install(*app, map[string]any{
-			"RepoAddr":      ss.GetRepoAddress(req.Name),
-			"SSHPrivateKey": keys.Private,
-		}); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func addNewEnv(
-	repoIO installer.RepoIO,
-	req createEnvReq,
-	keys installer.KeyPair,
-	pcloudRepoPublicKey []byte,
-) error {
-	kust, err := repoIO.ReadKustomization("environments/kustomization.yaml")
-	if err != nil {
-		return err
-	}
-	kust.AddResources(req.Name)
-	tmpls, err := template.ParseFS(filesTmpls, "env-tmpl/*.yaml")
-	if err != nil {
-		return err
-	}
-	for _, tmpl := range tmpls.Templates() {
-		dstPath := path.Join("environments", req.Name, tmpl.Name())
-		dst, err := repoIO.Writer(dstPath)
-		if err != nil {
-			return err
-		}
-		defer dst.Close()
-		if err := tmpl.Execute(dst, map[string]string{
-			"Name":       req.Name,
-			"PrivateKey": base64.StdEncoding.EncodeToString([]byte(keys.Private)),
-			"PublicKey":  base64.StdEncoding.EncodeToString([]byte(keys.Public)),
-			"GitHost":    envManagerFlags.repoIP,
-			"KnownHosts": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s %s", envManagerFlags.repoIP, pcloudRepoPublicKey))),
-		}); err != nil {
-			return err
-		}
-	}
-	if err := repoIO.WriteKustomization("environments/kustomization.yaml", *kust); err != nil {
-		return err
-	}
-	if err := repoIO.CommitAndPush(fmt.Sprintf("%s: initialize environment", req.Name)); err != nil {
-		return err
-	}
+	s := welcome.NewEnvServer(
+		envManagerFlags.port,
+		ss,
+		repoIO,
+	)
+	s.Start()
 	return nil
 }
diff --git a/core/installer/cmd/main.go b/core/installer/cmd/main.go
index 8a5e04b..e879c62 100644
--- a/core/installer/cmd/main.go
+++ b/core/installer/cmd/main.go
@@ -23,7 +23,6 @@
 		"",
 	)
 	rootCmd.AddCommand(bootstrapCmd())
-	rootCmd.AddCommand(createEnvCmd())
 	rootCmd.AddCommand(installCmd())
 	rootCmd.AddCommand(appManagerCmd())
 	rootCmd.AddCommand(envManagerCmd())
diff --git a/core/installer/soft/client.go b/core/installer/soft/client.go
index 59e94a8..2a820a0 100644
--- a/core/installer/soft/client.go
+++ b/core/installer/soft/client.go
@@ -1,7 +1,6 @@
 package soft
 
 import (
-	"encoding/base64"
 	"fmt"
 	"golang.org/x/crypto/ssh"
 	"log"
@@ -30,10 +29,6 @@
 	}
 	log.SetPrefix("SOFT-SERVE: ")
 	log.Printf("Created signer")
-	pub := signer.PublicKey().Marshal()
-	b := make([]byte, 100)
-	base64.StdEncoding.Encode(b, pub)
-	log.Printf("%s\n", string(b))
 	return &Client{
 		ip,
 		port,
diff --git a/core/installer/welcome/index.html b/core/installer/welcome/create-admin-account.html
similarity index 100%
rename from core/installer/welcome/index.html
rename to core/installer/welcome/create-admin-account.html
diff --git a/core/installer/welcome/create-env.html b/core/installer/welcome/create-env.html
new file mode 100644
index 0000000..6633d65
--- /dev/null
+++ b/core/installer/welcome/create-env.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html lang="en" data-theme="light">
+	<head>
+        <link rel="stylesheet" href="/static/pico.min.css">
+		<meta charset="utf-8" />
+		<meta name="viewport" content="width=device-width, initial-scale=1" />
+	</head>
+	<body>
+		<div style="display: contents">
+            <main class="container">
+              <article class="grid">
+                <div>
+                  <form action="/env" method="POST">
+                    <input
+                      type="text"
+                      name="name"
+                      placeholder="Name"
+                      required
+                    />
+                    <input
+                      type="test"
+                      name="domain"
+                      placeholder="Domain"
+                      required
+                      />
+                    <input
+                      type="email"
+                      name="contact-email"
+                      placeholder="Contact Email"
+                      required
+                      />
+                    <button type="submit" class="contrast">Create Environment</button>
+                  </form>
+                </div>
+              </article>
+            </main>
+        </div>
+	</body>
+</html>
diff --git a/core/installer/cmd/env-tmpl/config-kustomization.yaml b/core/installer/welcome/env-tmpl/config-kustomization.yaml
similarity index 100%
rename from core/installer/cmd/env-tmpl/config-kustomization.yaml
rename to core/installer/welcome/env-tmpl/config-kustomization.yaml
diff --git a/core/installer/cmd/env-tmpl/config-secret.yaml b/core/installer/welcome/env-tmpl/config-secret.yaml
similarity index 100%
rename from core/installer/cmd/env-tmpl/config-secret.yaml
rename to core/installer/welcome/env-tmpl/config-secret.yaml
diff --git a/core/installer/cmd/env-tmpl/config-source.yaml b/core/installer/welcome/env-tmpl/config-source.yaml
similarity index 100%
rename from core/installer/cmd/env-tmpl/config-source.yaml
rename to core/installer/welcome/env-tmpl/config-source.yaml
diff --git a/core/installer/cmd/env-tmpl/kustomization.yaml b/core/installer/welcome/env-tmpl/kustomization.yaml
similarity index 100%
rename from core/installer/cmd/env-tmpl/kustomization.yaml
rename to core/installer/welcome/env-tmpl/kustomization.yaml
diff --git a/core/installer/cmd/env-tmpl/namespace.yaml b/core/installer/welcome/env-tmpl/namespace.yaml
similarity index 100%
rename from core/installer/cmd/env-tmpl/namespace.yaml
rename to core/installer/welcome/env-tmpl/namespace.yaml
diff --git a/core/installer/welcome/env.go b/core/installer/welcome/env.go
new file mode 100644
index 0000000..e871830
--- /dev/null
+++ b/core/installer/welcome/env.go
@@ -0,0 +1,304 @@
+package welcome
+
+import (
+	"embed"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/http"
+	"path"
+	"text/template"
+
+	"github.com/labstack/echo/v4"
+
+	"github.com/giolekva/pcloud/core/installer"
+	"github.com/giolekva/pcloud/core/installer/soft"
+)
+
+//go:embed env-tmpl
+var filesTmpls embed.FS
+
+//go:embed create-env.html
+var createEnvFormHtml string
+
+type EnvServer struct {
+	port int
+	ss   *soft.Client
+	repo installer.RepoIO
+}
+
+func NewEnvServer(port int, ss *soft.Client, repo installer.RepoIO) *EnvServer {
+	return &EnvServer{
+		port,
+		ss,
+		repo,
+	}
+}
+
+func (s *EnvServer) Start() {
+	e := echo.New()
+	e.StaticFS("/static", echo.MustSubFS(staticAssets, "static"))
+	e.GET("/env", s.createEnvForm)
+	e.POST("/env", s.createEnv)
+	log.Fatal(e.Start(fmt.Sprintf(":%d", s.port)))
+}
+
+func (s *EnvServer) createEnvForm(c echo.Context) error {
+	return c.HTML(http.StatusOK, createEnvFormHtml)
+}
+
+type createEnvReq struct {
+	Name         string `json:"name"`
+	ContactEmail string `json:"contactEmail"`
+	Domain       string `json:"domain"`
+}
+
+func (s *EnvServer) createEnv(c echo.Context) error {
+	var req createEnvReq
+	if err := func() error {
+		var err error
+		f, err := c.FormParams()
+		if err != nil {
+			return err
+		}
+		if req.Name, err = getFormValue(f, "name"); err != nil {
+			return err
+		}
+		if req.Domain, err = getFormValue(f, "domain"); err != nil {
+			return err
+		}
+		if req.ContactEmail, err = getFormValue(f, "contact-email"); err != nil {
+			return err
+		}
+		return nil
+	}(); err != nil {
+		if err := json.NewDecoder(c.Request().Body).Decode(&req); err != nil {
+			return err
+		}
+	}
+	keys, err := installer.NewSSHKeyPair()
+	if err != nil {
+		return err
+	}
+	{
+		readme := fmt.Sprintf("# %s PCloud environment", req.Name)
+		if err := s.ss.AddRepository(req.Name, readme); err != nil {
+			return err
+		}
+		fluxUserName := fmt.Sprintf("flux-%s", req.Name)
+		if err := s.ss.AddUser(fluxUserName, keys.Public); err != nil {
+			return err
+		}
+		if err := s.ss.AddCollaborator(req.Name, fluxUserName); err != nil {
+			return err
+		}
+	}
+	{
+		repo, err := s.ss.GetRepo(req.Name)
+		if repo == nil {
+			return err
+		}
+		if err := initNewEnv(s.ss, installer.NewRepoIO(repo, s.ss.Signer), req); err != nil {
+			return err
+		}
+	}
+	{
+		repo, err := s.ss.GetRepo("pcloud")
+		if err != nil {
+			return err
+		}
+		ssPubKey, err := s.ss.GetPublicKey()
+		if err != nil {
+			return err
+		}
+		if err := addNewEnv(
+			installer.NewRepoIO(repo, s.ss.Signer),
+			req,
+			keys,
+			ssPubKey,
+		); err != nil {
+			return err
+		}
+	}
+	return c.String(http.StatusOK, "OK")
+}
+
+func initNewEnv(ss *soft.Client, r installer.RepoIO, req createEnvReq) error {
+	appManager, err := installer.NewAppManager(r)
+	if err != nil {
+		return err
+	}
+	appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+	// TODO(giolekva): env name and ip should come from pcloud repo config.yaml
+	// TODO(giolekva): private domain can be configurable as well
+	config := installer.Config{
+		Values: installer.Values{
+			PCloudEnvName:   "pcloud",
+			Id:              req.Name,
+			ContactEmail:    req.ContactEmail,
+			Domain:          req.Domain,
+			PrivateDomain:   fmt.Sprintf("p.%s", req.Domain),
+			PublicIP:        "46.49.35.44",
+			NamespacePrefix: fmt.Sprintf("%s-", req.Name),
+		},
+	}
+	if err := r.WriteYaml("config.yaml", config); err != nil {
+		return err
+	}
+	{
+		out, err := r.Writer("pcloud-charts.yaml")
+		if err != nil {
+			return err
+		}
+		defer out.Close()
+		_, err = out.Write([]byte(`
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: GitRepository
+metadata:
+  name: pcloud
+  namespace: lekva
+spec:
+  interval: 1m0s
+  url: https://github.com/giolekva/pcloud
+  ref:
+    branch: main
+`))
+		if err != nil {
+			return err
+		}
+	}
+	rootKust := installer.NewKustomization()
+	rootKust.AddResources("pcloud-charts.yaml", "apps")
+	if err := r.WriteKustomization("kustomization.yaml", rootKust); err != nil {
+		return err
+	}
+	appsKust := installer.NewKustomization()
+	if err := r.WriteKustomization("apps/kustomization.yaml", appsKust); err != nil {
+		return err
+	}
+	r.CommitAndPush("initialize config")
+	{
+		app, err := appsRepo.Find("metallb-config-env")
+		if err != nil {
+			return err
+		}
+		if err := appManager.Install(*app, map[string]any{
+			"IngressPrivate": "10.1.0.1",
+			"Headscale":      "10.1.0.2",
+			"SoftServe":      "10.1.0.3",
+			"Rest": map[string]any{
+				"From": "10.1.0.100",
+				"To":   "10.1.0.255",
+			},
+		}); err != nil {
+			return err
+		}
+	}
+	{
+		app, err := appsRepo.Find("ingress-private")
+		if err != nil {
+			return err
+		}
+		if err := appManager.Install(*app, map[string]any{}); err != nil {
+			return err
+		}
+	}
+	{
+		app, err := appsRepo.Find("certificate-issuer-public")
+		if err != nil {
+			return err
+		}
+		if err := appManager.Install(*app, map[string]any{}); err != nil {
+			return err
+		}
+	}
+	{
+		app, err := appsRepo.Find("core-auth")
+		if err != nil {
+			return err
+		}
+		if err := appManager.Install(*app, map[string]any{
+			"Subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
+		}); err != nil {
+			return err
+		}
+	}
+	{
+		app, err := appsRepo.Find("headscale")
+		if err != nil {
+			return err
+		}
+		if err := appManager.Install(*app, map[string]any{
+			"Subdomain": "headscale",
+		}); err != nil {
+			return err
+		}
+	}
+	{
+		keys, err := installer.NewSSHKeyPair()
+		if err != nil {
+			return err
+		}
+		user := fmt.Sprintf("%s-welcome", req.Name)
+		if err := ss.AddUser(user, keys.Public); err != nil {
+			return err
+		}
+		if err := ss.AddCollaborator(req.Name, user); err != nil {
+			return err
+		}
+		app, err := appsRepo.Find("welcome")
+		if err != nil {
+			return err
+		}
+		if err := appManager.Install(*app, map[string]any{
+			"RepoAddr":      ss.GetRepoAddress(req.Name),
+			"SSHPrivateKey": keys.Private,
+		}); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func addNewEnv(
+	repoIO installer.RepoIO,
+	req createEnvReq,
+	keys installer.KeyPair,
+	pcloudRepoPublicKey []byte,
+) error {
+	kust, err := repoIO.ReadKustomization("environments/kustomization.yaml")
+	if err != nil {
+		return err
+	}
+	kust.AddResources(req.Name)
+	tmpls, err := template.ParseFS(filesTmpls, "env-tmpl/*.yaml")
+	if err != nil {
+		return err
+	}
+	repoIP := "192.168.0.211" // TODO(giolekva): configure
+	for _, tmpl := range tmpls.Templates() {
+		dstPath := path.Join("environments", req.Name, tmpl.Name())
+		dst, err := repoIO.Writer(dstPath)
+		if err != nil {
+			return err
+		}
+		defer dst.Close()
+		if err := tmpl.Execute(dst, map[string]string{
+			"Name":       req.Name,
+			"PrivateKey": base64.StdEncoding.EncodeToString([]byte(keys.Private)),
+			"PublicKey":  base64.StdEncoding.EncodeToString([]byte(keys.Public)),
+			"GitHost":    repoIP,
+			"KnownHosts": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s %s", repoIP, pcloudRepoPublicKey))),
+		}); err != nil {
+			return err
+		}
+	}
+	if err := repoIO.WriteKustomization("environments/kustomization.yaml", *kust); err != nil {
+		return err
+	}
+	if err := repoIO.CommitAndPush(fmt.Sprintf("%s: initialize environment", req.Name)); err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/core/installer/welcome/main.go b/core/installer/welcome/welcome.go
similarity index 98%
rename from core/installer/welcome/main.go
rename to core/installer/welcome/welcome.go
index 03a4047..04e08f5 100644
--- a/core/installer/welcome/main.go
+++ b/core/installer/welcome/welcome.go
@@ -13,7 +13,7 @@
 	"github.com/giolekva/pcloud/core/installer"
 )
 
-//go:embed index.html
+//go:embed create-admin-account.html
 var indexHtml string
 
 //go:embed static/*