# 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

```sh
npm install scribase
# or
pnpm add scribase
```

The same package also ships the `scribase` CLI (`npx scribase init`) — see
[Quickstart](https://docs.scribase.com/docs/getting-started.md).

## Credentials

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](https://docs.scribase.com/docs/api/tokens.md). |

Keep the token in an environment variable or secret manager, never in source.

## Quick start

```ts
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`:

```ts
const ref = { organizationId: 'acme', projectId: 'store', environmentId: 'production' };
```

Project-scoped calls (branches, project exports) take a `ProjectRef`:
`{ organizationId, projectId }`.

## Client options

```ts
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

```ts
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

```ts
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):

```ts
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

```ts
// 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:

```ts
import { restoreEvidence } from 'scribase';

const evidence = restoreEvidence(restore);
```

## Export

Build a runnable, signed bundle of an environment:

```ts
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

```ts
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

```ts
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

```ts
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

```ts
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

```ts
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](https://docs.scribase.com/docs/concepts/functions.md) for writing and running functions.

## Operations

```ts
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

```ts
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

```ts
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

```ts
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`:

```ts
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:

```ts
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 suspend
```

Keys are 8–128 printable ASCII characters. A replayed request returns the
original response, and the result's `idempotencyReplayed` flag is `true`.
