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).
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
- Create an app from a template with
createAppChoice(use theelysiastarter — see the template note below). - Authenticate & push your code to the app's Forgejo remote (see "Pushing your code" below).
- Deploy with
requestDeploy. The CI pipeline builds and starts the app. - Your app is live at
https://{app}.{org}.kapable.run.
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.
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:
current_step/step_index/total_steps— live build progress.deployment.statuspasses throughrequested → building → live_pending_tls. On failure,build_log_tail+failed_steptell you why.- Heads-up: the app's top-level
statuscan staycreatedeven while the deployment is serving. Confirm liveness by requesting thepublic_urldirectly (it returns 200 over TLS once live).
Apps & templates
| Method | Path | SDK (client.auth) |
|---|---|---|
| GET | /v1/templates | listTemplates |
| POST | /v1/app-choices | createAppChoice |
| GET | /v1/app-choices/{id} | getAppChoice |
| POST | /v1/app-choices/fork | forkAppChoice |
| POST | /v1/app-choices/{id}/publish-as-template | publishAppAsTemplate |
| GET | /v1/orgs/{org_id}/apps | listOrgApps |
| 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
| Method | Path | SDK (client.auth) |
|---|---|---|
| POST | /v1/apps/{app_id}/deploy | requestDeploy |
| POST | /v1/apps/{app_id}/rebuild | rebuildApp |
| POST | /v1/apps/{app_id}/deployments/{deployment_id}/retry | retryDeployment |
| POST | /v1/apps/{app_id}/pause | pauseApp |
| POST | /v1/apps/{app_id}/resume | resumeApp |
| POST | /v1/apps/{app_id}/abort | abortPendingApp |
| GET | /v1/apps/{app_id}/logs | appLogs |
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.
| Method | Path | SDK (client.auth) |
|---|---|---|
| GET | /v1/apps/{app_id}/environments | listAppEnvironments |
| POST | /v1/apps/{app_id}/environments | createAppEnvironment |
| GET | /v1/apps/{app_id}/environments/{env} | getAppEnvironment |
| DELETE | /v1/apps/{app_id}/environments/{env} | deleteAppEnvironment |
| GET | /v1/apps/{app_id}/environments/{env}/env_vars | listAppEnvVars |
| POST | /v1/apps/{app_id}/environments/{env}/env_vars | createAppEnvVar |
| 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.
| Method | Path | SDK (client.auth) |
|---|---|---|
| GET | /v1/apps/{app_id}/environments/{env}/lanes | listLanes |
| POST | /v1/apps/{app_id}/environments/{env}/lanes | createLane |
| 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}/deploy | deployToLane |
| POST | /v1/apps/{app_id}/environments/{env}/lanes/{lane}/promote | promoteLane |
| POST | /v1/apps/{app_id}/environments/{env}/lanes/{lane}/drain | drainLane |
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?;
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.
- 2xx — healthy; the lane routes traffic.
- 404 — treated as alive but unmonitored: your app keeps serving, it just loses granular health detection. You will never be taken down for lacking the route.
- Other non-2xx — the app is considered alive but unhealthy; the router routes away from the lane until it recovers.
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.