Quickstart

Scaffold an extension that auto-creates a Customer when an Invoice references one that doesn't exist. End-to-end in five minutes.

We’ll build a real before-save hook that touches three entities — read a Customer, create one if missing, attach the resulting id to the Invoice. By the end you’ll have used the SDK’s Entity global for query, create, and update.

1. Scaffold

backfill init customer-autocreate
cd customer-autocreate

You get:

customer-autocreate/
├── backfill.config.ts
├── package.json
├── tsconfig.json
├── .backfillrc      # gitignored
└── src/

2. Manifest

// backfill.config.ts
import { defineExtension } from "@backfill-io/sdk";

export default defineExtension({
  key: "customer-autocreate",
  name: "Customer Autocreate",
  version: "0.1.0",
  permissions: {
    entities: {
      Invoice:  ["read", "write"],
      Customer: ["read", "write"],
    },
  },
});

3. The hook

src/hooks/invoice/before-save.ts. The folder name maps to the entity (invoiceInvoice); the filename maps to the event (before-savebefore_save). Default export, async function:

// src/hooks/invoice/before-save.ts
import { Customer, Log } from "@backfill-io/sdk";
import type { HookContext } from "@backfill-io/sdk";

export default async function (ctx: HookContext) {
  const { customer_id, customer_name, billing_address } = ctx.entity;

  // Already linked to a real customer? Nothing to do.
  if (customer_id) return null;

  // Need at least a name to create one.
  if (!customer_name) return null;

  // Look for an existing customer by name.
  const matches = Customer.query({
    where: { name: customer_name },
    limit: 1,
  });

  if (matches.length > 0) {
    Log.info("Linked invoice to existing customer", {
      customerId: matches[0].id,
      customerName: customer_name,
    });
    return { customer_id: matches[0].id };
  }

  // Otherwise, create one.
  const created = Customer.create({
    name: customer_name,
    billing_address,
  });

  Log.info("Created customer for invoice", {
    customerId: created.id,
    customerName: customer_name,
  });

  return { customer_id: created.id };
}

What’s happening:

  • Customer is the typed entity class for the Customer entity — imported from @backfill-io/sdk, with the same shape as Entity but pre-bound so there’s no first argument. Invoice, Payment, Account, etc. all work the same way.
  • query returns an array. create returns the new record. Both are synchronous from your code’s perspective.
  • The handler returns the changes to merge into the invoice — here, just customer_id. Return null to leave the invoice alone.
  • Log.info writes to backfill logs --follow and the dashboard’s audit trail.

4. Type-check

npx tsc --noEmit

Catches typos in field names, wrong types, missing imports — everything before the platform sees your code.

5. Validate without deploying

backfill build

Runs the same discovery + validation the server runs. Common errors:

src/hooks/invoice/before-save.ts: unknown hook event 'beforesave'
src/hooks/invoiced/before-save.ts: unknown entity type 'Invoiced'

6. Develop with auto-redeploy

backfill dev

The CLI watches your files and pushes a draft on every save. Open backfill logs --follow in another terminal and create an invoice in the dashboard — your Log.info lines appear immediately.

7. Ship it

backfill deploy

Or, to dry-run against live traffic without committing writes:

backfill deploy --observe
backfill logs --follow
# happy?
backfill promote

What’s next

  • File structure — the rest of the layout: jobs, routes, custom entities, pages.
  • Hooks — every event, every return contract, the recalculate hook.
  • Entities — the full Entity API, including update, delete, exists, count, and action.