@kortix/froglet-sdk
v0.1.0
Published
TypeScript client for the Froglet API.
Downloads
80
Keywords
Readme
@kortix/froglet-sdk — TypeScript SDK
Programmatic access to the Froglet API. Boot sandboxes, exec commands, manage
templates, attach volumes, run backups — all from Node, Bun, Deno, or the
browser. Node 18+ or anywhere fetch is global; no Node-specific imports.
It mirrors the @kortix/froglet-cli
command-line tool one-for-one. If the CLI has a command, the SDK has the
method.
Install
npm install @kortix/froglet-sdk
pnpm add @kortix/froglet-sdkQuickstart
import { FrogletClient } from '@kortix/froglet-sdk';
const froglet = new FrogletClient({
apiKey: process.env.FROGLET_API_KEY,
// baseUrl: defaults to https://api.froglet.sh
});
// 1. boot a sandbox
const created = await froglet.sandboxes.create({
templateSlug: 'ubuntu-24.04',
size: 'xs',
slug: 'demo',
});
// 2. grab a handle with the id pre-bound
const sb = froglet.sandbox(created.id);
await sb.waitForState('running');
// 3. run something inside it
const result = await sb.exec('uname -a');
console.log(result.stdout);
// 4. tear it down
await sb.destroy();You don't have to use the handle — every method is also available flat on the
resource (froglet.sandboxes.exec(id, 'uname -a')). The handle just removes
the repetition when you're doing several things with the same sandbox.
Client options
new FrogletClient({
apiKey: 'fl_…', // required
baseUrl: 'https://api.froglet.sh', // default
fetch: globalThis.fetch, // override for testing
userAgent: '@kortix/froglet-sdk/0.1.0', // sent on every request
timeoutMs: 60_000, // per-request timeout
});For a session-cookie auth flow (i.e. from the browser, talking to your own
backend), pass fetch: customFetch that forwards the cookie. The SDK doesn't
care how Authorization gets onto the request as long as the response shape
is { data: T } | { error: { code, message } }.
Concepts in 60 seconds
- Template — a rootfs image. Every workspace gets
ubuntu-24.04for free; build your own from any Docker image withtemplateBuilds.create. - Sandbox — one running VM. Boots from a template; has its own IP, CPU,
memory, disk. Identified by a UUID; the short slug shown in
sandboxes.list(hbuh,smwlk) is just a friendlier alias. - Volume — persistent storage that outlives sandboxes. Attached at a mount path, read-write or read-only.
- Backup — R2-uploaded full sandbox snapshot (VM RAM + dirty disk
blocks). Restoring drops the sandbox back to the exact instant the backup
was taken. Different from
sandboxes.snapshot, which is a fast in-place checkpoint that is not uploaded anywhere. - Port — a public HTTPS route to an internal port inside a sandbox
(e.g.
https://web--demo.froglet.sh → :8080in the guest). - SSH access — a workspace-scoped JWT for the bastion at
ssh.froglet.sh:2222.
Recipes
Build a template from a Docker image, then use it
const build = await froglet.templateBuilds.create({
name: 'acme-app',
slug: 'acme-app',
sourceKind: 'image',
sourceRef: 'ghcr.io/acme/app:1.2',
});
// poll for state=ready (or hit the SSE stream)
let row;
do {
await new Promise((r) => setTimeout(r, 5_000));
const builds = await froglet.templateBuilds.list();
row = builds.find((b) => b.id === build.id);
} while (row && row.state !== 'ready' && row.state !== 'error');
if (row?.state === 'ready') {
const sb = await froglet.sandboxes.create({ templateSlug: 'acme-app', size: 's' });
await froglet.sandboxes.waitForState(sb.id, 'running');
}Exec with custom cwd + stdin
const r = await froglet.sandboxes.exec(
sb.id,
'python -c "import sys; print(len(sys.stdin.read()))"',
{
cwd: '/tmp',
stdin: 'hello\nworld\n',
timeoutMs: 30_000,
},
);
console.log(r.exit_code, r.stdout, r.stderr);Upload + download a file
await froglet.sandboxes.uploadFile(
sb.id,
'/data/input.txt',
new TextEncoder().encode('hello from node'),
);
const bytes = await froglet.sandboxes.downloadFile(sb.id, '/data/output.txt');
const text = new TextDecoder().decode(bytes);Expose a port
const port = await froglet.ports.create(sb.id, {
internalPort: 8080,
label: 'web', // public route lives at https://web--<slug>.froglet.sh
// public: false // gated route (token required) — defaults to true
// ttlSec: 600 // auto-revoke after 10 min
});
console.log(port.url);For a one-time share link:
const { token, expires_at } = await froglet.sandboxes.mintPortShareToken(sb.id, port.id, 300);SSH into a sandbox
const access = await froglet.sshAccess.create(sb.id, { name: 'laptop', ttlHours: 4 });
console.log(access.command);
// ssh -p 2222 <jwt>@ssh.froglet.shSnapshot, mutate, restore
const backup = await froglet.sandboxes.backup(sb.id, 'before-experiment');
// ... break things ...
const job = await froglet.sandboxes.restore(sb.id, backup.id);
// state=in_progress; poll listRestoreJobs to trackMount a persistent volume
const vol = await froglet.volumes.create({ name: 'shared-data', size_gb: 25 });
await froglet.volumes.attach(sb.id, {
volume_id: vol.id,
mount_path: '/data',
// mode: 'ro' for read-only
});
// later, on a different sandbox:
await froglet.volumes.attach(otherSb.id, { volume_id: vol.id, mount_path: '/data' });Wait for a sandbox to reach a state
await froglet.sandboxes.waitForState(sb.id, 'running', { timeoutMs: 30_000 });Stream a PTY into the browser
const ticket = await froglet.sandboxes.ptyTicket(sb.id);
const ws = new WebSocket(ticket.url); // wss://…?ticket=<jwt>
ws.onmessage = (ev) => term.write(ev.data);
term.onData((data) => ws.send(data));Working with a single sandbox
client.sandbox(id) returns a SandboxHandle whose methods are pre-bound to
that id. Everything you can do to a sandbox is on the handle:
const sb = froglet.sandbox(sandboxId);
// status + lifecycle
const { sandbox, events } = await sb.fetch();
await sb.start();
await sb.stop();
await sb.destroy();
await sb.waitForState('running');
// exec + files
const r = await sb.exec('uname -a');
await sb.uploadFile('/tmp/x', new TextEncoder().encode('hi'));
const bytes = await sb.downloadFile('/tmp/x');
await sb.listFiles('/etc');
const tkt = await sb.ptyTicket();
// snapshots + backups
await sb.snapshot('checkpoint');
const b = await sb.backup('nightly');
await sb.listBackups();
await sb.restore(b.id);
await sb.setBackupPolicy({
enabled: true,
intervalMinutes: 60,
retainCount: 3,
retainDays: 7,
storage: 'r2',
});
// ports + ssh access
const port = await sb.exposePort({ internalPort: 8080, label: 'web' });
await sb.revokePort(port.id);
const t = await sb.mintPortShareToken(port.id, 300);
const access = await sb.createSshAccess({ name: 'laptop', ttlHours: 4 });
await sb.revokeSshAccess(access.id);
// volumes
await sb.attachVolume({ volume_id: vol.id, mount_path: '/data' });
await sb.attachedVolumes();
await sb.detachVolume(vol.id);The handle is purely a convenience — it forwards to the same client.sandboxes,
client.ports, client.sshAccess, and client.volumes resources. Use
whichever style fits the call site better.
API surface
Top-level namespaces on FrogletClient:
| Namespace | What it covers |
| ---------------- | ------------------------------------------------------------------------- |
| me | Who am I (workspace, role, scopes). |
| sandboxes | Lifecycle, exec, files, PTY, snapshots, backups, restore. |
| templates | Catalog + publish/unpublish/delete. |
| templateBuilds | Build templates from Docker images. |
| ports | Public routes per sandbox. |
| sshAccess | SSH bastion tokens per sandbox. |
| volumes | Persistent storage + attach/detach/search. |
| keys | Workspace API keys. |
| members | Workspace member management. |
| quotas | Usage + limits + raise-quota requests. |
| admin | Platform-admin operations (hosts, orders, cells). Workspace keys get 403. |
Each namespace returns plain TypeScript objects whose shapes are exported
alongside the resource (e.g. Sandbox, Backup, BackupPolicy,
SandboxPort, Volume, Member, ApiKey). Import them when you need to
annotate variables:
import type { Sandbox, ExecResult, Backup } from '@kortix/froglet-sdk';Errors
Every request returns the unwrapped data field on success and throws
FrogletApiError on 4xx/5xx with the structured body:
import { FrogletApiError } from '@kortix/froglet-sdk';
try {
await froglet.sandboxes.create({ templateSlug: 'unknown', size: 'm' });
} catch (e) {
if (e instanceof FrogletApiError) {
console.log(e.status, e.code, e.message);
// 404 not_found "template 'unknown' not found"
}
throw e;
}Common codes:
| Status | Code | Meaning |
| ------ | ------------------------- | ------------------------------------------------------ |
| 401 | unauthorized | Missing / wrong API key. |
| 403 | forbidden_scope | Token doesn't carry the scope this route needs. |
| 403 | platform_admin_required | You tried an admin/* route with a workspace key. |
| 404 | not_found | Resource doesn't exist or your token can't see it. |
| 429 | quota_exceeded | Workspace at the limit. Inspect error.payload. |
| 422 | backup_failed | Backup pipeline rejected the request (e.g. host loss). |
Browser usage
The SDK has no Node-specific deps — globalThis.fetch, Uint8Array, and a
small structured-clone-safe envelope decoder are the only platform surfaces.
The one caveat: don't put your raw API key in a browser bundle. Either use a
session cookie + your own fetch wrapper, or talk to a Vercel/Cloudflare Worker
function that forwards requests with the key server-side.
Gotchas
sandboxes.createtakesslug, notname— the field that becomes the short human alias.sandboxes.execandsandboxes.listFilesreturn snake_case fields (exit_code,modified_ms) — the rest of the API uses camelCase. Already reflected in the SDK types.volumes.create,volumes.attach,volumes.detachuse snake_case input (size_gb,volume_id,mount_path). The SDK input types match.sandboxes.stopcan exceed 60s because the host takes an auto-snapshot before suspending. For hot sandboxes, bump the client timeout:new FrogletClient({ ..., timeoutMs: 180_000 }).volumes.searchreturns 503 on prod today — the API process is missingFROGLET_VOLUMED_URLenv vars. Operator-side fix.templateBuilds.listmay return state'ready'(template usable) or'error'.'done'is the in-between state after the build finishes but before the template row is created — usually transient.
Compared to the CLI
| What you want | CLI | SDK |
| ------------------ | -------------------------------------------------------------- | ------------------------------------- | ------------------------------ |
| Try things by hand | froglet sb new --template ubuntu-24.04 --size xs --name demo | not the right tool |
| Script lifecycle | bash + --json | jq | client.sandboxes.create(...) |
| Build into an app | doesn't fit | the whole point |
| Snapshot ID lookup | shown short in ls, full in --json | always full UUIDs in returns |
| Slug vs UUID | accepts either (resolves via list) | use UUIDs returned by list/create |
Both ship one-for-one — the SDK is what @kortix/froglet-cli shells out to
conceptually, and the dashboard uses the same client.
Source layout
clients/sdk/typescript/
src/
client.ts FrogletClient + options
core/
envelope.ts { data: T } | { error: { code, message } } unwrap
errors.ts FrogletApiError
http.ts HttpClient (Bearer, JSON, timeout, abort)
resources/
sandboxes/ everything per-sandbox: lifecycle, exec, files, PTY, backups
templates/
template-builds/
ports/
ssh-access/
volumes/
keys/
members/
me/
quotas/
admin/ hosts/orders/cells (platform-admin only)
index.ts barrel — re-exports every resource + types
version.ts VERSION string baked into the User-Agent
tests/
client.test.ts unit tests for HttpClient + envelope
resources.test.ts unit tests for every resource (mocked fetch)Unit tests (pnpm test) run in <1s with mocked fetch.
