BrandApp Playground

Migrations

Move record data alongside a schema change with imperative, reviewable migrations. Use Schema Sync for declarative schema reshaping; reach for a migration when you also need to backfill or transform existing records.

defineMigration()

Author a migration with an async up (and optional down). The context exposes schema (add / remove / rename entities and attributes by name) and records (backfill, bulk ops). The expand → backfill → contract pattern keeps a rename loss-free.

migrations/20260601_rename_tier_to_level.ts
// migrations/20260601_rename_tier_to_level.ts
import { defineMigration } from "@reopt-ai/brandapp-sdk/eav/migrate";

export default defineMigration({
  name: "rename_tier_to_level",
  up: async ({ schema, records, log }) => {
    // 1. expand — add the new attribute
    await schema.addAttribute("contacts", "level", {
      dataType: "string",
      label: "Level",
    });

    // 2. backfill — copy data from the old attribute to the new one
    await records.backfill("contacts", {
      transform: (r) => ({ level: r.values.tier ?? "free" }),
      onProgress: ({ processed, total }) => log(`${processed}/${total}`),
    });

    // 3. contract — drop the old attribute
    await schema.removeAttribute("contacts", "tier");
  },
  // Forward-only is recommended; only declare down for trivially reversible work.
});

runMigration()

Execute a migration's up (or down) against a live client. The runner returns a MigrationRunResult ({ name, checksum, durationMs, alreadyApplied }); recording the applied-state audit row is the CLI's job.

scripts/run-migration.ts
import { createReoptEavClient } from "@reopt-ai/brandapp-sdk/eav";
import { runMigration } from "@reopt-ai/brandapp-sdk/eav/migrate";
import migration from "./migrations/20260601_rename_tier_to_level";

const eav = createReoptEavClient({
  clientId: process.env.BRANDAPP_CLIENT_ID!,
  clientSecret: process.env.BRANDAPP_CLIENT_SECRET!,
  brandappId: process.env.BRANDAPP_ID!,
});

const result = await runMigration(eav, migration, {
  direction: "up", // "up" (default) | "down"
  checksum: "sha256-of-the-migration-file",
  onLog: (msg) => console.log(msg),
});

// result: { name, checksum, durationMs, alreadyApplied }
console.log(`${result.name} applied in ${result.durationMs}ms`);

// runMigration does NOT record applied state — the CLI (reopt brandapp eav
// migrate run) records the audit row after this resolves.

runMigrationCollecting()

Buffer the schema operations instead of sending them one-by-one, then hand the resulting MigrationOperation[] to the server's batch apply endpoint for a single-transaction apply. Backfills inside up() still run live against the current server state.

scripts/collect-migration.ts
import { createReoptEavClient } from "@reopt-ai/brandapp-sdk/eav";
import { runMigrationCollecting } from "@reopt-ai/brandapp-sdk/eav/migrate";
import migration from "./migrations/20260601_rename_tier_to_level";

const eav = createReoptEavClient({
  clientId: process.env.BRANDAPP_CLIENT_ID!,
  clientSecret: process.env.BRANDAPP_CLIENT_SECRET!,
  brandappId: process.env.BRANDAPP_ID!,
});

const { operations, durationMs } = await runMigrationCollecting(eav, migration);

// operations: MigrationOperation[] captured from ctx.schema.* calls, in order:
//   [{ kind: "addAttribute", entity: "contacts", name: "level", def: {...} },
//    { kind: "removeAttribute", entity: "contacts", name: "tier" }]
// Hand the array to POST /v1/brandapp/{id}/migrations/apply for an atomic,
// single-transaction apply. (Backfills inside up() already ran live.)
console.log(`Buffered ${operations.length} op(s) in ${durationMs}ms`);

createMigrationContext()

Build the { schema, records, log } context yourself for ad-hoc or scripted runs that don't need the defineMigration() wrapper. The runners construct this context internally.

scripts/ad-hoc-migration.ts
import { createReoptEavClient } from "@reopt-ai/brandapp-sdk/eav";
import { createMigrationContext } from "@reopt-ai/brandapp-sdk/eav/migrate";

const eav = createReoptEavClient({
  clientId: process.env.BRANDAPP_CLIENT_ID!,
  clientSecret: process.env.BRANDAPP_CLIENT_SECRET!,
  brandappId: process.env.BRANDAPP_ID!,
});

// ctx exposes the same { schema, records, log } a migration's up() receives —
// useful for one-off scripts that don't need the defineMigration() wrapper.
// (runMigration / runMigrationCollecting build this context internally — reach
// for createMigrationContext only when you want manual control.)
const ctx = createMigrationContext(eav, { log: console.log });

await ctx.schema.addAttribute("contacts", "newsletter", {
  dataType: "boolean",
  label: "Newsletter opt-in",
});
await ctx.records.backfill("contacts", {
  transform: () => ({ newsletter: false }),
});