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 (invoice → Invoice); the filename maps to the event (before-save → before_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:
Customeris the typed entity class for theCustomerentity — imported from@backfill-io/sdk, with the same shape asEntitybut pre-bound so there’s no first argument.Invoice,Payment,Account, etc. all work the same way.queryreturns an array.createreturns 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. Returnnullto leave the invoice alone. Log.infowrites tobackfill logs --followand 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
EntityAPI, includingupdate,delete,exists,count, andaction.