Kaps

A kap is one folder of files that runs at its own address the moment you save it. It belongs to your organisation and is private to your organisation's signed-in members by default. No build step, no deploy pipeline, no containers to manage.

From code, not just the browser. Both SDKs and the CLI can do everything this page describes. Kaps lives on your org host, so build the client against it with a member session token:

import { KapableClient } from '@kapable/sdk';   // 0.22.0 or later
const client = new KapableClient({
  baseUrl: 'https://api.kapable.ai',
  orgBaseUrl: 'https://acme.kapable.ai',   // your org host
  token: 'kses_...',                        // a member session; an sk_org_ key is refused
});
const { kaps } = await client.kaps.listKaps(orgId);

Rust: client.kaps() on kapable-sdk 0.19.0 with .org_base_url(...). Terminal: kapable kap list after kapable login (CLI 0.21.3). The full route table is on Kaps API.

Your first kap

Sign in at https://{org}.kapable.ai and open https://{org}.kapable.ai/ui/kaps. You see your organisation's kap list and a New kap button. Name it: it is born with a main.ts hello handler, a request trigger, and one machine token (shown once). Saving opens a single screen with the files, the rendered app, versions, log, triggers and audience side by side.

Edit main.ts and press Save (or ⌘S). The app pane reloads and a new version appears. The kap's address is:

https://{kap}.{org}.kapable.run

Prefer the API? The same thing as a script, with a member session:

curl -X POST https://acme.kapable.ai/v1/orgs/$ORG_ID/kaps \
  -H "X-Session-Token: kses_..." \
  -H "X-Kapable-Client: my-script" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "hello",
    "main_file": "main.ts",
    "files": [{"path": "main.ts", "content": "export default (request: Request): Response => new Response(\"Hello\")"}]
  }'
# → 201 with the kap, its first version, its address, and a once-only bypass_token

The handler contract

The request trigger runs your main file for every request the kap receives. The contract is one default export that takes a web Request and returns a Response:

// main.ts
export default async (request: Request): Promise<Response> => {
  return new Response("Hello");
};

Other trigger kinds hand your handler a different argument: a schedule gets { lastRunAt }, an inbox trigger gets the parsed email message. See Triggers below.

The std module

Import the platform module for zero-config access to your kap's own database, file store, environment and caller identity. Deno resolves the import and the TypeScript directly; there is nothing to install:

import { db, files, env } from "https://kaps.kapable.ai/std/v1/mod.ts";

export default async (_request: Request): Promise<Response> => {
  await db.query("CREATE TABLE IF NOT EXISTS notes (id integer PRIMARY KEY, body text)");
  await db.query("INSERT INTO notes VALUES ($1,$2) ON CONFLICT(id) DO UPDATE SET body=EXCLUDED.body", [1, "hello"]);
  await files.put("note.txt", "hello", "text/plain");
  const row = await db.query("SELECT body FROM notes WHERE id=$1", [1]);
  const text = await (await files.get("note.txt")).text();
  return Response.json({ row: row.rows[0], text, configured: !!env.get("API_KEY") });
};

Keep store calls inside the handler: the first evaluation of your module runs before the kap's database exists, so a store call at the top of the file fails. Inside the handler it always works.

Versions, diff and restore

Every save replaces the whole file set and creates an immutable version. Restoring never deletes anything: a restore creates another version. You can list versions, diff any two, and restore from the editor's Versions pane or the API:

A save against a stale base is refused with 409 VERSION_CONFLICT and the current version id, so two people (or two scripts) editing at once never silently overwrite each other.

Triggers

A file in your kap can run on its own, not only when a request arrives. Attach triggers from the Triggers pane or the API. A file supports one trigger of each kind.

// schedule.ts
export default async (ctx: { lastRunAt: Date | null }) => {
  // null on the first run; otherwise the previous run's start time.
  console.log(ctx.lastRunAt?.toISOString() ?? "first run");
};
// inbox.ts
import { files } from "https://kaps.kapable.ai/std/v1/mod.ts";
export default async (msg) => {
  for (const attachment of msg.attachments) {
    const response = await files.get(attachment.key);
    const bytes = await response.arrayBuffer();
    // Work with the decoded attachment bytes here.
  }
};

Triggers can be paused, resumed, removed, and run by hand; every run — including failed ones and every visit to the kap's web address — appears in the Runs pane with a plain-words outcome and its log.

Who can reach a kap

A kap starts as members: only signed-in members of your organisation get in. The Audience pane (and the API) offers three settings:

In practice:

For an outside sender that cannot hold a token — Stripe or GitHub delivering a webhook — mint an endpoint secret on the Triggers pane. The sender then either calls a secret address (https://{kap}.{org}.kapable.run/__kaps/e/{secret}) or signs the request with an HMAC-SHA256 header. An endpoint secret does not change the kap's audience: a members-only kap stays invisible to everyone else. Your handler can check who is calling with identity.verify(request):

import { identity } from "https://kaps.kapable.ai/std/v1/mod.ts";

export default async (request: Request): Promise<Response> => {
  const caller = await identity.verify(request);
  return Response.json({ kind: caller.caller_kind, subject: caller.sub });
};

Next

Full route reference: Kaps API.