הדף הזה עדיין לא תורגם לעברית: הנה הוא באנגלית.
Install
No dependencies: it uses fetch, WebSocket and node:crypto, and Node 23.6 or later runs its TypeScript sources directly. The package is glac on npm, with JavaScript and type declarations for runtimes that do not run TypeScript:
npm install glacFirst lines
import { Glac, Sandbox } from "glac";
const glac = new Glac(); // GLAC_API_KEY and GLAC_API_URL; or new Glac({ apiKey, baseUrl })
const app = await glac.cube.deploy("ghcr.io/acme/api:1.4", { name: "api", port: 8080 }); // Cube: waits until it serves
console.log(app.url);
const sbx = await glac.sandboxes.create({ image: "python:3.12" }); // Sandbox
console.log((await sbx.run("python3 -c 'print(6*7)'")).stdout); // res.stdout, res.stderr, res.exitCode
const drive = await glac.drives.create({ name: "data", sizeBytes: 10 * 2 ** 30 }); // Drive: disks that outlive sandboxes
console.log((await Sandbox.run("echo hello")).stdout); // the shorthand: a sandbox for one command, on the default clientThe client
new Glac({ apiKey, baseUrl, retries }) is one key at one address; what is not given is read from GLAC_API_KEY and GLAC_API_URL. It has a namespace for each product:
| Namespace | What it is | Calls |
|---|---|---|
glac.cube |
Cube: images that run as services at addresses of their own (the API calls them apps, /v1/apps). glac.workers is another name for it, marked deprecated |
deploy(image, options), create(options), get(idOrName), getOrCreate(name, options), list() |
glac.sandboxes |
Sandboxes (/v1/sandboxes) |
run(command, options), create(options), get(idOrName), getOrCreate(name, options), list({ status, metadata }) |
glac.sandboxes.snapshots |
The sandboxes’ snapshots | get(idOrName), list({ sandboxId }) |
glac.sandboxes.sshKeys |
SSH keys into sandboxes and app instances | create(publicKey, { name, sandboxId, expiresAt }), get(idOrName), list({ sandboxId }) |
glac.drives |
Drives: disks of the project’s that outlive their sandboxes, mounted read-write by one at a time, and their snapshots, which any number mount read-only (/v1/drives, a control plane’s) |
create({ name, sizeBytes, region, durability }), get(idOrName), list(), update(drive, { name }), delete(drive), attach(sandbox, drive, path, { mode }), detach(sandbox, drive, { force }), snapshot(drive, { name, fsConsistent }), snapshots(drive), getSnapshot(drive, snapshot), deleteSnapshot(drive, snapshot), fork(drive, { name, fromSnapshot }), restore(drive, snapshot), attachSnapshot(sandbox, snapshot, path, { drive }), detachSnapshot(sandbox, snapshot, { force }) |
glac.volumes |
Volumes: the disks underneath Drives (/v1/volumes), kept for the transition: a node that stands alone, and shared volumes |
create({ name, sizeBytes, shared, fromBackup }), get(idOrName), list(), backup(volume, { expiresInDays }), backups(volume) |
glac.backups |
The backups of volumes: whole copies of a volume at an instant, in the object store, which outlive it (/v1/backups, a control plane’s) |
list({ volume }), get(backup), delete(backup), restore(backup, { name }) |
glac.secrets |
The project’s secrets | put(name, value, { allowedHosts }), update(name, allowedHosts), list(), delete(name) |
glac.variables |
The project’s shared variables | put(name, value), list(), delete(name) |
glac.deploys |
The project’s deploys (/v1/deploys, a control plane’s): several of its services deployed together, in the order of what each waits for, and switched together (a project’s deploy) |
create(services, { wait, timeoutSeconds }), get(id), list(): create takes each service’s version fields as the API names them ({}: as it is), waits for the deploy (following one that took its place), and throws DeployError with deploy, service and the deploy’s code when it fails |
glac.domains |
The project’s own domains (/v1/domains, a control plane’s): a name for an app or for a port of a sandbox |
create(domain, { app } or { sandbox, port }), get(idOrName), list({ app, sandbox }), delete(idOrName) |
glac.webhooks, glac.keys |
Where the project’s events are sent (/v1/webhooks), and its API keys (/v1/keys), a control plane’s |
create, get, list, update (webhooks), delete |
glac.destinations |
Where the logs, metrics and traces of the project’s apps are exported (/v1/destinations), a control plane’s |
create, get, list, update, delete, test |
The platform’s own calls are the client’s methods: glac.account(), updateAccount(), usage(), billing(), updateBilling(), balance(), updateBalance(), topUp(usd), charges(), ledger(), project() and events(). An API the SDK does not wrap: glac.json("GET", "/v1/webhooks"), and glac.pages(path) for lists. What a call returns (a Sandbox, an App, a Drive, a Volume) keeps its client, and its own methods use it.
The shorthand: Sandbox.run, Sandbox.create, Sandbox.get, Sandbox.getOrCreate and Sandbox.list are the calls of glac.sandboxes on the default client, which is made from GLAC_API_KEY and GLAC_API_URL the first time; { client } names another. No other product has one.
undefined options are not sent. null means something on some options: sleepAfterSeconds: null is never sleep, sleepNoticeMs: null in an update tells its programs nothing more before it sleeps, env: { KEY: null } deletes a key, egress: null removes all network rules.
Cube
A Cube, which the API calls an app, is an image that runs as a service at an address of its own, with as many instances as its requests need and none while there are none. The SDK’s App is one.
import { DeployError, Glac } from "glac";
const glac = new Glac();
const app = await glac.cube.deploy("ghcr.io/acme/api:1.4", { name: "api", port: 8080 });
try {
await app.deploy({ image: "ghcr.io/acme/api:1.5" }); // a new version, waited for
} catch (e) {
if (!(e instanceof DeployError)) throw e; // it did not come up: the traffic stays where it was
}
for await (const line of app.logs({ follow: true })) if (line.kind === "output") console.log(line.text);| Call | What it does |
|---|---|
glac.cube.deploy(image, { name, ...every option of create, wait, timeoutSeconds }) |
As glac deploy does: to the app of name, its own settings that are given changed first (PATCH) and a version made of the rest; to a name no app has, or with none, a new app with its first version. Waits for the version unless wait: false, throws DeployError when it fails, and returns the app. |
glac.cube.create(...), get(idOrName), getOrCreate(name, options), list() |
Makes an app (with an image, its first version is waited for); finds one; finds or makes one, deploying nothing; lists them. drives are the Drives its one instance mounts read-write (an app of many instances or of keys is refused, 409 drive_single_writer); keyDrives give each key’s instance of a keyed app Drives of its own, made with its first instance and deleted with the key. |
app.deploy(...), app.rollback(version), app.update(changes), app.versions(), app.delete() |
A version of what is given; traffic back to another version; a change (a version where what a version is made of changes); the versions; delete. |
app.managedBy |
What changes the app alone (a ManagedBy). The database it is under, { object: "database", id: "db-…" }: an update, a deploy, a rollback or a delete of it here is refused, 409 managed_by_database (its log is read and cut as any app’s). Or the Sandboxes product, { object: "sandboxes", id: null }: the project’s default sandbox app, sbx-sandbox, or the app of an image a sandbox was made of, whose instances are the project’s sandboxes; every change but naming the default app’s image is refused, 409 managed_by_sandboxes, and it may be deleted. null for an app that is none’s. |
runtime of glac.cube.create, deploy, app.deploy and app.update |
The version’s runtime settings, a ServiceRuntime: ulimits ({ nofile: 65535 }), sysctls, tmpfs, dns, shm_size_bytes; replaced whole (null in an update: none). |
app.logs({ follow, since, ... }), app.logUsage(), app.logsState, app.instance(key) |
The log (for await, which comes back by itself after a lost connection), what it counted, whether each machine that keeps it keeps what is printed there now for the long term (logs), and keyed instances. |
instance.upgrade({ version, when, keepDays }), app.upgradeInstance(key, { version, when, keepDays }); await upgrade.wait({ timeoutSeconds }); instance.upgrades(), app.instanceUpgrades(key), app.instanceUpgrade(key, id) |
Moves a key’s instance to another version of the app, in place: the app’s current version unless version names one (an id, a number or a Version; one that is ready, an older one too). It keeps its id, its node, its key, its address, its tokens and its own Drives with what they hold; its disk, its machine and its processes become the new version’s, its open connections end, and its requests are held at the edges while it is switched. Resolves to an Upgrade (status preparing, switching, done or failed; fromVersion, toVersion, timings, error, putBack; kind (upgrade, or revert), when, keepDays, rollback (what a revert of it needs, kept for keepDays), reverts, batch; an upgrade asked through the API to wait for its instance to sleep is waiting until then, and cancelled where it was cancelled (the API’s DELETE, or its batch stopped): one that was cancelled is over, and its wait raises as one that failed does, with the code cancelled), or null where the instance is on that version already. wait() throws an UpgradeError (its code upgrade_failed or upgrade_not_here) where the new version did not come up: the instance is then on the version it was on. instance.refresh() reads the instance again, its versionId among the rest. when: "asleep" has it wait (waiting) until the instance sleeps and begin then, so that none of its requests is held; keepDays (0 to 365, 7 unless given) is how long what a revert needs is kept once it is done, billed as snapshots are. A version whose services mount other Drives or volumes than the instance’s is no upgrade in place: it ends failed with upgrade_not_here. |
upgrade.cancel(), app.cancelInstanceUpgrade(key, upgrade); upgrade.revert({ keepDays }), app.revertInstanceUpgrade(key, upgrade, { keepDays }) |
An upgrade that waits (for its instance to sleep, or for its turn in a batch) cancelled: cancelled, the instance on its version (one that has begun is 409 upgrade_started). A revert: the instance back to what an upgrade kept, its disk, its Drives as they were before it (a snapshot of what they hold now made first) and the version it came from; a new Upgrade of kind revert, reverts naming the upgrade, which goes and is waited for as an upgrade is. Refused 409 rollback_gone where what it goes back to is not kept any more. |
app.upgradeInstances({ version, fromVersions, keys, when, keepDays, concurrency, maxFailures }); app.upgradeBatches(), app.upgradeBatch(id), app.stopUpgradeBatch(id); batch.wait({ timeoutSeconds }), batch.refresh(), batch.stop() |
A batch of upgrades of the app’s keys to one version (the app’s current one unless given), of fromVersions and among keys where given: each once its instance sleeps (when: "asleep", the default) or as its turn comes ("now"), concurrency at once (4; two on one node), stopped failed after maxFailures (5). Resolves to an UpgradeBatch: status (running, done, failed, stopped), stopReason, counts, upgrades (each member’s key, upgrade, status and error), skipped, origin (api, or deploy where the app’s keys say upgrade_on_deploy). wait() resolves however it ended, throwing nothing; a member whose instance never sleeps keeps it running, so timeoutSeconds throws the wait_timeout error. stop() cancels the members that wait. |
app.openInstancePort(key, port, { from, protocol }), app.instancePorts(key), app.closeInstancePort(key, port) |
The ports open at the own public IPv6 address of a key’s instance, wherever it is: its sandbox’s openPort, ports and closePort, by the app and the key (keyed instances). An instance with no address of its own is refused (409 no_ipv6). |
checkpoint of glac.cube.create, deploy and app.update; app.checkpoint; sbx.checkpoint |
Checkpoints of the app’s keyed instances while they are awake (an AppCheckpoint, { everySeconds }: 1 to 600, 10 when it is left out or null; app.update({ checkpoint: null }) takes them away), and how an instance’s go (a SandboxCheckpoint: everySeconds, lastAt, behindSeconds, bytesPerSecond, storedBytes, error; null without them). |
app.metrics({ start, end, step }) |
What the machines that serve the app counted of it, a step a minute (step: "hour": an hour): requests by the class of their status, how long they took, its instances and what they used (metrics). The API’s object. |
traces of glac.cube.create, deploy, getOrCreate and app.update; app.traces |
A span of the platform’s for a share of the app’s requests, which the instance’s own spans are children of (an AppTraceSettings: { sampleRate: 0.1, trustIncoming: false }; none unless given), sent to the project’s destinations that take "traces" (traces); what the app has. |
app.createKeySecret(), app.keySecrets(), app.keySecret(id), app.deleteKeySecret(id), signKey(secret, appId, key, { expiresAt, path }) |
The secrets an app’s keys are signed with (a KeySecret, whose secret only the one just made has), and a key signed with one, as a request through the edge carries it (signed keys); path: true writes it as a path segment. |
Sandbox
| Call | What it does |
|---|---|
glac.sandboxes.create({ image, snapshot, name, env, metadata, volumes, drives, publicPorts, sleepAfterSeconds, sleepNoticeMs, sleepHoldMaxMs, expiresAt, egress, imagePullSecret, start, startCommand, ready, projectNetwork, hostname, stop, checkpoint }) |
Makes a sandbox; every option may be left out. drives: [{ path, drive }] mounts Drives, and [{ path, drive, snapshot }] a Drive’s snapshot, read-only (below). A service started when it boots: start: true (the image’s own start command) or startCommand, with ready: { tcp: true, port: 5432 } (or { http: "/health", port: 8080 }, { command: "…" }); hostname names its machine and stop: { signal, graceMs } says how its service is stopped; projectNetwork: true puts it on the project’s private network from the start, at sbx.projectAddress; a volume’s copy: true gives a new volume what the image has at its path (making a sandbox). |
sbx.appId, sbx.versionId, sbx.key, sbx.imageDigest |
A sandbox made through the control plane is an instance of an app: the project’s default sandbox app (sbx-sandbox) when it was made with no image, else the app of its image ((await glac.apps.get(sbx.appId)).instances() lists that app’s sandboxes, each under its key); the version of that app it is of, the image it was made of (a tag that was moved since is a new version of the app, which the sandboxes made after it are of); its key there, its name or its id; and its image by digest. Said by its create’s answer and by every read of it, and kept; null for one made from a template or a snapshot, and for a fork. |
sbx.startCommand, instance.startCommand |
How its start command runs, as its guest said last (a StartCommandState): status (running; restarting, started again at restarting_at; exited), restarts (a key’s instance and a stateful service’s are started again in their machine when their command ends on its own, each start an event instance.restarted), last_exit (status, signal, at); null where its node said nothing of it. |
suspendAfterMs, wakeDailyAt of create and sbx.update; sbx.suspendAfterMs, sbx.wakeDailyAt |
How long it stays paused, once idle has stopped it, before its memory is saved and it sleeps for real (a second to a day; its node holds it to ten minutes at the most), and a time of day in UTC, "HH:MM", at which it is woken every day while it rests, for work of its own that waits for a wake; null in an update takes either back. |
onExpiry, heldAsleep of create and sbx.update; sbx.onExpiry, sbx.heldAsleep |
What expiresAt does when it comes: "delete" the sandbox (the default), "sleep" (put it to sleep and keep it, a request waking it as ever) or "hold" (the same, held asleep); and whether a request to one of its ports wakes it while it sleeps: held asleep, its preview address answers 502 sandbox_paused, and a call of the API wakes it. null in an update takes either back. |
init: "systemd" of create and run; sbx.init |
A whole system: the image’s own systemd is the sandbox’s PID 1 (without image, the platform’s Ubuntu with systemd and OpenSSH), so packages, services started with systemctl, cron and the journal work as on a machine, and it has a machine ID of its own, kept through sleeps, wakes and moves. Not with start, startCommand or ready (400 invalid_start), nor with snapshot. sbx.init is "systemd" for one and null for the platform’s init. |
glac.sandboxes.run(command, options) |
A sandbox for one command: made, used, deleted. |
glac.sandboxes.get(idOrName), getOrCreate(name, options), list({ status, metadata }) |
Finds one; finds or makes one; lists. |
sbx.run(command, { cwd, env, user, timeoutSeconds, stdin, onStdout, onStderr, signal }) |
Runs and waits. Returns a Result. |
sbx.exec(command, { stdinOpen, tty }) |
Starts and returns a Command: wait(), output(from), write(), kill(). |
sbx.terminal({ cols, rows, join, onData }) |
A terminal over WebSocket: write, resize, close. |
sbx.files |
read, readText, write, list, stat, exists, mkdir, move, remove, and setAttributes(path, { mode, sizeBytes, modifiedAt }): a file’s mode, its length, its time of last modification. |
sbx.files.upload(localPath, path), download(path, localPath), readStream(path, { offset, length }), write(path, blobOrStream) |
A file of any length in one call, held whole nowhere, neither here nor by the API: upload sends a local file as it reads it, download writes the bytes to a local file as they come (put in place when every byte has come), readStream gives a ReadableStream of them, and write also takes a Blob, a ReadableStream or an async iterable of bytes. A stream that ends short of the length the answer said errors at its end: read it again. |
sbx.url(port), sbx.rotateUrls(), sbx.token({ scopes, expiresInSeconds }) |
A link to a port (signed locally); new link secret; a token for this sandbox. |
sbx.ipv6, sbx.openPort(port, { from, protocol }), sbx.ports("tcp"), sbx.closePort(port, { protocol }), inbound: […] of create and update |
Its own public IPv6 address, where it has one (null otherwise); a port opened at it to the sources listed (IPv6 prefixes or addresses, "::/0" for everybody), with where it is reached; the ports open; one closed (it answers whether it was open); the whole list. Inbound is closed by default; what is open stays open while it sleeps and when it moves. See networking. |
sbx.revokeToken(token) |
That token alone is refused from then on, and every other works as before: what token() gave (a SandboxToken, whose id, tok_…, says which), the token itself, or its id. A sandbox keeps 1,000 such revocations that have not run out (409 too_many_revoked_tokens past them: revokeTokens() then). Returns a TokenRevocation with its tokenId and expiresAt. |
sbx.revokeTokens() |
Every token handed out for this sandbox so far stops working (401 invalid_token), those in preview links and cookies and those SSH sessions hold too; tokens made after it work. Needs the API key. Returns a TokenRevocation: sandboxId, generation, revokedAt. |
sbx.update(changes), sbx.sleep(), sbx.restart(), sbx.delete() |
Change it; sleep now; boot from disk; delete. |
checkpoint of glac.sandboxes.create and sbx.update; sbx.checkpoint |
Checkpoints while it is awake ({ everySeconds: 10 }; null in an update takes them away), so that it goes on with its processes when its machine is lost; how they go. 409 checkpoints_unavailable where no machine takes them. |
sbx.diskWrites |
What its disk has written today (a UTC day), against the day’s share at full speed (a SandboxDiskWrites): todayBytes, of which checkpointCopyBytes its checkpoints’ copies wrote to the host’s disk; fullSpeedUntilBytes, the share (100 GiB unless its node is told otherwise; null: its node slows nothing, and counts all the same); limited and limitedUntil (a Date): past the share its disk takes writes at 2 MiB a second until the day ends, and no write fails. Null on a node that does not copy its disks to an object store, which counts nothing. What it writes to its Drives is not counted here: writing to a Drive costs nothing. |
sbx.snapshot(), sbx.fork(), sbx.restore(snapshot), glac.sandboxes.snapshots, snap.update({ name, expiresAt }) |
Snapshot, fork, back to a snapshot in place; the snapshots; another name for one (null: none), or when it deletes itself (null: never). |
glac.sandboxes.sshKeys.create(publicKey, { sandboxId, expiresAt }) |
An SSH key for one sandbox, or for every sandbox of the project; a private key is refused before anything is sent. |
sbx.runCode(code), sbx.agent("claude"), new Desktop(sbx) |
Code execution, coding agents, and the screen of a desktop sandbox. |
Output as it comes, files, snapshots and forks:
const built = await sbx.run("make", { onStdout: (text) => process.stdout.write(text) });
const cmd = await sbx.exec("node server.js"); // do not wait
for await (const piece of cmd.output()) process.stdout.write(piece.text);
await sbx.files.write("/tmp/notes.txt", "hello\n");
const before = await sbx.snapshot({ name: "before-upgrade" }); // memory, processes and disk
const [copy] = await sbx.fork(); // goes on from this moment
await sbx.restore(before); // back to the snapshot
await copy.delete();Drive
A Drive is a disk of the project’s that outlives its sandboxes: mounted read-write by one sandbox at a time, when the sandbox is made or while it runs, and kept, with what it holds, when the sandbox goes, for the next sandbox to mount: of its region, or of another whose machines read its home’s object store (below). It is copied into its region’s object store as it is written, so a sandbox on another node mounts it too: it is brought there first. Only what is written is billed (usedBytes). Drives are a control plane’s (/v1/drives): against a node that stands alone every call throws the node’s 404 no_such_route, and its disks are volumes (below).
const drive = await glac.drives.create({ name: "data", sizeBytes: 10 * 2 ** 30 }); // or none: 64 GiB, or 5 GiB on Free
const sbx = await glac.sandboxes.create({ drives: [{ path: "/data", drive: drive.id }] });
await sbx.files.write("/data/notes.txt", "hello\n");
await glac.drives.detach(sbx, drive);
const other = await glac.sandboxes.create();
await glac.drives.attach(other, drive, "/data"); // the same Drive, in another sandboxA Drive has id (a vol-…: a Drive is a volume), name, sizeBytes, usedBytes, attachedTo (the sandbox, the path, the mode), region, home, status, lease (the node of its one writer, the epoch, until when), appId and key (a keyed instance’s), parentDriveId and rootDriveId (a fork’s: the Drive it was forked from, which may be deleted since, and the first of its line; null for a Drive that is no fork), managedBy (a ManagedBy, of a database’s Drive; null for one that is none’s), and attach, detach, update({ name }), refresh() and delete() of its own.
- A Drive is one sandbox’s at a time: one that another sandbox has is refused (409
drive_attached), and so is deleting one that is attached. A keyed instance’s Drive goes with its key (409key_drive). - A database’s Drive (
drive.managedBy) is mounted read-write by the database’s instance alone, and renamed, restored and deleted through the database: those calls here, and a read-write mount of it, are refused (409managed_by_database). Its snapshots and forks are the project’s to take, and a snapshot of it anybody’s to mount read-only. - One that another node holds is brought to the sandbox’s node first; while the object store does not hold it as it is yet (503
copy_behind), or it could not be brought (503drive_not_brought), the call isretryable: the client tries again as itsretriessay, then throws, for you to ask again a moment later. - Across regions (slice D9). A Drive’s home (
home) is the region whose object store holds its copy: the region it was made in. A sandbox of another region mounts it too, where that region’s machines read its home’s store (409not_on_one_machinewhere they do not): the Drive is let go as a version and taken over there, read from its home’s store as its guest reads it, and what its guest writes goes to the store of the region it is in now (region). Its home follows it once it has been there for an hour, or at once withglac.drives.moveHome(drive)(drive.moveHome()): its machine copies what is left in its home’s store into its own, andhomeis the new region once nothing is left there;drive.homeMovesays how far the move is (asked_at,asked_by,copied_objects,copied_bytes,objects_left,error). Its old home’s copy goes once no version kept needs it (a day after). A Drive’s home moves once in six hours at the most (409home_moved_recently); one whose home is the region it is in is 409home_here. - The plan’s limits: 3 Drives of at most 10 GiB, 15 GiB together, on Free; 500 of at most 1 TiB on Paid (403
drive_limit_reached). glac.usage({ drive }),kind: "drive"andgroupBy: "drive"report what Drives kept, were written and read cold, andglac.usage({ snapshot: "dsnap-…" })what a snapshot kept alone. The eventsdrive.created,drive.deleted,drive.attachedanddrive.detachedcarry anEventDriveor anEventDriveAttached, anddrive.snapshot.createdanddrive.snapshot.deletedanEventDriveSnapshot.
Snapshots, forks and restores. A snapshot of a Drive is a version of it in the object store: nothing is copied. A Drive a running sandbox writes is snapshotted with its writes held for a moment: with fsConsistent: true the sandbox’s agent freezes its file system (consistency "frozen"; 409 not_frozen where the agent cannot: take it without), otherwise its machine is paused ("paused", the Drive as at a power cut, which its file system’s journal is for); one no running sandbox has is taken as it is ("at_rest"). The hold lasts ten seconds at the most, then the call is refused, 503 unavailable (retryable).
const snap = await drive.snapshot({ name: "before" }); // a version in the object store: nothing is copied
const reader = await glac.sandboxes.create({ drives: [{ path: "/before", drive: drive.id, snapshot: snap }] }); // read-only
await glac.drives.attachSnapshot(sbx, snap, "/before"); // into a running sandbox too, as many as want it
const copy = await snap.fork({ name: "data-2" }); // a new Drive, mountable at once
await glac.drives.detach(other, drive);
await drive.restore(snap); // detached first: back to the snapshot| Call | What it does |
|---|---|
glac.drives.snapshot(drive, { name, fsConsistent }), snapshots(drive), allSnapshots({ driveDeleted }), getSnapshot(drive, snapshot), deleteSnapshot(drive, snapshot) |
Takes a snapshot (its name need not be the only one); the Drive’s snapshots, newest first (by the Drive’s id once it is deleted), or the project’s of every Drive (driveDeleted: true: those of Drives deleted since alone); one by its id (404 snapshot_not_found if the Drive has none of that id); deletes one: the forks made of it keep what they need, and a sandbox that has it mounted keeps the mount until it is detached. |
glac.drives.fork(drive, { name, fromSnapshot }) |
A new Drive whose first version is the snapshot’s, or the Drive’s as it is now: nothing is copied, and it is mounted at once. The Drive’s size, held to the plan’s Drives as a create is (403 drive_limit_reached); its parentDriveId and rootDriveId say what it was made of. |
glac.drives.restore(drive, snapshot) |
Brings the Drive back to one of its snapshots, and returns it. |
glac.drives.create({ historySeconds }), update(drive, { historySeconds }), versions(drive, { at }), fork(drive, { at }), restore(drive, undefined, { at }); drive.versions({ at }), drive.historySeconds, drive.history |
A Drive’s window of versions (slice DB8): how far back it can be restored, or forked, by a time, 3600 to 86400 seconds (none unless given; turned off, what it kept goes). Every 30 seconds in which it was written the window gets a version, the Drive at one moment; what they keep alone is billed as a snapshot’s is. A restore or a fork to a time takes the newest version at or before it, which the versions call with that time names. versions gives DriveVersions, the newest first; restore takes a snapshot or { at }, one of the two (a TypeError here for both or neither); historySeconds: null turns the window off. |
glac.drives.create({ cacheBytes }), update(drive, { cacheBytes }); drive.update({ cacheBytes }), drive.cacheBytes, drive.cachedBytes |
A Drive’s cache: how much of it its node keeps on its NVMe at the most: all of it unless given (its size); less, down to 0, and what is past it is in the object store alone, read from there when its guest reads it (the least recently read let go first, and never what the store does not hold yet). The cache is billed by the hour as it is filled, beside what the Drive holds. A smaller one lets go of what is past it once the store holds it (of a Drive a running sandbox mounted with all of it cached, from that sandbox’s next sleep or stop); a larger one keeps what its guest reads from then on. The API calls Drives volumes (/v1/volumes); the clients reach them by /v1/drives, which answers as it did. cacheBytes: null in an update is all of it again. drive.cacheBytes is its size where the control plane says nothing of a cache; drive.cachedBytes, what of it is on its node’s NVMe now (null where no node holds it). A usage report’s groups and totals have cache_gb_hours, what the caches kept. |
glac.drives.attachSnapshot(sandbox, snapshot, path, { drive }), detachSnapshot(sandbox, snapshot, { force }) |
Mounts a snapshot read-only in a running sandbox, and takes the mount away by the snapshot’s id; both answer the DriveSnapshot. A DriveSnapshot says its Drive; a snapshot’s id alone needs drive, or it is refused before anything is sent (400 missing_field). |
drive.snapshot(), drive.snapshots(), drive.fork(), drive.restore(snapshot); snap.refresh(), snap.delete(), snap.fork({ name }), snap.restore(), snap.attach(sandbox, path), snap.detach(sandbox) |
The same on a Drive and on a DriveSnapshot, which has id (dsnap-…), name, driveId, consistency, usedBytes (what the Drive held), exclusiveBytes (what it keeps alone), exclusiveAt, version and createdAt. |
- A snapshot outlives its Drive: it is listed, read, forked and mounted read-only after the Drive is deleted, by the Drive’s id. It is billed for what it keeps alone (
exclusiveBytes), by the hour, at the Drives’ data price ($0.02 a GB-month), until it is deleted, after its Drive is too. - A fork is billed by its
usedBytes, its full logical contents, like any Drive: a fork of a 10 GB snapshot is billed as a 10 GB Drive. - A restore writes a new version after the Drive’s newest whose content is the snapshot’s; the versions in between stay where a snapshot or a fork keeps them, so restoring to an older snapshot and then to a newer one works. What the Drive holds now that no snapshot keeps is gone: take one first. A Drive a sandbox has read-write is not restored (409
drive_attached): detach it first. - A read-only mount: any number of sandboxes, on any machine of the Drive’s region, whatever has the Drive and after it is deleted. Its blocks come from the object store as the guest reads them, at no cost; they show under the sandbox that mounts it (
glac.usage({ sandbox }),drive_cold_read_bytes); what its machine holds already costs nothing. The guest cannot write it, and a sandbox that has one cannot move to another machine. The sandbox lists it among itsvolumes, withview_of. durabilityofglac.drives.createisstandard, orstrict, whose guest’s fsync returns once the object store has every write before it; a Drive’sdurabilitysays it, for good (how durable a write is).- A node that copies nothing to an object store has no snapshots, forks or restores (409
not_copied). An app’s version mounts a Drive read-write only: read-only snapshot mounts are a sandbox’s.
glac.volumes is the API’s /v1/volumes, the disks underneath Drives, kept for the transition: what only it has is a node that stands alone, whose disks are volumes, and shared volumes, file systems several sandboxes mount at once. A sandbox or an app mounts one as volumes: [{ path: "/data", volume: vol.id }].
const team = await glac.volumes.create({ name: "team", shared: true }); // no size is needed
const box = await glac.sandboxes.create({ volumes: [{ path: "/team", volume: team.id }] });Backups of volumes. A backup is a copy of a volume (a Drive is one) at one instant, in the object store, kept until it expires: taken by a call, every day by the volume’s rule, or before a service’s new version replaces its instance. Unlike a snapshot of a Drive it is a copy of its own, a volume is made from it alone, and it outlives its volume; a volume’s backups keep only what changed since the one before, and a block several of them need is billed once, on the earliest. It is making until the store has all of it, then ready, and billed from then by what the store holds of it (storedBytes) at the price of what sleeping sandboxes keep (backups). Backups are a control plane’s.
const b = await glac.volumes.backup("data", { expiresInDays: 7 }); // "making", then "ready"
while ((await b.refresh()).status === "making") await new Promise((r) => setTimeout(r, 1000));
const back = await glac.backups.restore(b, { name: "data-back" }); // a new volume: "restoring", then "ready"glac.volumes.backup(volume, { expiresInDays })(orvol.backup()) takes one now, keptexpiresInDays(1 to 365) or as the volume’s rule says (30 days unless it was set);glac.volumes.backups(volume)lists a volume’s, newest first (a deleted volume’s by its id). A node that copies nothing to an object store has none (409not_copied).glac.backups.list({ volume })lists the project’s, newest first, those of deleted volumes among them;get(backup),delete(backup)(nothing more is billed for it; a volume made from it stays), andrestore(backup, { name }), a new volume made from a ready one, the same asglac.volumes.create({ fromBackup, name })(409backup_not_readywhile it is being made, 409backup_unusableonce it failed). ABackuphasid(bak-…),volumeId,status(making,ready,failed),reason(manual,daily,before_version),storedBytes(what it is billed for),ownBytes(what deleting it would save),sharedBytes,volumeName(the volume’s name then),consistency(frozen,crash),frozenMs(how long the volume’s writers waited for the copy; null where it was not frozen),sizeBytes(what the volume held),volumeSizeBytes(the most it held: what a volume made from it holds),shared,region,failure,createdAt,readyAt(from when it is billed),expiresAt, andrefresh(),restore({ name }),delete().- A volume made from a backup has the size and kind of the volume it was taken of (
sizeBytesandsharedare not given withfromBackup), in the backup’s region. Itsstatusisrestoringuntil it holds the backup whole, thenready, and nothing attaches it before;fromBackupnames the backup. Where the machine it was placed on cannot bring it back, the control plane makes it once more on another machine of the region:info.restore_attemptis 2 then, andinfo.first_failuresays why the first try failed. If that fails too, it isfailed(info.failure), and can only be deleted. glac.usage({ backup }),kind: "backup"andgroupBy: "backup"report what backups kept, on the meter of state (state_gb_hours);meters.state.backups_gb_hoursofglac.billing()says how much of that meter was backups.
Domains
A domain of the project’s is a name that leads to an app, as its default address does, or to a port of a sandbox. Domains are a control plane’s.
| Call | What it does |
|---|---|
glac.domains.create(domain, { app }) or { sandbox, port }, get(idOrName), list({ app, sandbox }), domain.update(...), domain.refresh(), domain.delete() |
A name for an app or a sandbox’s port, pending_verification until DNS shows a record of verification.records (your own domain); 409 domain_exists when the project has it. |
const name = await glac.domains.create("api.example.com", { app: "api" });Browser
Not open yet. Browser is not open to accounts yet: what follows is the SDK as it stands, ahead of its opening.
A session is a hosted Chromium that Playwright, Puppeteer or any other client of the Chrome DevTools Protocol (CDP) drives from outside. It is awake while a client is connected and falls asleep, its pages kept, sleep_after_seconds after the last one leaves. Its cdpUrl is all a CDP client needs: the credential is in the address’s host name, so it stays out of the session’s JSON.
await using session = await glac.browsers.create({ name: "agent-42" }); // closed and deleted at the end of the block
const browser = await session.connect(); // Playwright, where it is installed
const page = browser.contexts()[0].pages()[0];
await page.goto("https://example.com");-
glac.browsers.create({ kind, name, region, sleep_after_seconds, connection_idle_seconds, delete_after_idle_seconds, metadata, profile, proxy })makes one, awake (kind:chromium-headless, the default,chromium, on a screen, orlightpanda, a browser without a layout engine); the name of one there is answers that one, woken, with its pages.profile: { name, save, version }loads a profile before the session is answered (below).get(idOrName),list({ status, kind, name })(status:awake,asleep,unavailableorfailed),update(session, changes),delete(session),sleep(session),rotateUrl(session); aBrowserSessionhas the same of its own. One whose sandbox fails before it runs is no session: its create is refused, 503session_failed. -
session.statusisawake,asleep,unavailable(its node cannot be reached) orfailed(its sandbox failed after its create was answered: its browser stopped and did not come back, say), and thensession.errorsays why ({ code, message }, as its node says it;nullunless it failed). The eventbrowser.failedsays so too (its data typedEventBrowserFailed). -
session.cdpUrlis whatchromium.connectOverCDPand Puppeteer’sbrowserURLtake.rotateUrl()gives new addresses: the old ones are refused from then on, and the connections they let in end. -
session.connect({ chromium, timeoutMs })connects Playwright:playwrightorplaywright-corewhere one is installed (neither is this package’s dependency), or thechromiumgiven.close()closes that browser; the session stays, and sleeps once no client is connected. -
session.liveUrlis where its page is watched live in a browser (a headless session’s tab drawn as it changes, a headful one’s screen), the viewer’s mouse and keyboard reaching the page;await session.live({ interactive: false })gives an address of its own where nothing reaches the page, to show people. A viewer keeps the session awake. Both are rotated byrotateUrland need the key’surlscope. -
Profiles (its cookies, Local Storage, IndexedDB, Session Storage and preferences, kept by the platform for the project):
create({ profile: { name: "acme-login", save: true } })loads one and saves it again when the session is deleted (delete({ saveProfile: false })does not);session.saveProfile(name?)saves it now (the browser restarts, and a connected client reconnects; another save of the profile meanwhile is refused 409profile_busy);session.profileis the one it holds;glac.browsers.profiles.list(),get(name),delete(name). -
Your own proxy, on Paid (403
proxy_not_on_planon Free):create({ proxy: "http://user:password@proxy.example.com:3128" })(or{ url }) has the session’s connections to ports 80 and 443 go out through that HTTP proxy, which takesCONNECT, by a relay beside the browser, which is told nothing of it; QUIC is refused there, so the browser speaks HTTP over TCP.session.update({ proxy })changes it on a running session without starting the browser again (its connections through the one before end),{ proxy: null }takes it away. The credentials are kept by the platform and are in no answer:session.proxyis{ url, username }, andsession.proxyByteswhat went through it, both ways (proxySentBytesandproxyReceivedBytes, each way). A page whose proxy takes no tunnel is answered 502proxy_failedover HTTP. See your own proxy.
A session is billed by the Cube resources it uses, as any instance is: its CPU while it computes, its memory while it is awake and its traffic out (pricing and plans). session.awakeSeconds is how long it has been awake. What went through its proxy is its traffic as any. Sessions are a control plane’s.
Databases
A database is a managed Postgres: a PostgreSQL of its own on a Drive, asleep when nothing asks anything of it and woken by a connection, its sessions kept. It is made in about two seconds, with a root branch, main, and an owner, glacdb_owner (a member of glac_superuser, no superuser). The owner’s password is in the answer of create and of resetPassword alone, and kept nowhere: connectionString() of the database one of those returned has it.
import pg from "pg";
const db = await glac.databases.create("shop"); // ready, with the owner's password
const client = new pg.Client({ connectionString: db.connectionString() }); // from a sandbox or a service of the project
await client.connect();
await client.query("create table orders (id bigserial primary key, total numeric)");glac.databases.create(name, { version, region, database_name, size, storage, sleep_after_seconds, suspend_after_seconds, wake_daily_at, metadata })makes one and waits for its first start:ready, orcreating(db.refresh()until it is ready).version: 16, 17 or 18 (the default);size: { vcpus, memory_bytes }, upper bounds (1 vCPU and 1 GiB);storage: { size_bytes, durability }, its Drive,strict(the default: a commit returns once the object store has it) orstandard;suspend_after_seconds: how long it stays paused, a connection answered in a millisecond or two, before its memory is saved and it sleeps for real, 1 to 86400 (300);wake_daily_at: a time of day in UTC,"HH:MM", at which it is woken every day while it rests (itspg_cronjobs wake it at their own times anyway). The name of one there is answers that one, without its password.update(db, { suspend_after_seconds, wake_daily_at })(anddb.update) changes these two, every branch’s at once;nulltakes either back (to 300, and to none). On a Free plan a database’s rest is fixed, as Neon’s Free scales to zero: paused a second after its last query (sleep_after_seconds1) and asleep five minutes later (suspend_after_seconds300); another value is refused 400invalid_field, on a create and on a change.glac.databases.createBranch(db, name, { parent, expires_at })(anddb.createBranch(name, { parent, expires_at })) makes a branch of its own: its parent (mainunlessparentnames another) as it is now, nothing copied, with a PostgreSQL, an endpoint and a Drive of its own, at<database>-<name>on the project’s network, deleted by itself atexpires_atwhere one is given. Its roles and passwords are its parent’s: the branch’sconnectionString()has the owner’s password where theDatabaseit was made from has it.deleteBranch(db, branch)(db.deleteBranch(branch),branch.delete()) stops its PostgreSQL and deletes its Drive; notmain(409root_branch), nor a branch others were made of (409has_branches).glac.databases.create(name, { history_seconds }),update(db, { history_seconds }),restore(db, { at, exact })andcreateBranch(db, name, { from: { at, exact } })(anddb.update,db.restore,db.historySeconds,db.history): its history (slices DB8 and DB5): how far back its root branch can be restored, or branched from, by a time: a day unless given; 0 (none) to 30 days on Paid, a day on Free. A restore takes the root branch back in place: to a whole minute or a version’s own moment, the newest version of its Drive’s window at or before it (one every 30 seconds in which it was written, a day of it at the most); further back, to the second, or withexact: true, the moment itself, to the millisecond, by its WAL archive (its WAL replayed to it: seconds to minutes, the call waits). Its endpoint, its connection strings and its owner’s password stay what they were, its connections drop, and its state before is kept as a branch of its own,main_old_<the restore's moment>; one whose new version does not serve within five minutes is undone (503database_restore_failed). A branch of a time is made of that moment instead. A time its history does not have is 400invalid_history, its message saying from when the window and the archive reach. A time to the second (orexact) is the archive’s or refused, never the window’s version before it: 400invalid_historywhere the archive does not reach it (a database that keeps none, or a time before its oldest base), 503database_archive_unavailable(retryable) while the archive’s store refuses its segments; each refusal names the window’s version at or before the time, whose own moment can be asked for instead. One whose blocks the store does not give is refused 503drive_unreadable(retryable), nothing made. An outage of the object store long enough that more of its WAL waited than its Drive keeps for the log gives that WAL up, so that the database stays up: a gap of its archive, indb.history.gaps(DatabaseHistoryGap:from,to,from_lsn,to_lsn): when the WAL given up began, until when the archive restores no time (the end of the first base taken after it; null until there is one), and where in the log it begins and ends. A time in a gap is restored only from its Drive’s window (a whole minute, or a version’s own moment); to the second, or exact, it is 400invalid_history. The gap is said as events too:database.history_gapwhen it is first found (once a gap, while the database is awake), anddatabase.history_resumedwhen the archive restores again after it; theirdatais anEventDatabaseGap: the database, the gap’sfrom,to(null of the first),from_lsnandto_lsn,segments(how many segments of 16 MB were given up) and, of the first,error(what the object store’s last refusal said).create(name, { tiering: { enabled: true } })andupdate(db, { tiering: { enabled } })turn its tiered storage on or off (slice DB18: on Paid; each branch’s PostgreSQL restarts once for it), and a database’sinfo.tieringsays whether it is on and what it bills by now (tiered_bytes).get(idOrName),list({ name }),delete(db, { finalSnapshot })(a snapshot of its Drive is kept seven days,db.finalSnapshot, unlessfinalSnapshot: false),branches(db)(mainfirst, then its own, the oldest first),branch(db, "main"),sleep(db, "main"),wake(db, "main")(its memory saved, its sessions there when it wakes; a connection wakes it too),resetPassword(db, { role })(a new password in this answer alone; the old one is refused from the next login on). ADatabasehas the same of its own.glac.databases.roles(db)(anddb.roles()) lists the roles of its root branch that log in, read from PostgreSQL itself ({ name, owner }): its owner and the roles you made (CREATE ROLE … LOGIN), the platform’s not among them.resetRolePassword(db, role)(anddb.resetRolePassword(role)) gives any of them a new password, in this answer alone: aDatabaseRoleSecretwith the role’spasswordand the root branch’s connection strings of that role (connectionString()); the owner’s is made of the database the API answers. A branch of its own keeps the password it was made with.db.connectionString({ password, pooled, private })is a string node-postgres (connectionString), psql and libpq take as it is: the public one where the API gives one, else the one on the project’s network, with the owner’s password of the answer it came with, orpassword(percent-encoded). A public one’ssslmode=requireis said as node-postgres takes it today,sslmode=verify-full.db.defaultBranchhas the connection strings as the API gives them (connectionUri,pooledUri,privateUri), itsendpointand itsstate(awake,paused,asleep).
A database is reached on the project’s network, from its sandboxes (made with project_network) and its services, at its name: that is its private_uri. Where an edge serves PostgreSQL it is reached from anywhere at its public address too (connection_uri, port 5432, TLS alone; the pooler’s: pooled_uri); they are null where none does, or where the database has no region to be named under, and the connection string takes them where they are given. A database’s JSON leaves the password out. Databases are a control plane’s.
Managed Applications
An application of the catalogue (Rivet, say) is installed in the project as its own Cube resources: its apps, Volumes, secrets and buckets, labelled with the installation, changed through it alone, and billed as they are (Managed Applications). An install’s answer has every output, the secret ones too (an admin token, an endpoint with it in), and a read shows those to a key with the admin scope alone: an installation object keeps each output an earlier answer gave that a later one hides.
const rivet = await glac.catalogue.get("rivet"); // its parameters, and a month of it in Cube's meters
console.log(rivet.calculator?.month);
const engine = await glac.installations.create("rivet", { parameters: { disk_gib: 20 } }); // waits until it is ready
console.log(engine.outputs.endpoint); // RIVET_ENDPOINT, its token in it| Call | What it does |
|---|---|
glac.catalogue.list(), glac.catalogue.get(app, { version }) |
The catalogue’s applications, each a CatalogueApp (app, version, title, summary, verified, licence, versions, parameters: a JSON Schema, outputs, calculator with its month); one, at its newest version or the one named, with its definition. |
glac.installations.create(app, { name, version, region, parameters, requires, wait, timeoutSeconds }) |
Installs it, and waits until it is ready (wait: false: at once). One that failed throws InstallationError, with its words and the installation; one still being made after timeoutSeconds, a GlacError of the code wait_timeout. |
glac.installations.list({ app }), glac.installations.get(idOrName) |
The project’s installations, the newest first; one. |
installation.update({ version, parameters }, { wait }), installation.delete(), refresh(), wait({ timeoutSeconds }) |
Another version, or its parameters as a merge patch (null takes one back to its default); deleted with everything it is made of (status: deleted, or deleting). |
installation.status, failure, outputs, secretOutputs, parameters, apps, volumes, buckets, requires, requiredBy |
As the API says them. |
Managed Applications are a control plane’s.
Secrets and variables
glac.secrets.put(name, value, { allowedHosts }) keeps a value that no answer gives back, and that goes only to the hosts it was given; an app is given one as an environment variable (secrets: { DATABASE_URL: "db_url" }). glac.variables.put(name, value) is a shared variable that an app’s env refers to as ${{ shared.NAME }}.
The project, the account and the bill
glac.project() and glac.events(); glac.account() and glac.usage(), and updateAccount(), which sets where the account’s notices go ({ email }) and the language they are written in among the rest ({ language: "de" }: “en”, “de” or “fr”, or null to follow the billing address again); an account’s email_verified says whether its address is verified, and email_undeliverable (an EmailUndeliverable: why, since, detail) that mail to it bounced, which hold its notices back (where notices go); its keeps (an AccountKeeps, behind a control plane) is what the key’s project keeps of Drives and volumes, by the numbers a create is held to, each with what it has (volumes, volume_bytes, volumes_bytes, whose limit is null for no limit, and unsized_bytes: see raising limits); glac.billing(), glac.balance(), glac.topUp(usd), glac.charges() and glac.ledger(), and updateBilling() and updateBalance() to change the plan and auto reload. The bill and the project are the control plane’s.
The data of an event. Every type of event the control plane has has an interface of its data (event.data), named after the description’s schema of it (event.data as EventDatabase). A database’s: EventDatabase (database.created, with its region and PostgreSQL version; database.deleted, with final_snapshot_id, the snapshot of its Drive kept seven days), EventDatabaseBranch (database.branch_created, with its parent; database.branch_deleted, with its reason), EventDatabaseRestored (database.restored: the moment at, and old_branch_id, the branch the state before is kept as), EventDatabaseArchive (database.archive_failing: since, how many segments in a row failures, archived_until, how far a restore reaches meanwhile, and the object store’s error; database.archive_recovered: recovered_at) and EventDatabaseGap. The others likewise: a sandbox’s (EventSandboxSlept, EventSandboxWoke, EventSandboxMoved, EventSandboxDeleted, …), an app’s and its keys’ (EventApp, EventAppFailed, EventKeyCreated, …), the project’s deploys and key, backups, domains, destinations and Drives’ snapshots (EventProjectDeployed, EventBackup, EventDomain, EventDestination, EventDriveSnapshot, …), and the account’s plan, top-ups, refunds, withdrawals, balance, Credits, storage, egress cap, spend limits, hold and debts (EventSubscriptionPeriod, EventTopupCollected, EventWithdrawalReceived, EventBalance, EventInDebt, …). A field that not every event of a type has is optional.
Webhooks, API keys, spend limits, the project. glac.webhooks.create(url, { events, enabled }) sends the project’s events to an address of yours (the secret that signs each delivery is in that answer alone); get, list, update, delete. glac.keys.create({ name, scopes, expiresAt }) makes an API key (the key itself, key, is in that answer alone); get, list, delete. glac.limits() and glac.updateLimits(usd) are the project’s spend limit and what was spent this month (null takes the limit away), and sbx.limits() and sbx.updateLimits(usd) a sandbox’s own. glac.requestLimit(limit, wanted, why) asks for one of the account’s limits raised on request (a cap by its name, "volumes" or "volume_bytes"; bytes of the two in bytes; 409 limit_request_pending while one of the same limit waits), and glac.limitRequests({ limit, cursor }) and glac.limitRequest(id) read them with the operator’s answers (LimitRequest: status pending, granted or refused, granted, answer). The data of instance.restarted, instance.upgraded, instance.reverted, instance.upgrade_failed and limit_request.granted and .refused events are EventInstanceRestarted, EventInstanceUpgraded, EventInstanceReverted, EventInstanceUpgradeFailed and EventLimitRequest. glac.updateProject({ deleteAfterIdleDays: 30 }) sets after how many days asleep its sandboxes are deleted (null: never). A control plane’s. Webhooks and API keys need a key with the admin scope, and so do setting the project’s limit and updateProject; setting a sandbox’s limit needs write.
Log destinations, the audit log, the billing documents. glac.destinations is where the logs (and the metrics and the requests’ traces: signals: ["logs", "traces"]) of the project’s apps are exported (OpenTelemetry endpoints): create(name, { endpoint, headers, ... }), which tries it at once with one record (status.connected), get, list, update, delete, test (one record now); a header’s value is a credential, sent when a destination is made or changed and in no answer (what is returned has the headers’ names alone). glac.audit({ limit, cursor }) is a page of the project’s audit log, the newest first (who, what method, what path, what status): every call that was no reading, a refused one too, and every reading of an app’s log by an operator. glac.statement("2026-09") is a month’s statement (this month’s unless given; a Date in the month too) with each database’s own line in it (databases: what it used in the month, its history and its tiered data apart, and what that comes to at the list prices), glac.invoices() the invoices at Stripe (with the PDF’s link), glac.notices() the mail the account was sent, glac.withdrawals() what it may withdraw from now and the withdrawals it made (a consumer in the EEA or the UK may withdraw from a purchase within 14 days), and glac.withdraw(chargeId, { name, email }) the buyer’s own statement of withdrawal, received at once. A control plane’s. Making, changing, testing and deleting a destination, reading the audit log and withdrawing need a key with the admin scope; the rest, one that may read.
Buying at once (startNow). A consumer in the European Economic Area or the United Kingdom (an account with no tax id that is verified or being verified) may withdraw from a purchase within 14 days, and what it buys is provided at once only at its own express request. topUp(20, { startNow: true }), updateBilling({ plan: "paid", startNow: true }) (Paid, bought from Free) and updateBalance({ autoReload: true, startNow: true }) (for every auto reload while auto reload stays on) carry that request, in the words the console shows beside its box (and GET /v1/billing/withdrawals has, right.start_now):
I ask for this purchase to be provided at once, within the 14 days in which I may withdraw from it. If I withdraw, I pay for what was provided until then. I understand that once all the Credits of a top-up are used, it has been provided in full and I can no longer withdraw from it.
The service starts now; within the 14 days the buyer may still withdraw, and pays for what was provided until then; once it is fully provided (a top-up whose Credits are all used), the right to withdraw is lost. Give it only when the person buying asks for that: the SDK never sends it unless it is given. Without it, such an account is refused: a GlacError of 400 start_now_required, param start_now, in words that name its country and quote the request, and nothing is bought. An account whose auto reload is on without the request reloads nothing, and is told once a month (the event auto_reload.no_request, whose data is EventAutoReloadNoRequest).
Errors
A non-zero exit code is a result: read res.exitCode. An error of the API throws GlacError, with status, type, code, param, retryable, requestId and message. DeployError (a version that did not come up, or a project’s deploy that failed) and LogsError (a log that could no longer be read) are GlacErrors too.
Behaviour worth knowing
- Sleeping sandboxes. No call fails because a sandbox sleeps: it is woken and the call goes on. There is no wake call.
- Retries. No answer, or a
retryableerror, is retried twice (retries), from 0.2 s doubling. EveryPOSTcarries anIdempotency-Key. Calls with a byte body are not retried. - What it does not have yet that the Python SDK has: a time limit on
cmd.wait()and a client timeout, raw output bytes, and exact decimal usage sums. It has been run on Node only.