Search⌘KHide sidebarOpen menu
Switch to dark mode
Copy page content as Markdown⌘⌥C
Login
Build integrations

SDK reference

The public API of @beetlio/connect 0.6: exports, the integration definition, the sync context, auth helpers and CLI commands.

@beetlio/connect is the TypeScript SDK you write Connect packages with. The SDK is open source under the Apache-2.0 license: the full source and type definitions are on GitHub, and its user guide is at beetlio.github.io/connect. This page covers version 0.6, which needs Node.js 24.2 or newer.

npm install --save-dev @beetlio/connect

Entry points

ImportUse
@beetlio/connectEverything you need to write a package: defineIntegration, z, auth, the helpers and the types.
@beetlio/connect/builderpackIntegration, buildIntegration and createIntegrationManifest, for your own build tooling.
@beetlio/connect/hostFor programs that execute integrations. You do not need it to write a package for Beetl.

Exports

ExportKindPurpose
defineIntegration(definition)FunctionDefines the integration. Make it the default export of integration.ts.
zNamespaceZod 4, re-exported. Use it for every schema so versions match.
authObjectAuthentication helpers. See Auth helpers.
secret(schema)FunctionMarks a string credential as secret. Its value is masked in prompts and forms.
batchRecords(source)Async generatorGroups a record-only stream into batches. Use with yield*.
storageRecord(schema)FunctionTurns a provider object schema into a record schema with fixed columns.
HttpErrorClassThrown by ctx.json for HTTP failures. Has status, detail and requestId.

Types such as SyncContext, Batch and RetryPolicy are exported too.

The SDK can also define destinations, which write records to an API. Beetl runs a package's syncs, and a package installed in Beetl must declare at least one sync.

The integration definition

integration.ts
import { auth, defineIntegration, secret, z } from "@beetlio/connect";

const Order = z.object({ id: z.string(), total: z.number(), status: z.string() });
const Page = z.object({ orders: z.array(Order), next: z.string().nullable() });

export default defineIntegration({
  key: "acme",
  displayName: "Acme",
  connection: {
    origin: { type: "input", input: "baseUrl" },
    inputs: z.strictObject({ baseUrl: z.url().meta({ title: "API URL" }) }),
    auth: auth.custom({
      credentials: z.strictObject({ apiKey: secret(z.string().min(1)) }),
      headers: { "x-api-key": { credential: "apiKey" } },
    }),
    async verify(ctx) {
      await ctx.json("/me", z.object({ id: z.string() }));
    },
  },
  syncs: (sync) => ({
    orders: sync({
      records: Order,
      inputs: z.strictObject({ status: z.enum(["open", "closed"]).default("open") }),
      async *run(ctx) {
        for await (const page of ctx.paginate({
          request: {
            path: "/orders",
            query: { status: ctx.config.sync.status },
          },
          schema: Page,
          next: ({ data, request }) =>
            data.next === null
              ? undefined
              : { ...request, query: { ...request.query, cursor: data.next } },
        })) {
          yield { records: page.data.orders };
        }
      },
    }),
  }),
});

This example reads every order with the chosen status on each run. Its pagination cursor is used only within that run, so it suits the Replace write mode, which the person who adds the sync chooses. For incremental reads with a checkpoint that carries across runs, see Authoring a sync.

defineIntegration

FieldTypeNotes
keystringIdentifies the integration.
displayNamestringShown in the app.
descriptionstring, optionalShown in the app.
icon"icon.png" or "icon.webp", optionalA file in the package. List it in package.json files.
connectionConnectionDefinitionSee below.
syncs(sync) => Record<string, SyncDefinition>Build each sync with the sync function passed in, so types are inferred. Keys are sync keys.

connection

FieldTypeNotes
originProviderOriginDefinitionA fixed URL, { type: "input", input } (a connection setting), { type: "environment", input, values } (a setting mapped to fixed URLs) or { type: "oauth", oauthTokenField } (an OAuth token-response field). Authenticated origins require HTTPS.
authAuthDefinition, optionalBuilt with an auth helper. Omit it for public APIs.
inputsz.ZodObject, optionalConnection settings, shown as a form. Use z.strictObject.
retryRetryPolicy or false, optionalmaxAttempts, statuses, methods, initialDelayMs, maxDelayMs. The default retries status 408, 429, 500, 502, 503 and 504 on GET, HEAD and OPTIONS, up to 3 attempts. false disables retries.
verify(ctx)(ctx: ConnectionContext) => Promise<void>, optionalMake a small authenticated request and throw if access is invalid. ctx.config holds the connection settings.

Input schemas become forms, so they cannot use transforms or custom refinements. Set labels and help text with .meta({ title, description }).

sync(definition)

FieldTypeNotes
recordsz.ZodObjectThe schema of each output row. Its output must be representable as JSON Schema.
inputsz.ZodObject, optionalSync settings, shown when a user adds the sync.
checkpointZod schema, optionalThe shape of the position you resume from. Required before you yield a checkpoint. A sync that declares one resumes from it when its write mode is Append.
displayNamestring, optionalShown in the app.
run(ctx)Async generatorYields batches. Returning ends the run successfully.

