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.runPrefer 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_tokenThe 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") });
};db.query(sql, params?)— one PostgreSQL statement; parameters support null, bool, numbers, text, UUID and JSON/JSONB.files.get(key)returns a webResponse;files.put,files.listandfiles.delete(key)cover the rest.env.get(name)reads this kap's supplied environment (its secrets); a missing name isundefined. There is no write method.identity.verify(request)tells your handler who the caller is —member,machine,colleagueoranonymous.
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:
GET /kaps/{id}/versions— complete snapshots, newest first.GET /kaps/{id}/diff?from=&to=— exact before/after text per file.POST /kaps/{id}/versions/{v}/restore— a new version with the old content.
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.
- Request — every request to the kap's address runs the main file.
- Schedule — an interval (default minimum 15 minutes) or a five-field UTC cron.
- Inbox — email sent to
{kap}@{org}.kapable.emailis parsed and handed to the file.
// 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:
- members — signed-in members of your organisation.
- invited — members plus an invite list of email addresses and domains.
- public — anyone on the internet, no sign-in.
In practice:
- A member visiting the kap's address goes through one sign-in round trip and lands in the app.
- A non-member is redirected to the organisation's sign-in page.
- A machine calls it with the kap's token:
Authorization: Bearer kbt_…. Tokens are shown once when minted; mint and revoke them from the kap screen or the API.
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.