Overview

When to reach for defineConnector and what it gives you over a plain extension.

Connectors vs extensions:

  • A connector (defineConnector) brings data in from an external provider — Stripe, Shopify, a bank, an OCR vendor. The platform handles streams, signed webhook ingestion, and canonical record emission.
  • An extension (defineExtension) augments Backfill’s behavior — computing tax, validating fields, adding dashboard pages, posting to Slack on status changes.

A connector is just an extension with extra connector-shaped manifest entries. Both share file layout, the standard library, and the deploy lifecycle.

What changes

import { defineConnector } from "@backfill-io/sdk";

export default defineConnector({
  key: "stripe",
  name: "Stripe",
  provider: "stripe",
  kind: "source",
  version: "0.2.0",
  permissions: { /* same shape as defineExtension */ },
  connection: {
    testConnection: { route: "/setup/test_connection", method: "POST" },
    setup: { webhookUrlTemplate: "...", instructions: "..." },
    settingsSchema: { /* JSON Schema */ },
  },
  webhooks: { endpoint: "/webhooks/stripe", auth: "stripe_signature" },
  streams: [
    { key: "events",    mode: "webhook" },
    { key: "customers", mode: "poll", schedule: "*/15 * * * *" },
    { key: "invoices",  mode: "poll", schedule: "*/15 * * * *" },
    /* ... */
  ],
});

What you keep from defineExtension

  • File/folder structure under src/ — hooks, jobs, api routes, entities all live in the same places.
  • The standard library globals: Entity, Log, Http, Settings, Secrets, api, and generated entity classes.
  • Permissions, custom entities, UI pages, UI contributions.

What’s added

  • connection — defines how a workspace admin sets the connector up (test connection route, setup instructions, settings schema).
  • webhooks — declares a single signed webhook ingress endpoint with provider-specific signature verification.
  • streams — the catalog of data streams this connector ingests (poll or webhook mode, with cron schedules for poll streams).
  • provider, kind — identity. kind: "source" means data flows in; the platform plans for "sink" (outbound) and bidirectional kinds in future.

What’s added at runtime

Connector sync routes use a runtime global, ingest, that plain extensions don’t get. It writes a normalized payload as a canonical entity, with idempotency keying and provenance:

import { ingest } from "@backfill-io/sdk";

ingest.canonical("customer", customerCanonical(stripeCustomer), {
  stream: "customers",
  idempotencyKey: `stripe:customer:${stripeCustomer.id}`,
});

Detail in Sync routes.

When defineExtension is enough

If your integration only needs hooks and an admin settings UI, you don’t need any of this. Plain extensions can call Http, write to entities, and run on schedules — they just don’t get the connector-specific scaffolding for ingest, signed webhooks, or stream-level retries / checkpoints.