Batches

Each value run yields is one batch. Beetl accepts a batch's records and checkpoint together before the generator continues; the dataset changes only when the run succeeds.

A sync does not choose a write mode, declare a primary key or report deletions. The person who adds the sync chooses Append or Replace. See Write modes.

FieldTypeNotes
recordsArrayRows, each parsed with the records schema.
checkpointCheckpoint type, optionalThe position to resume after this batch. Omitting it keeps the previous one; null is a value.

A batch holds at most 10,000 records. For the per-run limits, see Write modes.

Sync context

run(ctx) receives a SyncContext.

MemberTypeNotes
ctx.config.connectionObjectParsed connection settings.
ctx.config.syncObjectParsed sync settings, with defaults applied.
ctx.checkpointCheckpoint type or undefinedThe last committed checkpoint. undefined on the first run, after the sync's settings change, and in every Replace run.
ctx.signalAbortSignalPass it to async work that does not go through ctx.
ctx.logIntegrationLoggerdebug, info, warn and error, each (message, fields?) => Promise<void>. Lines appear on the connection's Logs tab. Never log credentials.
ctx.fetch(path, init?)Promise<Response>Sends an authenticated request to a path relative to the origin, or a same-origin URL. init takes method, headers, body and signal. Redirects are not followed.
ctx.json(path, schema, init?)Promise<output>Like fetch, then parses the JSON body with schema. Throws HttpError for an unsuccessful status.
ctx.paginate(options)Async generator of PaginationPageSee below.

ctx.paginate takes a request (path, optional query and headers), a schema for each page, and a next function. next receives the page and returns the next request, or undefined to stop. Each page has data, response (status and headers), request and index. A pagination stops with an error after 10,000 pages or on a repeated request.

Await every ctx call. The SDK adds credentials to each request, so sync code never handles secrets.

Auth helpers

HelperCredential fields the user entersUse
auth.bearer()tokenBearer token in the Authorization header.
auth.basic()username, passwordHTTP basic authentication.
auth.apiKey({ in, name })apiKeyAn API key in a header or query parameter called name.
auth.oauth2({ issuer, authorizationUrl, tokenUrl, scopes, clientSecret?, tokenFields? })clientId, plus clientSecret when clientSecret: trueOAuth 2.0 authorization code with PKCE and token refresh. tokenFields maps token-response fields for use as an origin.
auth.tokenExchange({ credentials, request, response, session? })Your credentials schemaExchanges credentials for a session token at request.path, reads it from response.tokenPath, and renews it by response.expiry.
auth.custom({ credentials, headers?, query? })Your credentials schemaPlaces credentials in headers or query parameters with { credential: "field" }.

Custom and token-exchange credentials are strict objects of strings, and they cannot have defaults. Wrap sensitive fields in secret(). See Authentication.

Helpers

batchRecords(source) takes an iterable or async iterable of records and yields record-only batches of a suitable size. Use it as yield* batchRecords(rows) when a sync has no checkpoint; yield batches yourself when each one carries a checkpoint.

storageRecord(schema) wraps a provider object schema so every top-level field gets a fixed column type. Strings, numbers, booleans, arrays and strict nested objects keep their type. A union of scalar types is stored as text, and other dynamic values, such as records with arbitrary keys, are stored as JSON text. The schema must be a plain object without a catchall or root refinements.

import { auth, batchRecords, defineIntegration, storageRecord, z } from "@beetlio/connect";

const Customer = z.object({
  id: z.number(),
  name: z.string(),
  attributes: z.record(z.string(), z.unknown()),
});

export default defineIntegration({
  key: "acme-customers",
  displayName: "Acme customers",
  connection: { origin: "https://api.example.com", auth: auth.bearer() },
  syncs: (sync) => ({
    customers: sync({
      records: storageRecord(Customer),
      async *run(ctx) {
        yield* batchRecords(await ctx.json("/customers", z.array(Customer)));
      },
    }),
  }),
});

CLI

The SDK installs the beetl-connect command. Run it with npx beetl-connect, and add --help to any command for every option.

CommandOptionsWhat it does
configure <dir> [sync]--profile, --connection, --origin, --inputs <json>, --reauthorizePrompts for settings and credentials, authorizes OAuth, runs verify, and saves a local profile.
sync <dir> [sync]--profile, --output <path>, --state <path>, --mode append|replaceRuns a sync and writes its records as NDJSON. Checkpoints are saved under .beetl/state/ unless you pass --state. --mode replace starts from the beginning and writes the file only when the run succeeds.
pack <dir>--output <path>Creates the .tgz you upload to Beetl and prints the files it contains.
compatibilityNoneChecks the installed SDK against its compatibility fixtures.

The sync argument is optional when the integration has one sync. Local credentials are saved unencrypted in files only your user can read, in the operating system's user configuration directory. See Local development and Pack and upload.

Last updated on

On this page