Apps & Deployment

The platform's core story: create an app from a template, push code to the built-in Git remote, request a deploy, and it's live at {app}.{org}.kapable.run. The full lifecycle — environments, env vars, deployment lanes, logs, pause/resume — is part of the auth SDK module today (the methods live on client.auth).

Token tier: session required

App lifecycle endpoints are permission-gated: authenticate with a kses_ session token (org role owner/member). A bare sk_live_ API key passes authentication but is rejected by the permission layer on app mutations.

The deploy flow

  1. Create an app from a template with createAppChoice (use the elysia starter — see the template note below).
  2. Authenticate & push your code to the app's Forgejo remote (see "Pushing your code" below).
  3. Deploy with requestDeploy. The CI pipeline builds and starts the app.
  4. Your app is live at https://{app}.{org}.kapable.run.
Fresh apps: use requestDeploy, not deployToLane

deployToLane targets an existing lane and returns 404 on a freshly created app that has never deployed. The verified first-deploy path is requestDeploy; lanes come into play once the app exists in an environment.

Template note (2026-06-09)

The elysia starter builds and deploys (~90s). The ts-hono template is listed in GET /v1/templates but its build image is currently missing (Image "kapable-tpl-ts-hono" not found, surfaces as a "checkout failed" deploy error) — use elysia until that's fixed.

Pushing your code

The app's repo lives at git.kapable.dev. Mint a scoped bot token for it, then push over HTTPS with the bot username embedded in the URL:

# 1. Mint a per-app Forgejo PAT (token shown once)
curl -X POST -H "Authorization: Bearer kses_..." \
  https://api.kapable.ai/v1/apps/{app_id}/bot-pat
# → { "token": "...", "bot_username": "kbot-{app}" }   (bot_username is also on the app detail)

# 2. Push using bot_username:token@ (repo_url is returned when the app is created)
git remote add kapable https://kbot-myapp:PAT@git.kapable.dev/{org}/{repo}.git
git push kapable main

Watching a deploy

requestDeploy returns 202 {"status":"requested"}. Poll GET /v1/apps/{app_id} (getAppDetail) to watch progress:

Apps & templates

MethodPathSDK (client.auth)
GET/v1/templateslistTemplates
POST/v1/app-choicescreateAppChoice
GET/v1/app-choices/{id}getAppChoice
POST/v1/app-choices/forkforkAppChoice
POST/v1/app-choices/{id}/publish-as-templatepublishAppAsTemplate
GET/v1/orgs/{org_id}/appslistOrgApps
GET/v1/apps/{app_id}getAppDetail
PATCH/v1/apps/{app_id}renameApp
DELETE/v1/apps/{app_id}deleteApp

SDK Examples

// 1. Create an app from the elysia starter template
const choice = await client.auth.createAppChoice({
  user_id: me.user.id,
  org_id: me.org.id,
  org_slug: 'acme',
  template_slug: 'elysia',
  app_name: 'my-api',
});

// 2. Push code to the app's Forgejo remote, then:

// 3. Request the first deploy
await client.auth.requestDeploy(choice.id);

// 4. Live at https://my-api.acme.kapable.run
// Same flow in Rust (methods on client.auth())
let choice = client.auth().create_app_choice(&CreateAppChoiceRequest {
    user_id: me.user.id,
    org_id: me.org.id,
    org_slug: "acme".into(),
    template_slug: "elysia".into(),
    app_name: "my-api".into(),
}).await?;

client.auth().request_deploy(choice.id).await?;

Deployments & runtime control

MethodPathSDK (client.auth)
POST/v1/apps/{app_id}/deployrequestDeploy
POST/v1/apps/{app_id}/rebuildrebuildApp
POST/v1/apps/{app_id}/deployments/{deployment_id}/retryretryDeployment
POST/v1/apps/{app_id}/pausepauseApp
POST/v1/apps/{app_id}/resumeresumeApp
POST/v1/apps/{app_id}/abortabortPendingApp
GET/v1/apps/{app_id}/logsappLogs

Environments & env vars

Apps have named environments (e.g. production), each with its own env-var set and deployment lanes. A simpler app-level env API (listAppEnv / upsertAppEnv / deleteAppEnv) also exists at /v1/apps/{app_id}/env.

MethodPathSDK (client.auth)
GET/v1/apps/{app_id}/environmentslistAppEnvironments
POST/v1/apps/{app_id}/environmentscreateAppEnvironment
GET/v1/apps/{app_id}/environments/{env}getAppEnvironment
DELETE/v1/apps/{app_id}/environments/{env}deleteAppEnvironment
GET/v1/apps/{app_id}/environments/{env}/env_varslistAppEnvVars
POST/v1/apps/{app_id}/environments/{env}/env_varscreateAppEnvVar
PUT/v1/apps/{app_id}/environments/{env}/env_vars/{name}updateAppEnvVar
DELETE/v1/apps/{app_id}/environments/{env}/env_vars/{name}deleteAppEnvVar

Deployment lanes

Lanes are parallel deployment tracks within an environment (blue/green, canary). Promote a lane to make it the live one; drain it to stop routing traffic.

MethodPathSDK (client.auth)
GET/v1/apps/{app_id}/environments/{env}/laneslistLanes
POST/v1/apps/{app_id}/environments/{env}/lanescreateLane
GET/v1/apps/{app_id}/environments/{env}/lanes/{lane}getLane
PATCH/v1/apps/{app_id}/environments/{env}/lanes/{lane}updateLane
DELETE/v1/apps/{app_id}/environments/{env}/lanes/{lane}deleteLane
POST/v1/apps/{app_id}/environments/{env}/lanes/{lane}/deploydeployToLane
POST/v1/apps/{app_id}/environments/{env}/lanes/{lane}/promotepromoteLane
POST/v1/apps/{app_id}/environments/{env}/lanes/{lane}/draindrainLane

SDK Examples

// Set an env var on production, then deploy to a canary lane
await client.auth.createAppEnvVar(appId, 'production', {
  name: 'FEATURE_FLAG_X',
  value: 'on',
});

await client.auth.deployToLane(appId, 'production', 'canary');

// Happy with the canary? Promote it.
await client.auth.promoteLane(appId, 'production', 'canary');

// Tail recent logs
const logs = await client.auth.appLogs(appId, { lines: 200 });
client.auth().upsert_app_env(app_id, "FEATURE_FLAG_X",
    &EnvVarUpsertRequest { value: "on".into() }).await?;
client.auth().promote_lane(app_id, "production", "canary").await?;
let logs = client.auth().app_logs(app_id, Some(200)).await?;
Heads-up: these methods live under client.auth

The app surface is part of the auth module today because apps are org-owned resources managed by kapable-auth. A first-class client.apps namespace (same methods, better discoverability) is on the SDK roadmap.

Health checks

Every app should serve GET /health returning any 2xx — fast and dependency-free (no DB calls). The platform's lane prober uses it for granular health detection, and the deploy pipeline health-gates new containers on it.

Public /health on {app}.{org}.kapable.run proxies straight through to your app like any other path. New scaffolds and the seed templates ship the route by default. For React Router, it's a resource route:

// app/routes/health.ts
export function loader() {
  return new Response(JSON.stringify({ status: "ok" }), {
    status: 200,
    headers: { "Content-Type": "application/json" },
  });
}

// app/routes.ts
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
  route("health", "routes/health.ts"),
  index("routes/home.tsx"),
] satisfies RouteConfig;

Static sites (the blank stack) can commit a plain file named health at the repo root — it's served at /health with a 200. Other stacks: any one-line GET /health handler works.