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