Standards Docs
Tutorial — Build a CRM

Chapter 1 — schema

Define contact and company objects with attributes, a relation, and register them.

The schema lives at packages/schema/src/objects/ in the recommended monorepo template. Start with two files: contact.ts and company.ts.

Define the contact object

// @noverify
import { object, text, phone, select, status } from "@stndrds/schema";

export const contactDefinition = object({ name: "contacts", label: "Contact" })
  .icon("users")
  .pluralLabel("Contacts")
  .labelExpression("{{ lastName }} {{ firstName }}")
  .attribute(text({ name: "firstName", label: "First name" }).required())
  .attribute(text({ name: "lastName", label: "Last name" }).required())
  .attribute(text({ name: "email", label: "Email" }).optional())
  .attribute(phone({ name: "phone", label: "Phone" }).optional())
  .attribute(
    select({ name: "kind", label: "Kind" })
      .optional()
      .options([
        { id: "client", label: "Client", value: "client", color: "blue" },
        { id: "lead", label: "Lead", value: "lead", color: "yellow" },
        { id: "partner", label: "Partner", value: "partner", color: "green" },
      ])
  )
  .attribute(
    status({ name: "stage", label: "Stage" })
      .optional()
      .options([
        { id: "prospect", label: "Prospect", value: "prospect", color: "gray", group: "idle" },
        { id: "engaged", label: "Engaged", value: "engaged", color: "blue", group: "in_progress" },
        { id: "won", label: "Won", value: "won", color: "green", group: "finished" },
      ])
  );

About system entities

Everything you declare in code defaults to system: true. Users can add their own custom attributes alongside, but they cannot delete or modify the ones declared in your codebase — this protects the structural contract your application depends on. To opt out (tests, seeds, and fixtures only), chain .runtime() on the builder.

Define the company object

// @noverify
import { object, text, relation } from "@stndrds/schema";
import { contactDefinition } from "./contact";

export const companyDefinition = object({ name: "companies", label: "Company" })
  .icon("building")
  .pluralLabel("Companies")
  .labelExpression("{{ name }}")
  .attribute(text({ name: "name", label: "Name" }).required())
  .attribute(text({ name: "website", label: "Website" }).optional())
  .attribute(
    relation({ name: "contacts", label: "Contacts" })
      .to("contacts")
      .many()
      .optional()
  );

Register both objects

// @noverify
import { registry } from "@stndrds/schema";
import { contactDefinition } from "./contact";
import { companyDefinition } from "./company";

registry.register(contactDefinition);
registry.register(companyDefinition);

export { registry };

Register contactDefinition before companyDefinition. The relation on company targets the "contacts" object, so it must already be present in the registry when company is registered.


Next: Chapter 2 — Backend

On this page