diff --git a/project-demos/sliplane-manage/Apps/SliplaneDeployApp.cs b/project-demos/sliplane-manage/Apps/SliplaneDeployApp.cs
new file mode 100644
index 00000000..2984265c
--- /dev/null
+++ b/project-demos/sliplane-manage/Apps/SliplaneDeployApp.cs
@@ -0,0 +1,51 @@
+namespace SliplaneManage.Apps;
+
+using SliplaneManage.Apps.Views;
+using SliplaneManage.Services;
+
+///
+/// Route: /sliplane-deploy-app
+/// Opened via the "Host your Ivy app on Sliplane" button in GitHub READMEs.
+/// The ?repo= query param is captured by RepoCaptureFilter before Ivy SPA loads,
+/// stored in DeploymentDraftStore, and read here to pre-fill the deploy form.
+///
+[App(
+ id: "sliplane-deploy-app",
+ icon: Icons.Rocket,
+ title: "Deploy on Sliplane",
+ searchHints: ["deploy", "host", "sliplane"],
+ isVisible: true)]
+public class SliplaneDeployApp : ViewBase
+{
+ public override object? Build()
+ {
+ var config = this.UseService();
+ var auth = this.UseService();
+ var session = auth.GetAuthSession();
+ var apiToken = config["Sliplane:ApiToken"]
+ ?? session.AuthToken?.AccessToken
+ ?? string.Empty;
+
+ // Repo from internal navigation args or last saved value (per-user)
+ var draftStore = this.UseService();
+ var args = this.UseArgs();
+ var repoUrl = args?.Repo ?? draftStore.LastRepoUrl ?? string.Empty;
+
+ if (string.IsNullOrWhiteSpace(apiToken))
+ {
+ return Layout.Center()
+ | (Layout.Vertical().Align(Align.Center).Gap(6)
+ | Icons.Rocket.ToIcon()
+ | Text.H2("Deploy to Sliplane")
+ | (string.IsNullOrWhiteSpace(repoUrl)
+ ? Text.Muted("Sign in with Sliplane to deploy your Ivy app.")
+ : Text.Muted($"Repository: {repoUrl}"))
+ | Text.Muted("No API token. Please sign in or configure Sliplane:ApiToken."));
+ }
+
+ return new DeployView(apiToken, repoUrl);
+ }
+}
+
+/// Arguments for internal Ivy navigation to SliplaneDeployApp.
+public record DeployArgs(string Repo);
diff --git a/project-demos/sliplane-manage/Apps/SliplaneOverviewApp.cs b/project-demos/sliplane-manage/Apps/SliplaneOverviewApp.cs
index f489245c..ef8e4644 100644
--- a/project-demos/sliplane-manage/Apps/SliplaneOverviewApp.cs
+++ b/project-demos/sliplane-manage/Apps/SliplaneOverviewApp.cs
@@ -51,7 +51,7 @@ public class SliplaneProjectsApp : ViewBase
///
/// Services app — list services, create, edit, pause, delete.
///
-[App(icon: Icons.Box, title: "Services", searchHints: ["services", "deploy"])]
+[App(id: "sliplane-services-app", icon: Icons.Box, title: "Services", searchHints: ["services", "deploy"])]
public class SliplaneServicesApp : ViewBase
{
public override object? Build()
diff --git a/project-demos/sliplane-manage/Apps/Views/DeployView.cs b/project-demos/sliplane-manage/Apps/Views/DeployView.cs
new file mode 100644
index 00000000..f63e0fae
--- /dev/null
+++ b/project-demos/sliplane-manage/Apps/Views/DeployView.cs
@@ -0,0 +1,311 @@
+namespace SliplaneManage.Apps.Views;
+
+using System.ComponentModel.DataAnnotations;
+using SliplaneManage.Models;
+using SliplaneManage.Services;
+using SliplaneManage.Apps;
+
+public class DeployFormModel
+{
+ [Display(Name = "Server", Order = 1, Prompt = "Select a server")]
+ [Required(ErrorMessage = "Select a server")]
+ public string ServerId { get; set; } = "";
+
+ [Display(Name = "Project", Order = 2, Prompt = "Select a project")]
+ [Required(ErrorMessage = "Select a project")]
+ public string ProjectId { get; set; } = "";
+
+ [Display(Name = "Service name", Order = 3, Prompt = "my-ivy-service")]
+ [Required(ErrorMessage = "Enter a service name")]
+ [MinLength(2, ErrorMessage = "Service name must be at least 2 characters")]
+ public string Name { get; set; } = "";
+
+ [Display(Name = "Repository URL", Order = 4, Prompt = "https://github.com/user/repo")]
+ [Required(ErrorMessage = "Enter a repository URL")]
+ public string GitRepo { get; set; } = "";
+
+ [Display(Name = "Branch", Order = 5, Prompt = "main")]
+ public string Branch { get; set; } = "main";
+
+ [Display(GroupName = "Build", Name = "Dockerfile path", Order = 6, Prompt = "Dockerfile")]
+ public string DockerfilePath { get; set; } = "Dockerfile";
+
+ [Display(GroupName = "Build", Name = "Docker context", Order = 7, Prompt = ".")]
+ public string DockerContext { get; set; } = ".";
+
+ [Display(GroupName = "Build", Name = "Auto-deploy on push", Order = 8, Prompt = "Enable auto-deploy on push")]
+ public bool AutoDeploy { get; set; } = true;
+
+ [Display(GroupName = "Network", Name = "Public access", Order = 9, Prompt = "Expose service publicly")]
+ public bool NetworkPublic { get; set; } = true;
+
+ [Display(GroupName = "Network", Name = "Protocol", Order = 10, Prompt = "http or https")]
+ public string NetworkProtocol { get; set; } = "http";
+
+ [Display(GroupName = "Optional", Name = "Health check path", Prompt = "/health", Order = 11)]
+ public string Healthcheck { get; set; } = "/";
+
+ [Display(GroupName = "Optional", Name = "Start command", Prompt = "e.g. npm start", Order = 12)]
+ public string? Cmd { get; set; }
+}
+
+public class DeployView : ViewBase
+{
+ private readonly string _apiToken;
+ private readonly string _repoUrl;
+
+ public DeployView(string apiToken, string repoUrl)
+ {
+ _apiToken = apiToken;
+ _repoUrl = repoUrl;
+ }
+
+ public override object? Build()
+ {
+ var client = this.UseService();
+ var draftStore = this.UseService();
+ var refreshSender = this.CreateSignal();
+
+ var initialName = DeriveServiceName(_repoUrl);
+
+ var model = this.UseState(() => new DeployFormModel
+ {
+ GitRepo = _repoUrl,
+ Name = initialName,
+ });
+
+ // Keep DeploymentDraftStore in sync as the user edits the repo URL (per-user)
+ this.UseEffect(() => draftStore.SaveRepoUrl(model.Value.GitRepo), model);
+
+ var envList = this.UseState>(() => new List());
+ var showAddEnvDlg = this.UseState(false);
+ var addEnvKey = this.UseState(string.Empty);
+ var addEnvValue = this.UseState(string.Empty);
+ var reloadCounter = this.UseState(0);
+ var serverVolumes = this.UseState>(() => new List());
+ var volumeMountsList = this.UseState>(() => new List<(string, string)>());
+ var showAddVolumeDlg = this.UseState(false);
+ var addVolumeId = this.UseState(string.Empty);
+ var addMountPath = this.UseState(string.Empty);
+
+ QueryResult