BrandApp Playground

Schema Definition

Define your EAV data model with type-safe schema definitions.

defineEntity()

Define an entity with its attributes, types, and validation rules.

lib/eav.schema.ts
import { defineEntity } from "@reopt-ai/brandapp-sdk/eav/schema";

const contacts = defineEntity({
  name: "contacts",
  description: "Contact management",
  icon: "👤",
  attributes: {
    name: {
      dataType: "string",
      label: "Name",
      isRequired: true,
    },
    email: {
      dataType: "email",
      label: "Email",
      isRequired: true,
      isUnique: true,
    },
    phone: {
      dataType: "string",
      label: "Phone",
    },
    status: {
      dataType: "select",
      label: "Status",
      options: [
        { value: "active", label: "Active" },
        { value: "inactive", label: "Inactive" },
      ] as const,
    },
  },
});

defineSchema()

Combine multiple entities into a schema that can be synced with the server.

lib/eav.schema.ts
import { defineSchema, defineEntity } from "@reopt-ai/brandapp-sdk/eav/schema";

const contacts = defineEntity({ /* ... */ });
const products = defineEntity({ /* ... */ });

export default defineSchema({
  entities: { contacts, products },
});

Generate types (CLI)

Run reopt brandapp eav sync to push the schema and generate TypeScript types + ID mappings. The same flow is available programmatically via syncSchema(eav, schema, options) — see the Schema Sync page for the live walkthrough.

terminal
# Generate TypeScript types from your schema
npx reopt brandapp eav sync \
  -s ./lib/eav.schema.ts \
  -o ./lib/generated/eav.ts

# This generates:
# - Type definitions (e.g., ContactsValues)
# - Entity IDs map (ENTITY_IDS)
# - Attribute IDs map (ATTRIBUTE_IDS)

Generated Output

The sync command generates type definitions and ID constants.

lib/generated/eav.ts
// Auto-generated by @reopt-ai/cli
export type ContactsValues = {
  name: string;
  email: string;
  phone?: string;
  status?: "active" | "inactive";
};

export const ENTITY_IDS = {
  contacts: "clxyz...",
} as const;

export const ATTRIBUTE_IDS = {
  contacts: {
    name: "clxyz...",
    email: "clxyz...",
    phone: "clxyz...",
    status: "clxyz...",
  },
} as const;

InferValues (zero codegen)

Derive the record value type directly from a defineEntity() call — the runtime-inference alternative to the CLI-generated ContactsValues type.

lib/eav.schema.ts
import {
  defineEntity,
  type InferValues,
} from "@reopt-ai/brandapp-sdk/eav/schema";

const contacts = defineEntity({
  name: "contacts",
  attributes: {
    name: { dataType: "string", label: "Name", isRequired: true },
    email: { dataType: "email", label: "Email", isRequired: true },
    status: {
      dataType: "select",
      label: "Status",
      options: [
        { value: "active", label: "Active" },
        { value: "inactive", label: "Inactive" },
      ] as const,
    },
  },
});

// No `reopt brandapp eav sync` required — the type is inferred in place.
type ContactsValues = InferValues<typeof contacts>;
// { name: string; email: string; status?: "active" | "inactive" }

JSON Schema

Convert an entity or the whole schema to draft-07 JSON Schema — handy as an AI tool/function parameter schema, a form-builder spec, or a validator input.

lib/json-schema.ts
import {
  entityToJsonSchema,
  schemaToJsonSchema,
} from "@reopt-ai/brandapp-sdk/eav/schema";
import schema from "@/lib/eav.schema";

// A single entity → object schema
const contactsJson = entityToJsonSchema(schema.entities.contacts);
// {
//   type: "object",
//   title: "contacts",
//   properties: {
//     name: { type: "string", title: "Name" },
//     email: { type: "string", format: "email", title: "Email" },
//     status: { type: "string", enum: ["active", "inactive"], title: "Status" },
//   },
//   required: ["name", "email"],
// }

// The whole schema → one object per entity (great for AI structured output)
const fullJson = schemaToJsonSchema(schema);
// { $schema: "...draft-07...", type: "object", properties: { contacts: {...} } }

Drift detection

Compare a build-time schema hash against the live server with verifyEavSchema() so a forgotten migration surfaces as a 503 in your /api/health route.

app/api/health/route.ts
import {
  createReoptEavClient,
  verifyEavSchema,
} from "@reopt-ai/brandapp-sdk/eav";

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

export async function GET() {
  // localHash is embedded at build time (e.g. NEXT_PUBLIC_BRANDAPP_EAV_HASH).
  const localHash = process.env.NEXT_PUBLIC_BRANDAPP_EAV_HASH ?? "";
  const result = await verifyEavSchema({ client: eav, localHash });

  // result: { ok, localHash, remoteHash, drift?, schemaCount, fieldCount, ... }
  if (!result.ok) console.warn(result.drift);
  return Response.json({ eav: result }, { status: result.ok ? 200 : 503 });
}

// The build step computes localHash from the canonical schema shape:
//   import { computeEavSchemaHash } from "@reopt-ai/brandapp-sdk/eav";
//   const hash = await computeEavSchemaHash(canonicalSchemas);

computeDiff()

Preview exactly what a sync would change — create / update / delete / unchanged — by diffing the local schema against the live entities.

scripts/preview-diff.ts
import { computeDiff } from "@reopt-ai/brandapp-sdk/eav/schema";
import { getEav } from "@/lib/eav";
import schema from "@/lib/eav.schema";

const eav = await getEav();

// Server entities WITH attributes (list() is metadata-only, so hydrate via get())
const summaries = await eav.entities.list();
const serverEntities = await Promise.all(
  summaries.map((e) => eav.entities.get(e.id)),
);

const diff = computeDiff(schema, serverEntities, { deleteOrphans: false });
if (diff.hasChanges) {
  for (const entity of diff.entities) {
    console.log(entity.type, entity.key); // "create" | "update" | "delete" | "unchanged"
  }
}