@capydb/sdk
v1.23.2
Published
Official TypeScript SDK for the CapyDB control plane API - projects, preview databases, backups, imports, webhooks, and integrations.
Maintainers
Readme
@capydb/sdk
Official TypeScript SDK for the CapyDB control plane - projects, preview databases, backups & PITR restores, imports, Studio SQL, webhooks, and deployment integrations. Every CapyDB project runs in its own isolated database cell; the SDK drives the lifecycle around it.
Generated from the control plane's OpenAPI document
(GET https://capydb.dev/api/capydb/v1/openapi.json) with
@hey-api/openapi-ts, so every operation and model is
fully typed and always in lockstep with the API.
Install
pnpm add @capydb/sdkUsage
import { createCapyDB } from "@capydb/sdk";
const capydb = createCapyDB({ apiKey: process.env.CAPYDB_API_KEY! });
// List projects
const projects = await capydb.listProjects();
// Create a preview database for a branch and wait on its job
const preview = await capydb.createPreviewDatabase({
path: { projectID: "prj_..." },
body: { name: "pr-42", mode: "clone", ttl_hours: 72 },
});
// Fetch connection strings
const connections = await capydb.getPreviewDatabaseConnections({
path: { previewID: preview.data!.preview.id },
});Use a project-scoped API key (Dashboard → Settings → API keys) for CI and integrations: it can only touch the one project it was minted for.
Ephemeral databases (no account)
createCapyDB() works without an API key for the three calls that need none: creating a throwaway
database, reading it back with its claim token, and destroying it early with the same token. It is
destroyed after 72 hours unless claimed.
const anonymous = createCapyDB({});
const { data: created } = await anonymous.createEphemeralDatabase();
// created.claim_token is shown once and cannot be recovered - keep it.
const { data: details } = await anonymous.getEphemeralDatabase({
path: { projectID: created!.ephemeral_database.project_id },
headers: { "X-CapyDB-Claim-Token": created!.claim_token },
});
// details.connections.pooled_url once details.ephemeral_database.state === "ready"
// Done with it? Destroy it now and free its slot instead of waiting 72 hours.
await anonymous.destroyEphemeralDatabase({
path: { projectID: created!.ephemeral_database.project_id },
headers: { "X-CapyDB-Claim-Token": created!.claim_token },
});
// Or keep it instead: claim it into your organization (this call does need a key).
await capydb.claimEphemeralDatabase({
path: { projectID: created!.ephemeral_database.project_id },
body: { claim_token: created!.claim_token },
});Error handling
Operations do not throw on HTTP errors by default. Every call resolves to a
result object with a data/error union plus the underlying request and
response:
const { data, error, response } = await capydb.getProject({
path: { projectID: "prj_..." },
});
if (error !== undefined) {
// Non-2xx responses: `error` is the parsed JSON error body - the control
// plane always answers `{ error: "human-readable message" }`. If the body
// is not JSON, `error` is the raw response text. Network/fetch failures
// surface here too (e.g. a TypeError), in which case `response` is undefined.
console.error(`request failed (${response?.status}):`, error);
return;
}
// On 2xx, `data` is the fully typed response body and `error` is undefined.
console.log(data.project.state);Prefer exceptions? Pass throwOnError: true on each call. (Setting it through
the module-level client.setConfig() does not reach createCapyDB() instances:
each one has its own client.) The rejection value is the same parsed error body,
and data is then non-optional:
const { data } = await capydb.getProject({
path: { projectID: "prj_..." },
throwOnError: true, // rejects with { error: "project not found" } on 404
});One sharp edge: data is typed from the OpenAPI document, but it is the raw
parsed JSON - defensive callers should tolerate null where the server could
emit a JSON null for an empty list.
Extensions and alerts
List the Postgres extensions available to a project and enable one - enablement runs as an asynchronous job you can poll:
const { data } = await capydb.listProjectExtensions({
path: { projectID: "prj_..." },
});
const postgis = data?.extensions.find((e) => e.name === "postgis");
const enable = await capydb.enableProjectExtension({
path: { projectID: "prj_..." },
body: { name: "postgis" },
});
const job = await capydb.getJob({ path: { jobID: enable.data!.job.id } });
// Disable later (plain DROP EXTENSION, fails if other objects depend on it)
await capydb.disableProjectExtension({
path: { projectID: "prj_...", name: "postgis" },
});Threshold alerts (storage / connection usage against plan limits) can be listed
and acknowledged; alert.triggered / alert.resolved webhook events fire on
the same lifecycle:
const alerts = await capydb.listProjectAlerts({ path: { projectID: "prj_..." } });
for (const alert of alerts.data?.alerts ?? []) {
if (alert.resolved_at === undefined && alert.acknowledged_at === undefined) {
await capydb.acknowledgeProjectAlert({
path: { projectID: alert.project_id, alertID: alert.id },
});
}
}Development
pnpm install
pnpm generate # regenerate src/generated from the backend openapi.json
pnpm typecheck
pnpm build # tsdown → dist/
pnpm test # vitest: mock-fetch unit tests + live contract testspnpm test always runs the mock-fetch unit suite. The contract suite runs
against a disposable control plane (dry-run executor) that the global setup
boots automatically - via docker compose in ../backend when Docker is
available, otherwise natively with the local Go toolchain and a Postgres on
127.0.0.1:5432 - and tears down afterwards. Set CAPYDB_TEST_BASE_URL (and
optionally CAPYDB_TEST_ADMIN_TOKEN) to point at an already-running stack;
when no stack can be booted the contract suite is skipped.
The generated sources live in src/generated and are committed so consumers
can audit exactly what ships.
