SDK — scribase-js
scribase is the typed, fetch-based TypeScript client for the Scribase /v1
control-plane API. Every resource is a property on the client, mutations take an
idempotency key (one is minted for you when omitted), and transient failures
(429, 503, network errors) are retried with exponential backoff.
It runs anywhere fetch exists: Node.js 22.13+, Deno, Bun, and edge runtimes.
Installation
Not yet published. The
scribasepackage is not on the npm registry yet, sonpm install scribasedoes not work today. Until it is published, build it from the repository (packages/scribase-js) and install it from a local path or tarball:Terminal cd packages/scribase-js && npm install && npm run build && npm pack npm install /path/to/scribase-<version>.tgz # in your app
Once the package is published, the install is:
npm install scribase
# or
pnpm add scribaseCredentials
The client needs two things:
| Option | Where it comes from |
|---|---|
baseUrl |
https://api.scribase.com for hosted Scribase, or your own gateway in your own cluster |
token |
A personal access token (console Account → Access tokens) or an organization API key (console Organization → API tokens). See Access tokens & API keys. |
Keep the token in an environment variable or secret manager, never in source.
Quick start
import { ScribaseClient } from 'scribase';
const scribase = new ScribaseClient({
baseUrl: process.env.SCRIBASE_API_URL ?? 'https://api.scribase.com',
token: process.env.SCRIBASE_ACCESS_TOKEN ?? '',
});
// Create an environment — returns an accepted operation
const { operation } = await scribase.environments.create({
organization_id: 'acme',
project_id: 'store',
environment_id: 'production',
kind: 'production',
data_mode: 'snapshot',
region: 'us-east',
postgres_major: 17,
});
// Wait until it is terminal (throws OperationFailedError on failure)
await scribase.operations.wait(operation.id, 'acme');Most environment-scoped calls take an EnvironmentRef:
const ref = { organizationId: 'acme', projectId: 'store', environmentId: 'production' };Project-scoped calls (branches, project exports) take a ProjectRef:
{ organizationId, projectId }.
Client options
const scribase = new ScribaseClient({
baseUrl: 'https://api.scribase.com',
// A string, or a (possibly async) provider called before every request
token: async () => await readTokenFromVault(),
timeoutMs: 30_000, // per-attempt timeout (default 30s)
maxRetries: 3, // retries for 429/503/network failures (default 3)
retryBaseDelayMs: 250, // backoff base delay (default 250ms)
retryMaxDelayMs: 20_000, // ceiling for one backoff wait (default 20s)
defaultHeaders: { 'X-Request-Source': 'deploy-bot' },
userAgent: 'my-app/1.0',
});Organizations, members, and projects
await scribase.organizations.put('acme', { display_name: 'Acme Inc.' });
const org = await scribase.organizations.get('acme');
const orgs = await scribase.organizations.list();
await scribase.organizations.putMembership('acme', { subject: 'user:ada', role: 'developer' });
const members = await scribase.organizations.listMemberships('acme');
await scribase.projects.put('acme', 'store', { region: 'us-east', postgres_major: 17 });
const project = await scribase.projects.get('acme', 'store');
const projects = await scribase.projects.list('acme');Roles are owner, admin, developer, and viewer.
Environments
const env = await scribase.environments.get(ref);
// List every environment in an org (optionally one project); returns an array
const environments = await scribase.environments.list({ organizationId: 'acme', projectId: 'store' });
// Lifecycle transitions return accepted operations
const { operation } = await scribase.environments.suspend(ref, { idempotencyKey: 'suspend-prod-001' });
await scribase.operations.wait(operation.id, 'acme');
await scribase.environments.resume(ref);
await scribase.environments.delete(ref);
// Server-side only: the Postgres connection URI
const { connection_uri } = await scribase.environments.connection(ref);
// Recent service logs, newest first
const logs = await scribase.environments.logs(ref, { limit: 100, level: 'error' });kind is production, staging, or preview; data_mode is snapshot,
sanitized, or schema_only.
Previews
Previews are TTL-swept environments of kind preview. The resource defaults the
data mode to sanitized and always sets a TTL (24 hours unless you pass one):
const { operation } = await scribase.previews.create({
organizationId: 'acme',
projectId: 'store',
environmentId: 'pr-1487',
region: 'us-east',
postgres_major: 17,
ttlHours: 72,
});
await scribase.operations.wait(operation.id, 'acme');
const previews = await scribase.previews.list({ organizationId: 'acme', projectId: 'store' });
const pr = { organizationId: 'acme', projectId: 'store', environmentId: 'pr-1487' };
await scribase.previews.renew(pr, 48); // extend to 48h from now
const preview = await scribase.previews.get(pr);
scribase.previews.remainingSeconds(preview); // seconds until the sweep, or null
await scribase.previews.delete(pr);Backups and verified restore
// Newest first
const backups = await scribase.backups.list(ref, { limit: 20 });
// Record a new backup; its id is the idempotency key (minted when omitted)
const backup = await scribase.backups.create(ref, { backupId: 'nightly-2026-09-23' });
const one = await scribase.backups.get(ref, 'nightly-2026-09-23');
console.log(one.status, one.sha256, one.verified_at_epoch_seconds);
// Restore a backup into a target environment in the same project
const { restore } = await scribase.restores.start(ref, {
backupId: 'nightly-2026-09-23',
targetEnvironmentId: 'restore-drill',
});
console.log(restore.status); // 'pending' | 'restored' | 'failed'Once a restore reaches restored it carries its evidence — the number of
relations and RLS-protected relations restored, and the SHA-256 of the evidence
record. restoreEvidence(restore) returns it as a typed object, or null if the
restore has not completed:
import { restoreEvidence } from 'scribase';
const evidence = restoreEvidence(restore);Export
Build a runnable, signed bundle of an environment:
const { manifest, bundleRef } = await scribase.export.create(ref, { format: 'compose' });
console.log(manifest.database.sha256, manifest.artifacts.length, bundleRef);
// Project-scoped form: name the source environment in the request
await scribase.export.createForProject(
{ organizationId: 'acme', projectId: 'store' },
{ sourceEnvironmentId: 'production', format: 'kustomize' },
);
const exports = await scribase.export.list({ organizationId: 'acme', projectId: 'store' });
const saved = await scribase.export.getManifest('acme', manifest.bundle_id);Migrations
const migrations = [
{ version: '1', name: 'init', up: 'create table orders (id bigint primary key);' },
{ version: '2', name: 'add_total', up: 'alter table orders add column total numeric;', down: 'alter table orders drop column total;' },
];
// Plan and lint only — nothing executes
const planned = await scribase.migrations.plan(ref, migrations);
if (planned.lintFindings.some((finding) => finding.severity === 'BLOCK')) {
throw new Error('migration blocked by lint');
}
// Apply
const result = await scribase.migrations.run(ref, { migrations, apply: true });
console.log(result.applied);Branches
const project = { organizationId: 'acme', projectId: 'store' };
await scribase.branches.register(project, {
environment_id: 'pr-1487',
base_environment_id: 'production',
});
const branches = await scribase.branches.list(project);
const diff = await scribase.branches.diff(project, { base: 'production', head: 'pr-1487' });
const merge = await scribase.branches.merge(project, { base: 'production', head: 'pr-1487', force: false });
await scribase.branches.linkPullRequest(project, 'pr-1487', {
provider: 'github',
repository: 'acme/storefront',
number: 1487,
});Queues
await scribase.queues.create(ref, 'events');
const { msg_id } = await scribase.queues.send(ref, 'events', { type: 'order.created' }, { delaySeconds: 5 });
// read() returns an array of messages and hides them for the visibility timeout
const messages = await scribase.queues.read(ref, 'events', { quantity: 10, visibilityTimeoutSeconds: 30 });
for (const message of messages) {
await scribase.queues.archive(ref, 'events', message.msg_id);
}
const queues = await scribase.queues.list(ref); // with current depth
const waiting = await scribase.queues.peek(ref, 'events'); // look without consuming
await scribase.queues.purge(ref, 'events');Imports from Supabase
const request = {
sourceProjectRef: 'abcd1234',
sourceConnectionString: process.env.SOURCE_PG_URL ?? '',
sourceServiceKey: process.env.SOURCE_SERVICE_KEY ?? '',
destination: { organizationId: 'acme', projectId: 'store', environmentId: 'production' },
};
// Plan first — nothing is written
const plan = await scribase.imports.dryRun(request);
console.log(plan.planned.tables, plan.planned.authUsers, plan.findings);
// Then move it
const report = await scribase.imports.create(request);
console.log(report.moved, report.verification?.checks);
// History and a stored report
const history = await scribase.imports.history('acme');The source credentials are used once and never returned.
Edge functions
import { readFile } from 'node:fs/promises';
const source = await readFile('functions/hello/index.ts', 'utf8');
await scribase.functions.deploy(ref, { name: 'hello', source });
const { functions } = await scribase.functions.list(ref);
const { result } = await scribase.functions.invoke<{ hello: string }>(ref, 'hello', { name: 'world' });Function names are letters, digits, hyphens, or underscores. See Functions for writing and running functions.
Operations
const op = await scribase.operations.get('op_01J...', 'acme');
await scribase.operations.wait('op_01J...', 'acme', {
intervalMs: 2000, // poll interval (default 1000)
timeoutMs: 600_000, // give up after (default 300000)
onUpdate: (snapshot) => console.log(snapshot.state),
throwOnFailure: true, // default true
});Usage, billing, and audit
const usage = await scribase.usage.get(ref); // flat-price envelope and caps
const nearing = await scribase.usage.capsNearingLimit(ref, 0.8);
const billing = await scribase.usage.billing('acme');
const events = await scribase.audit.list('acme', { limit: 50 });
// Or iterate every page
for await (const event of scribase.audit.paginate('acme')) {
console.log(event);
}Account and credentials
const me = await scribase.account.get();
const tokens = await scribase.account.listTokens();
const created = await scribase.account.createToken({ name: 'ci', expires_in_days: 90 });
console.log(created.token); // shown once — store it now
await scribase.account.revokeToken(created.id);
const key = await scribase.organizations.createApiKey('acme', { name: 'deploy-bot', scopes: ['deploy'] });
await scribase.organizations.revokeApiKey('acme', key.api_key_id);Read-only views into a live environment
const users = await scribase.auth.listUsers(ref);
const providers = await scribase.auth.listProviders(ref);
const policies = await scribase.auth.listPolicies(ref);
const tables = await scribase.database.tables(ref);
const buckets = await scribase.storage.listBuckets(ref);
const objects = await scribase.storage.listObjects(ref, 'avatars');
const channels = await scribase.realtime.channels(ref);
const metrics = await scribase.metrics.get(ref);
const insights = await scribase.insights.get(ref);Error handling
Every error extends ScribaseError. HTTP failures are ScribaseApiError
subclasses keyed by status, carrying the problem+json code and detail:
import {
ConflictError,
NetworkError,
NotFoundError,
OperationFailedError,
QuotaExceededError,
TimeoutError,
} from 'scribase';
try {
const { operation } = await scribase.environments.create({ /* ... */ });
await scribase.operations.wait(operation.id, 'acme');
} catch (error) {
if (error instanceof QuotaExceededError) {
console.error('Plan cap reached — add a capacity pack or free a slot');
} else if (error instanceof OperationFailedError) {
console.error('Provisioning failed', error);
} else if (error instanceof NetworkError || error instanceof TimeoutError) {
console.error('Could not reach the API');
} else {
throw error;
}
}Idempotency keys
Mutating calls accept idempotencyKey. When omitted the SDK mints one per call;
pass your own to make a retried job safe across processes:
import { newIdempotencyKey } from 'scribase';
const key = newIdempotencyKey();
await scribase.environments.suspend(ref, { idempotencyKey: key });
await scribase.environments.suspend(ref, { idempotencyKey: key }); // replay, not a second suspendKeys are 8–128 printable ASCII characters. A replayed request returns the
original response, and the result's idempotencyReplayed flag is true.