Settings schema

JSON Schema describing per-installation connection settings — including encrypted secret fields.

Connectors use a JSON Schema document to describe the form an admin fills in to install a connection. The schema lives at connection.settingsSchema in the manifest. Plain extensions don’t have this — they use the simpler config defaults map (see Settings).

Real example

A real connector’s schema:

settingsSchema: {
  type: "object",
  required: ["api_key"],
  properties: {
    api_key: {
      type: "string",
      minLength: 1,
      title: "Secret API key",
      description:
        "Stripe restricted or secret API key used for poll and reconciliation routes.",
      "x-secret": true,
      "x-placeholder": "sk_live_... or sk_test_...",
    },
    webhook_secret: {
      type: "string",
      minLength: 1,
      title: "Webhook signing secret",
      description:
        "Stripe endpoint signing secret used by the verified webhook route.",
      "x-secret": true,
      "x-placeholder": "whsec_...",
    },
    account_id: {
      type: "string",
      title: "Connected account ID",
      description: "Optional Stripe Connect account ID...",
      "x-placeholder": "acct_...",
    },
    extract_tax_details: {
      type: "boolean",
      title: "Extract tax details",
      default: true,
    },
    settlement_currency: {
      type: "string",
      title: "Expected settlement currency",
      enum: ["auto", "USD", "EUR", "GBP", "CAD", "AUD", "JPY"],
      default: "auto",
    },
    account_ids: {
      type: "string",
      title: "Bank credit accounts",
      description: "Leave all unselected to import every bank credit account.",
      "x-options-route": "/setup/options/credit_accounts",
      "x-multi": true,
    },
  },
},

Standard JSON Schema bits

  • type: "object" and a properties map — required.
  • required: [...] — fields that must be present.
  • Per-property: type (string | number | boolean | object | array), title, description, default.
  • Standard validators: minLength, maximum, enum, pattern, etc.

Backfill-specific extensions

KeyEffect
x-secret: trueThe dashboard renders the field as a password input and stores the value in the encrypted secret store. The schema field name becomes the secret name (so api_key ends up readable as Secrets.get("api_key")).
x-placeholder: "..."Placeholder text in the dashboard input.
x-options-route: "/setup/options/..."The dashboard calls a connector setup route after the connection is saved and uses the returned options to render the field.
x-multi: trueFor discovered option fields, render a checkbox list and persist selected values as an array in connection settings.

x-secret is the bridge between the connector schema and the Secrets system. You don’t need to also list these names under permissions.secrets — by being declared in the connector’s settingsSchema, they’re owned by the connector.

Discovery-backed fields

Use x-options-route when the admin should choose from provider objects instead of pasting IDs by hand. The route is a normal connector API route under src/api/, uses the connection’s saved auth, and returns a bounded list of host-renderable options:

// backfill.config.ts
settingsSchema: {
  type: "object",
  required: ["api_token"],
  properties: {
    api_token: {
      type: "string",
      title: "API token",
      "x-secret": true,
    },
    account_ids: {
      type: "string",
      title: "Bank credit accounts",
      description: "Leave all unselected to import every Bank credit account.",
      "x-options-route": "/setup/options/credit_accounts",
      "x-multi": true,
    },
  },
}
// src/api/setup/options/credit_accounts.ts
import { api, Http, Secrets } from "@backfill-io/sdk";

export const config = { auth: "api_key" };

export const POST = api(async () => {
  const token = Secrets.get("api_token");
  const response = Http.get("https://api.example.com/credit", {
    auth: { bearer: token },
  });

  if (!response.ok) {
    return api.providerError("Credit account lookup failed", response);
  }

  return api.json({
    ok: true,
    options: response.body.accounts.map((account: any) => ({
      value: account.id,
      label: account.nickname || account.name || account.id,
      description: account.status,
    })),
  });
});

The response shape is:

{
  ok: true,
  options: [
    { value: "cred_123", label: "Credit Card Account • 1234", description: "Active" },
  ],
}

The setup screen is progressive. On the first save, Backfill stores the secret and activates the connection using the current settings. After that, discovered fields can load options through the saved credentials. This is not a separate auth/config lifecycle yet, so scheduled streams may start after the first save before an admin narrows discovered selections.

Reading the values

Non-secret fields come back from Settings.getAll() exactly like a plain extension’s config:

const acctId = Settings.get("account_id");
const extractTax = Settings.get("extract_tax_details");

Secret fields are read with Secrets.get(...):

const apiKey = Secrets.get("api_key");
const webhookSecret = Secrets.get("webhook_secret");

What happens at install

The dashboard renders the form from the schema, validates the admin’s inputs against the schema, splits secret-marked values into the secret store, and persists the rest as the connection’s settings. From that point on, your runtime calls to Settings.get and Secrets.get see the values.