installer: welcome
diff --git a/core/installer/welcome/main.go b/core/installer/welcome/main.go
new file mode 100644
index 0000000..c3279d8
--- /dev/null
+++ b/core/installer/welcome/main.go
@@ -0,0 +1,90 @@
+package welcome
+
+import (
+	"embed"
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/http"
+
+	"github.com/labstack/echo/v4"
+
+	"github.com/giolekva/pcloud/core/installer"
+)
+
+//go:embed index.html
+var indexHtml string
+
+//go:embed static/*
+var staticAssets embed.FS
+
+type Server struct {
+	port int
+	repo installer.RepoIO
+}
+
+func NewServer(port int, repo installer.RepoIO) *Server {
+	return &Server{
+		port,
+		repo,
+	}
+}
+
+func (s *Server) Start() {
+	e := echo.New()
+	e.StaticFS("/static", echo.MustSubFS(staticAssets, "static"))
+	e.POST("/create-admin-account", s.createAdminAccount)
+	e.GET("/", s.createAdminAccountForm)
+	log.Fatal(e.Start(fmt.Sprintf(":%d", s.port)))
+}
+
+func (s *Server) createAdminAccountForm(c echo.Context) error {
+	return c.HTML(http.StatusOK, indexHtml)
+}
+
+type createAdminAccountReq struct {
+	Username      string `json:"username,omitempty"`
+	Password      string `json:"password,omitempty"` // TODO(giolekva): actually use this
+	GandiAPIToken string `json:"gandiAPIToken,omitempty"`
+	SecretToken   string `json:"secretToken,omitempty"`
+}
+
+func (s *Server) createAdminAccount(c echo.Context) error {
+	var req createAdminAccountReq
+	if err := json.NewDecoder(c.Request().Body).Decode(&req); err != nil {
+		return err
+	}
+	// TODO(giolekva): accounts-ui create user req
+	{
+		appManager, err := installer.NewAppManager(s.repo)
+		if err != nil {
+			return err
+		}
+		appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+		{
+			app, err := appsRepo.Find("certificate-issuer-private")
+			if err != nil {
+				return err
+			}
+			if err := appManager.Install(*app, map[string]any{
+				"GandiAPIToken": req.GandiAPIToken,
+			}); err != nil {
+				return err
+			}
+		}
+		{
+			app, err := appsRepo.Find("tailscale-proxy")
+			if err != nil {
+				return err
+			}
+			if err := appManager.Install(*app, map[string]any{
+				"Username": req.Username,
+				"IPSubnet": "10.1.0.0/24", // TODO(giolekva): this should be taken from the config generated during new env creation
+			}); err != nil {
+				return err
+			}
+			// TODO(giolekva): headscale accept routes
+		}
+	}
+	return c.String(http.StatusOK, "OK")
+}