installer: welcome
diff --git a/core/installer/cmd/env_manager.go b/core/installer/cmd/env_manager.go
index 4279445..33b4058 100644
--- a/core/installer/cmd/env_manager.go
+++ b/core/installer/cmd/env_manager.go
@@ -100,12 +100,9 @@
 }
 
 type createEnvReq struct {
-	Name          string `json:"name"`
-	ContactEmail  string `json:"contactEmail"`
-	Domain        string `json:"domain"`
-	GandiAPIToken string `json:"gandiAPIToken"`
-	AdminUsername string `json:"adminUsername"`
-	// TODO(giolekva): take admin password as well
+	Name         string `json:"name"`
+	ContactEmail string `json:"contactEmail"`
+	Domain       string `json:"domain"`
 }
 
 func (s *envServer) createEnv(c echo.Context) error {
@@ -135,7 +132,7 @@
 		if repo == nil {
 			return err
 		}
-		if err := initNewEnv(installer.NewRepoIO(repo, s.ss.Signer), req); err != nil {
+		if err := initNewEnv(s.ss, installer.NewRepoIO(repo, s.ss.Signer), req); err != nil {
 			return err
 		}
 	}
@@ -160,7 +157,7 @@
 	return nil
 }
 
-func initNewEnv(r installer.RepoIO, req createEnvReq) error {
+func initNewEnv(ss *soft.Client, r installer.RepoIO, req createEnvReq) error {
 	appManager, err := installer.NewAppManager(r)
 	if err != nil {
 		return err
@@ -236,9 +233,16 @@
 		if err != nil {
 			return err
 		}
-		if err := appManager.Install(*app, map[string]any{
-			"GandiAPIToken": req.GandiAPIToken,
-		}); err != nil {
+		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
 		}
 	}
@@ -265,19 +269,28 @@
 		}
 	}
 	{
-		app, err := appsRepo.Find("tailscale-proxy")
+		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{
-			"Username": req.AdminUsername,
-			"IPSubnet": "10.1.0.0/24",
+			"RepoAddr":      ss.GetRepoAddress(req.Name),
+			"SSHPrivateKey": keys.Private,
 		}); err != nil {
 			return err
 		}
-		// TODO(giolekva): headscale accept routes
 	}
-
 	return nil
 }
 
diff --git a/core/installer/cmd/main.go b/core/installer/cmd/main.go
index e82b338..8a5e04b 100644
--- a/core/installer/cmd/main.go
+++ b/core/installer/cmd/main.go
@@ -27,6 +27,7 @@
 	rootCmd.AddCommand(installCmd())
 	rootCmd.AddCommand(appManagerCmd())
 	rootCmd.AddCommand(envManagerCmd())
+	rootCmd.AddCommand(welcomeCmd())
 }
 
 func main() {
diff --git a/core/installer/cmd/welcome.go b/core/installer/cmd/welcome.go
new file mode 100644
index 0000000..3b834b0
--- /dev/null
+++ b/core/installer/cmd/welcome.go
@@ -0,0 +1,85 @@
+package main
+
+import (
+	"fmt"
+	"golang.org/x/crypto/ssh"
+	"net"
+	"os"
+
+	"github.com/go-git/go-billy/v5/memfs"
+	"github.com/go-git/go-git/v5"
+	gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
+	"github.com/go-git/go-git/v5/storage/memory"
+	"github.com/spf13/cobra"
+
+	"github.com/giolekva/pcloud/core/installer"
+	"github.com/giolekva/pcloud/core/installer/welcome"
+)
+
+var welcomeFlags struct {
+	repo   string
+	sshKey string
+	port   int
+}
+
+func welcomeCmd() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:  "welcome",
+		RunE: welcomeCmdRun,
+	}
+	cmd.Flags().StringVar(
+		&welcomeFlags.repo,
+		"repo-addr",
+		"",
+		"",
+	)
+	cmd.Flags().StringVar(
+		&welcomeFlags.sshKey,
+		"ssh-key",
+		"",
+		"",
+	)
+	cmd.Flags().IntVar(
+		&welcomeFlags.port,
+		"port",
+		8080,
+		"",
+	)
+	return cmd
+}
+
+func welcomeCmdRun(cmd *cobra.Command, args []string) error {
+	sshKey, err := os.ReadFile(welcomeFlags.sshKey)
+	if err != nil {
+		return err
+	}
+	auth := authSSH(sshKey)
+	repo, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
+		URL:             welcomeFlags.repo,
+		Auth:            auth,
+		RemoteName:      "origin",
+		ReferenceName:   "refs/heads/master",
+		Depth:           1,
+		InsecureSkipTLS: true,
+		Progress:        os.Stdout,
+	})
+	s := welcome.NewServer(
+		welcomeFlags.port,
+		installer.NewRepoIO(repo, auth.Signer),
+	)
+	s.Start()
+	return nil
+}
+
+func authSSH(pemBytes []byte) *gitssh.PublicKeys {
+	a, err := gitssh.NewPublicKeys("git", pemBytes, "")
+	if err != nil {
+		panic(err)
+	}
+	a.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
+		// TODO(giolekva): verify server public key
+		fmt.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
+		return nil
+	}
+	return a
+}